[Java] 제네릭(Generic) 심화

LDB·2024년 12월 30일
0

Java

목록 보기
6/6
post-thumbnail

시작하기에 앞서서

지난글에는 제네릭이란 무엇이고 제네릭을 기본적으로 어떻게 사용하는지 알아 보았다. 그렇다면 이제 약간 심도깊게 알아보겠다.

원래는 제네릭 와일드카드도 같이 소개하려고 했다. 하지만 제네릭의 내용을 한 게시글에 정리하기에는 너무 많아서 둘로 나누어 정리하는 것이 좋겠다고 판단했다.

제네릭과 와일드 카드

전에 작성한 게시글에서 정리한 내용을 간단히 정리하자면

  • 제네릭은 잘못된 타입을 런타임이 아닌 컴파일에서 거를 수 있다.
  • 제네릭은 wrapper 클래스와 일반 클래스가 참조 타입이 될 수 있다.

이정도 이겠다, 하지만 반대로 특정 클래스 범위만 허용하고 나머지는 제한 할 수 없는가에 대해 생각해볼 수 있다. 그럴 때 사용하는 것이 extends, super, ?(와일드 카드)이다. 와일드 카드는 생소할 수 있는데 알수 없는 타입 이라는 의미이다. 뒤에서 설명할 것이지만 일단 이렇게 사용한다고 생각하면 된다.

<T extends K>	// K와 K의 자손 타입만 가능 (T는 들어오는 타입으로 지정)
<T super K>		// K와 K의 부모 타입만 가능 (T는 들어오는 타입으로 지정)

<? extends K>	// K와 K의 자손 타입만 가능
<? super K> 	// K와 K의 부모 타입만 가능

<?> 			// 모든 타입이 가능. <? extends Object>랑 같은 의미

이해하기 쉽게 말하면 다음과 같이 부른다.

  • ? extends K : 상단 경계
  • ? super K : 하단 경계
  • <?> : 와일드 카드

주의점
T extends K 와 ? extends K는 비슷한 구조이지만 차이점이 있다. 유형 경계를 지정한다는 점은 같으나 T는 특정 타입으로 지정되지만 ?는 타입이 지정되지 않는다는 의미를 가지고 있다. 다음예시를 보면 알 수 있다.

/**
* Numberd와 이를 상속하는 Integer, Short, Double, Long 등의
* 타입이 지정될 수 있으며, 객체 혹은 메소드를 호출 할 경우 K는
* 지정된 타입으로 변환이 된다.
*/
<K extends Number>
/**
* Number와 이를 상속하는 Integer, Short, Double, Long 등의
* 타입이 지정될 수 있으며, 객체 혹은 메소드를 호출 할 경우 지정된 타입이 없기에
* 타입 참조를 할 수는 없다.
*/
<? extends Number>

그래서 특정타입의 데이터를 조작하고싶은 경우에는 K와 같이 특정 제네릭 인수로 지정을 해야한다.


extends (공변)


위와 같은 구조의 클래스가 있다고 가정해보자

<T extends two1> 	// two1, three1타입만 올 수 있다.
<T extends three2> 	// three2타입만 올 수 있다.
<T extends one> 	// one, two1, three1, two2, three2 타입이 올 수 있다.

<? extends two1> 	// two1, three1타입만 올 수 있다.
<? extends three2> 	// three2타입만 올 수 있다.
<? extends one>		// one, two1, three1, two2, three2 타입이 올 수 있다.

앞에서도 이야기했지만 위의 3줄은 T타입으로 변환이되지만 ?는 '타입이 지정되지 않는다'라는 의미이기 때문에 타입 참조가 안된다는 차이가 있다. 그리고 extends뒤에 오는 타입은 최상위 타입으로 한계가 명확하다.

그림으로 더 쉽게 이해하려면 이렇게 표현할 수 있겠다.

대표적예시로 Number라는 클래스가 있는데 정수,소수를 포함한 숫자를 표현하기 위한 클래스가 있다. 대표적으로 Integer, Long, Byte, Double, Float, Short 같은 wrapper 클래스들이 Number 클래스로 부터 상속을 받는다.


(실제로 내부를 보니 Number 클래스를 확장하고 있었다.)

이것을 이야기하는 이유는 다음코드를 보면 알 수 있다.

class generic <T extends Number> {
    
}

public class main {
    public static void main(String[] args) {
        generic<Integer> num1 = new generic<Integer>();
        
        // String은 Number에 포함되어있지 않는  
        // 클래스이기에 때문에 에러가 난다.
        // generic<String> str1 = new generic<String>();
    }
}

위의 코드를 보면 Integer는 Number를 상속받기에 객체 생성이 가능하다, 하지만 String은 완전 별개의 클래스이기에 객체 생성이 불가능하다.


super (반공변)


super도 비교하기위해 위와 같은 구조의 클래스가 있다고 가정해보자

<T super two1> 		// two1, one타입만 올 수 있다.
<T super three2> 	// three2, two2, one타입만 올 수 있다.
<T super one> 		// one타입이 올 수 있다.

