자바생
article thumbnail
Published 2022. 12. 29. 20:26
Generic & Wildcard Java
728x90

Generic vs Wildcard

List<?> list // wildcard
List<T> list // generic

Generic은 타입이 정해지면 그 타입이 무엇인지 알고 그 이후에 사용하겠다는 뜻이고,

Wildcard는 타입도 모르겠고, 알 필요 없으니까 뭐든 들어가도 된다라는 뜻을 가지고 있습니다.

 

generic method, wildcard method

static <T> void method(T t) {

}

static <T extends Comparable> void method(List<T> list) {

}

static void method(List<?> t) {

}

static void method(List<? extends Comparable> list) {

}

메서드를 보면 generic 메서드와 wildacard 메서드의 차이를 알 수 있습니다.

generic은 메서드 내에서 여러 번 명시해야하지만 wildcard는 여러 번 사용하지 않습니다.

 

wildcard의 sub type, super type

static void genericList(List<Object> list) {
    list.forEach(s -> System.out.println(s));
}
static void wildcardList(List<?> list) {
    list.forEach(s -> System.out.println(s));
}

@Test
void genericVsWildcard() throws Exception {
    List<Integer> list = List.of(1, 2, 3);

    wildcardList(list);
    genericList(list);
}

genericList는 컴파일 에러가 발생하게 됩니다. 앞서 제네릭에서 type argument는 sub type과 super type에 영향을 끼치지 않기 때문에 Integer와 Object는 실제로 상속관계이지만 제네릭에서는 아무 영향이 없습니다.

그래서 List<Integer>는 List<Object>로 convert 될 수 없다는 에러가 발생하게 됩니다.

 

 

<? extends Object>와 ? 차이

List<? extends Object> list;
List<?> list;

<? extends Object>는 Object의 sub type만이 올 수 있고, Object 클래스에 정의되어 있는 기능만 가져다가 사용할 수 있습니다. 예로 equals(), hashCode(), toString() 등

 

?는 '?' 안에 어떤 타입이 오든 상관없습니다.

 

둘의 차이는 문법적으론 다르지만 의미상 같다고 할 수 있습니다.

 

bounded wildcard type 이해하기

wildcard를 공부할 때마다  <? extends E>, <? super E> 이런 것들이 매번 헷갈렸는데, 이번 기회를 통해 제대로 공부하려고 합니다.

class Box<T>{
    private T ob;

    public void setOb(T ob) {
        this.ob = ob;
    }

    public T getOb() {
        return ob;
    }
}

class BoxHandler{
    public static void outBox(Box<? extends Toy> box) {
        Toy t = box.getOb();
        System.out.println(t);
        box.setOb(new Toy()); //compile error
    }

    public static void inBox(Box<? super Toy> box, Toy n) {
        box.setOb(n);
        Toy ob = box.getOb(); //compile error
    }
}
//윤성우의 열혈 Java

 

 

outBox의 setOb()

outBox의 setOb에서는 왜 컴파일 에러가 발생할까요?

 

wildcard에 Toy의 sub class인 Robot이 온다고 가정해 보겠습니다.

그렇다면 Box<T>는 Robot이 되고, box.setOb(new Toy()) == Robot.setOb(new Toy())와 같다고 할 수 있습니다.

sub class에 super class를 넣는 것이기 때문에 당연히 컴파일 에러가 발생하게 됩니다.

 

inBox의 getOb()

box.getOb()에는 문제가 없지만 Toy로 받는 것이 문제가 됩니다.

 

Toy의 super class로 Wood가 오면 Box의 T는 Wood가 됩니다.

즉, Toy ob = Wood.getOb()가 되는데 하위 타입이 상위 타입을 받을 수 없기 때문에 컴파일 에러가 발생하게 됩니다.

 

왜 사용하는 것일까?

bounded wildcard type은 왜 사용할까요?

 

앞서 제네릭은 불공변 방식으로 List<Integer>와 List<Number>는 아무 관계가 없습니다.

하지만 List<Number>에 List<Integer>를 넣을 수 있도록 유연하게 사용하게 하는 bounded wildcard type을 제공합니다.

 

"유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라"
- 이펙티브 자바 Item 31

 

PECS

그렇다면 언제 extends를 쓰고, super를 쓰는 것일까요?

 

PECS라는 공식(?)이 있는데 producer-extends, consumer-super의 뜻으로 매개변수화 타입 T가 생산자면 extends를, 소비자면 super를 사용하라고 합니다.

 

생산자, 소비자 뜻을 알기 위해 Box 코드를 다시 봐보겠습니다.

 

