[자바의 정석] 와일드카드

June·2021년 1월 5일
1

자바

목록 보기
29/36

매개변수에 과일박스를 대입하면 주스를 만들어서 변환하는 Juicer라는 클래스가 있고, 이 클래스에는 과일을 주스로 만들어서 변환하는 makeJuice()라는 static메서드가 다음과 같이 정의되어 있다고 가정하자.

class Juicer {
    static Juice makeJuice(FruitBox<Fruit> box) {
        String tmp = "";
        for (Fruit f : box.getList()) {
            tmp += f + " ";
        }
        return new Juice(tmp);
    }
}   

Juicer 클래스는 지네릭 클래스가 아닌데다, 지네릭 클래스라고 해도 static 메서드에서는 타입 매개변수 T를 매개변수에 사용할 수 없으므로 아예 지네릭스를 적용하지 않던가, 위와 같이 타입 매개변수 대신, 특정 타입을 지정해줘야 한다.

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();
	...
System.out.println(Juicer.makeJuice(fruitBox));	//OK. FruitBox<Fruit>
System.out.println(Juicer.makeJuice(appleBox));	//에러. FruitBox<Apple>

이렇게 지네릭 타입을 'FruitBox<Fruit>'로 고정해 놓으면, 위의 코드에서 알 수 있듯이 'FruitBox<Apple>'타입의 객체는 makeJuice()의 매개변수가 될 수 없으므로, 여러 가지 타입의 매개변수를 갖는 makeJuice()를 만들 수 밖에 없다.

    static Juice makeJuice(FruitBox<Fruit> box) {
        String tmp = "";
        for (Fruit f : box.getList()) {
            tmp += f + " ";
        }
        return new Juice(tmp);
    }
    static Juice makeJuice(FruitBox<Apple> box) {
        String tmp = "";
        for (Fruit f : box.getList()) {
            tmp += f + " ";
        }
        return new Juice(tmp);
    }

그러나 위와 같이 오버로딩하면, 컴파일 에러가 발생한다. 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문이다.** 지네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해버린다. 그래서 위의 두 메서드는 오버로딩이 아니라 '메서드 중복 정의'이다. 이럴 때 사용하기 위해 고안된 것이 바로 '와일드 카드**'이다. 와일드 카드는 기호 '?'로 표현하는데, 와일드 카드는 어떠한 타입도 될 수 있다. '?'만으로는 Object 타입과 다를게 없으므로, 다음과 같이 'extends'와 'super'로 상한과 하한을 제한할 수 있다.

  • <? extends T> 와일드 카드의 상항 제한. T와 그 손자들만 가능
  • <? super T> 와일드 카드이ㅡ 하한 제한. T와 그 조상들만 가능
  • <?> 제한 없음.모든 타입이 가능. <> extends Object>와 동일

와일드 카드를 사용해서 makeJuice()의 매개변수 타입을 FruitBox<Fruit>에서 FruitBox <? extends Fruit>으로 바꾸면 다음과 같이 된다.

    static Juice makeJuice(FruitBox<? extends Fruit> box) {
        String tmp = "";
        for (Fruit f : box.getList()) {
            tmp += f + " ";
        }
        return new Juice(tmp);
    }

이제 이 메서드의 매개변수로 FruitBox<Fruit>뿐만 아니라, FruitBox<Apple>FruitBox<Grape>도 가능하게 된다.

매개변수의 타입을 FruitBox<? extends Object>로 하면, 모든 종류의 FruitBox가 이 메서드의 매개변수로 가능해진다. 대신, 전과 달리 box의 요소가 Fruit의 자손이라는 보장이 없으므로 아래의 for문에서 box에 저장된 요소를 Fruit타입의 참조변수로 못받는다.

static Juice makeJuice(FruitBox<? extends Object> box) {
    String tmp = "";

    for(Fruit f : box.getList()) tmp += f + " "; //에러. Fruit이 아닐 수 있음
    return new Juice(tmp);
}

그러나 실제로 테스트 해보면 문제없이 컴파일되는데 그 이유는 바로 지네릭 클래스 FruitBox를 제한했기 때문이다.

class FruitBox<T extends Fruit> extends Box<T> {}

컴파일러는 위 문장으로부터 모든 FruitBox의 요소들이 Fruit의 자손이라는 것을 알고있기에 문제삼지 않는다.

와일드 카드를 이용한 정렬

static <T> void sort(List<T> list, Comparator<? super T> c)

'static' 옆에 있는 ''는 메서드에 선언된 지네릭 타입이다. 이런 메서드를 지네릭 메서드라고 하는데, 다음 단원에서 배울 것이다. 첫 번째 매개변수는 정렬할 대상이고, 두 번째 매개변수는 정렬할 방법이 정의된 Comparator 이다. Comparator의 지네릭 타입에 하한 제한이 걸려있는 와일드 카드가 사용되었다. 먼저 다음과 같이 와일드 카드를 사용하지 않았다고 가정해보자.

static <T> void sort(List<T> list, Comparator<T> c)

만일 타입 매개변수 T에 Apple이 대입되면, 위의 정의는 아래와 같이 바뀔 것이다.

static void sort(List<Apple> list, Comparator<Apple> c)

이것은 List<Apple>을 정렬하기 위해서는 Comparator<Apple>이 필요하다는 것을 의미한다. 그래서 Comparator<Apple>을 구현한 AppleComp 클래스를 아래처럼 정의하였다.

class AppleComp implements Comparator<Apple> {
    public int compare(Apple t1, Apple t2) {
        return t2.weight - t1.weight;
    }
}

하지만 Apple 대신 Grape가 대입된다면 또 Comparator<Grape>가 필요하다.
AppleComp와 GrapeComp는 타입만 다를 뿐 완전히 같은 코드이다. 코드의 중복도 문제지만, 새로운 Fruit의 자손이 생길 때마다 위와 같은 코드를 반복해서 만들어야 한다는 것이 더 문제이다.

static <T> void sort(List<T> list, Comparator<? super T> c)

위의 문장에서 타입 매개변수 T에 Apple이 대입되면, 다음과 같이 된다.

static void sort(List<Apple> list, Comparator<? super Apple> c)

매개변수의 타입이 Comparator<? super Apple>이라는 의미는 Comparator의 타입 매개변수로 Apple과 그 조상이 가능하다는 뜻이다. 즉, Comparator<Apple>, Comparator<Fruit>,Comparator<Object>중의 하나가 두 번째 매개변수로올 수 있다는 뜻이다.

그래서 아래와 같이 FruitComp를 만들면, List과 List를 모두 정렬할 수 있다.

class AppleComp implements Comparator<Fruit> {
    public int compare(Apple t1, Apple t2) {
        return t2.weight - t1.weight;
    }
}

이러한 장점 때문에 Comparator에는 항상 <? super T>가 습관적으로 따라 붙는다.

0개의 댓글