<? super two1> 		// two1, one타입만 올 수 있다.
<? super three2> 	// three2, two2, one타입만 올 수 있다.
<? super one>		// one타입이 올 수 있다.

위의 내용은 extends와 비교했을 때 super는 자신보다 상위의 클래스를 호출 할 수 있다는 점이 다르다. 마찬가지로 그림으로 표현하자면 이렇게 표현할 수 있겠다. 말그대로 extends와 정반대인 것을 확인할 수 있다.

위에서 설명목적으로 <T super two1> 이런식으로 코드를 작성했지만 실제로는 <T super [타입]> 코드는 사실상 존재하지 않는다. 대신 <? super [타입]> 코드를 사용한다.

이유 : 만약 <T super HashMap>코드를 사용한다면 Type Erasure(컴파일에 제약조건을 정의하고 런타임에는 타입을 제거)로 Object로 변환된다, 그렇기에 추론되지 않는 T는 결국 Object와 다르지 않게 된다. 만약 Type Erasure가 이루어지지 않는다고 해도 문제가 있다. <T super HashMap>에서는 계층구조를 따라 T에 AbstractMap, Map, Cloneable, Serializable, Object가 모두 오게 되는데 이렇게 되면 T를 특정할 수 없기에 전혀 쓸모가 없어진다.

<? super T> 는 좀 있다 설명할 와일드 카드 부분에서 설명할 것이다.


<?> 와일드 카드

드디어 대망의 와일드 카드다. 와일드 카드 <?><? extends Object>와 같은 의미를 가진다고 앞에서 말했다. Object 타입은 자바의 모든 API및 사용자 클래스의 최상위 타입이다. 그렇기에 다음과 같이 나타낼 수 있겠다.

public class testClass {  }

public class testClass extends Object {  }

위의 두 클래스 선언 법으로 나타 냈는데 한마디로 우리들이 자바를 처음 배우고 클래스를 작성하면서 해왔던 행위들은 Object 객체를 암묵적으로 상속 받고 있었다 해도 과언이 아니다. 그렇다면 와일드 카드의 GET / SET 제약에 대해 알아보도록 하자

<? extends T> (상한 경계 방식)
<? extends T>방식은 extends를 사용하여 최상위 타입을 정의하고 상한 경계를 정의한다.

  • GET : pop클래스를 상한 경계로 주었을 경우 pop을 포함한 music과 Object는 값을 호출하는 것이 가능하지만 pop보다 하위클래스인 kpop클래스로 값을 꺼낼려는 경우에는 컴파일 에러가 발생한다.

  • SET : 하지만 값을 등록하려고 한다면 이야기가 완전히 달라진다. c가 pop의 하위 타입중에서 어떤 타입인지 모른다. 하위타입이 kpop이 될 수도 있고 아니면 jpop이라는 완전히 다른 또다른 하위 타입이 될 수 있기에 결정 할 수없다.

import java.util.*;

class music { 
    String name;
}

class pop extends music{ 
    pop(String name){
        this.name = name;
    }
}

class kpop extends pop{ 
    kpop(String name){
        super(name);
    }
}

class classic extends music{ 
    classic(String name){
        this.name = name;
    }
}

public class musicbox {
    public static void main(String[] args) {
        List<music> listMusic = new ArrayList<>();
        listMusic.add(new music());
        listMusic.add(new music());
        
        // music class는 pop의 하위 클래스가 아니기에 출력이 불가능하다.
        // printDataExtends(listMusic);

        List<pop> listpop = new ArrayList<>();
        listpop.add(new pop("pop1"));
        listpop.add(new pop("pop2"));
        printDataExtends(listpop);
    }

    void addlistExtends(List<? extends pop> c){
        // c.add(new Object()); 컴파일 에러!!!
        // c.add(new music()); 컴파일 에러!!!
        // c.add(new pop("popClass")); 컴파일 에러!!!
        // c.add(new kpop("kpopClass")); 컴파일 에러!!!
        // c.add(new classic("classicClass")); 컴파일 에러!!!
    }

    public static void printDataExtends(List<? extends pop> list) {

        for(Object obj : list){
            System.out.println(obj);
        }

        for(music obj : list){
            System.out.println(obj.name);
        }

        for (pop obj : list) {
            System.out.println(obj.name);
        }

        // kpop 클래스는 pop보다 상위 클래스가 아니기에 에러가 난다.
        /* for (kpop obj : list) {
            System.out.println(obj.name);
        } */
    }

}

만약 List<? extends pop> c 에 값을 추가 하고 싶으면 extends 대신에 super하한 경계를 사용해야한다.


<? super T> (하한 경계 방식)
<? super T>방식은 <? extends T> 반대라고 생각하면 되고 하한 경계라고 부른다.

  • GET : pop클래스를 하한 경계로 주었을 경우 부모 타입을 지정할 수 없기에 기존에 만든 music 클래스는 컴파일 에러가 발생한다. 하지만 Object타입은 Java에서 지원하는 모든객체중에 최상위 객체임이 명확하기 때문에 컴파일에러가 발생하지 않는다.

  • SET : pop클래스를 하한 경계로 주어졌고 pop을 포함한 하위 객체들은 추가가 가능하지만 pop보다 상위 객체들은 포함이 안된다.