outBox에서는 extends로 제한했습니다. 이때 get은 문제가 안 됐지만 set에서 컴파일 에러가 발생했습니다.

이렇듯 참조하는 인스턴스 T로부터 대상을 "꺼내는" 작업만이 허용되고, 이를 생산자라고 합니다.

 

inBox에서는 super로 제한했습니다. inBox에서의 set에는 문제가 없지만 get에서 문제가 생겼습니다.

이렇듯 참조하는 인스턴스 T를 대상으로 "넣는" 작업만 허용되어 소비자라고 합니다.

 

공식 문서에서의 사용 예

Collections 클래스의 copy 메서드를 보면 dest는 super, src는 extends로 제한하고 있습니다.

코드를 자세히 보면 src에서는 값을 꺼내고 있고(get), dest에는 값을 넣고(set) 있습니다.

 

위의 Box에서의 예시를 보면 꺼내는 작업을 하고 있는 생산자인 src는 extends를 사용하고, 넣는 작업을 하고 있는 소비자인 dest는 super를 사용하고 있습니다.

 

Capture 에러

wildcard type은 구체적인 타입 정보를 알 수 없기 때문에 "정보"에 관한 것을 사용할 수 없습니다. 즉, 구체적인 타입에서 사용하는 메서드나 data를 알 수 없습니다. 정보를 사용하려고 한다면 capture 에러가 발생하게 됩니다.

 

그래서 wildcard를 사용하면서 구체적인 타입의 정보를 알아야 하는 상황이 올 때, 제네릭으로 다시 변환시키거나 helper 메서드를 사용해야 합니다.

 

public class Generics {

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        reverse1(list);
        System.out.println(list);
    }

    static <T> void reverse1(List<T> list) {
        List<T> temp = new ArrayList<>(list);
        for (int i = 0; i < list.size(); i++) {
            list.set(i, temp.get(list.size() - i - 1));
        }
    }

    static void reverse2(List<?> list) {
        List<?> temp = new ArrayList<>(list);
        for (int i = 0; i < list.size(); i++) {
            list.set(i, temp.get(list.size() - i - 1)); //set이 값을 요구하는 부분
        }
    }

    static void reverse3(List<?> list) {
        reverseHelper(list);
    }

    private static <T> void reverseHelper(List<T> list) {
        List<T> temp = new ArrayList<>(list);
        for (int i = 0; i < list.size(); i++) {
            list.set(i, temp.get(list.size() - i - 1));
        }
    }
}

 

reverse 1은 제네릭 타입을 사용하기 때문에 구체적은 정보 ( set() )를 사용해도 무방합니다.

 

reverse 2는 wildcard 타입을 사용하기 때문에 구체적인 정보를 사용하려는 set()을 사용하려면 capture 에러가 발생하게 됩니다.

 

reverse 3(reverse 2의 해결 방법)은 capture 에러를 해결하기 위해서는 제네릭 타입으로 변경하거나 helperMethod를 사용해야 합니다. 하지만 제네릭 타입으로 변경하기보다는 helperMethod를 지향한다고 합니다.

 

왜냐하면 api를 사용하는 클라이언트가 코드를 볼 때, <T>를 직접 노출하기 때문에 helperMethod를 지향한다고 합니다.

 

그래서 helper 메서드를 통해 와일드카드 type인 list를 제네릭으로 받아서, 제네릭 파라미터가 적용된 T로 capture를 하게 되어 capture 에러가 해결됩니다.

 

정리

이번 글을 통해서 제네릭과 와일드카드의 차이가 무엇인지 알게 되었고, Java doc을 보면 자주 보이는 와일드카드를 사용한 상한, 하한 타입을 알 수 있었습니다.

이제 공식 문서를 보면서 공부했던 내용을 떠올리며 해당 파라미터는 생산자, 소비자 역할을 하기 때문에 extends, super를 사용했구나라고 생각하시면 이해가 더 잘될 겁니다.

 

REFERENCES

토비의 봄 Generics(1)

토비의 봄 Generics(2)

Effective Java 3/e

윤성우의 열혈 Java

728x90

'Java' 카테고리의 다른 글

파라미터에 Optional은 왜 안티패턴?  (0) 2023.03.25
unmodifiableList & copyOf & 방어적 복사  (0) 2023.02.21
Generic in Java  (2) 2022.12.17
JDK ? JRE ?  (0) 2022.09.15
정적 메서드는 왜 오버라이딩 되지 않을까?  (0) 2022.08.29
profile

자바생

@자바생

틀린 부분이 있다면 댓글 부탁드립니다~😀

검색 태그