import java.util.*;

class music { 
    String name;
}

class pop extends music{ 
    pop(String name){
        this.name = name;
    }
}

class kpop extends pop{ 
    kpop(String name){
        super(name);
    }
}

class classic extends music{ 
    classic(String name){
        this.name = name;
    }
}

public class musicbox {
    public static void main(String[] args) {
        List<music> listMusic = new ArrayList<>();
        listMusic.add(new music());
        listMusic.add(new music());
        printDataSuper(listMusic);

        List<pop> listpop = new ArrayList<>();
        listpop.add(new pop("pop1"));
        listpop.add(new pop("pop2"));
        printDataSuper(listpop);

        List<kpop> listkpop = new ArrayList<>();
        listkpop.add(new kpop("pop1"));
        listkpop.add(new kpop("pop2"));
        
        // listkpop은 pop의 부모 타입이 아니기 때문에 에러가 난다.
        // printDataSuper(listkpop);
    }

    void addlistSuper(List<? super pop> c){
        c.add(new pop("popClass"));
        c.add(new kpop("kpopClass"));
        // c.add(new Object()); 컴파일 에러!!!
        // c.add(new music()); 컴파일 에러!!!
        // c.add(new classic("classicClass")); 컴파일 에러!!!
    }

    public static void printDataSuper(List<? super pop> list) {
        for (Object obj : list) {
            System.out.println(obj);
        }

        // 명확한 부모객체가 아니기에 컴파일 에러가 난다.
        /* 
        for (music obj : list) {
            System.out.println(obj);
        }

        for (pop obj : list) {
            System.out.println(obj);
        }

        for (kpop obj : list) {
            System.out.println(obj);
        }  
        */
    }

}

와일드카드 주의점

1. 와일드 카드는 설계가 아닌 사용을 위한 것이다.

나도 처음에는 와일드 카드를 어디서든 사용이 가능한줄 알았는데 클래스나 인터페이스 제네릭을 설계할 때는 와일드 카드를 사용할 수 없다.

와일드 카드는 이미 만들어진 제네릭 클래스나 메서드를 사용할 때 이용하는 것으로 이해하면 되겠다. 예를들어 변수나 매개변수에 어떠한 객체의 타입 파라미터를 받을때 그에 대한 범위 한정을 정할때 사용된다고 보면 되겠다.

class Project<T> {
    public static <E> void run(List<? super E> l) {}
}

public class Main {
    public static void main(String[] args) {
        // ? 이기 때문에 가능하다.
		Project<?> s2= new Project<String>();
        
  		// Number가 Integer를 포함한 숫자 관련 클래스를 참조하기 때문에 가능하다.
        Project<? extends Number> s1 = new Project<Integer>();
        
        Project.run(new ArrayList<>());
    }
}

2. <?><Object>는 엄밀히 다른 개념이다.

위에서는 <?><Object>를 다르지 않은 것 처럼 설명 했으나 이 두가지는 엄밀히 말하자면 다른 개념이다, Object는 Object 하위면 모두 넣을 수 있지만 <?>는 null만 입력이 가능하다. 이것은 타입안정성을 지키기위한 제네릭의 특징으로 만약 모든 타입을 넣을 수 있게 되면 Integer타입의 리스트에 Double형을 추가하는 모순이 발생할 수 있기 때문이다.

public class main {
    public static void main(String[] args) {
        List<Integer> ints = new ArrayList<>();
        add(ints);
    }

    private static void add(List<?> ints){
        // 애초에 null 밖에 안들어가기 때문에 값이 들어갈 일이 없다.
        ints.add(1,null);
    }
    
}  

결론

이 게시글을 작성하기 까지 3일이 걸렸다. 그 만큼 제네릭은 아주 심도있고 다루기 힘든 내용이었다. 하지만 그 만큼 얻은 것이 많은거 같았다. 물론 이렇게 고생해서 작성한 게시글에도 허점이 분명 존재할 것이다. 그 부분은 제네릭에 대해 아주 잘알고 있는 개발자 지인에게 검토를 거치고 계속 수정할 계획이다.


참고 사이트

https://st-lab.tistory.com/153

https://inpa.tistory.com/entry/JAVA-☕-제네릭-와일드-카드-extends-super-T-완벽-이해#하한_경계_와일드카드_반공변

https://velog.io/@kimdy0915/기본형primitive-vs.-래퍼-클래스wrapper-class

https://velog.io/@kasania/Java-Generic에-대한-관찰-2

https://yarisong.tistory.com/48

https://pathas.tistory.com/160

(항상 감사합니다.)

profile
가끔은 정신줄 놓고 멍 때리는 것도 필요하다.

0개의 댓글