Java Generic - Wildcard

제네릭 타입에서 사용하는 와일드카드가 있다.

와일드카드는 "?" 특수문자를 의미하며, 제네릭 타입에서는 여러 타입으로 들어오도록 한다.


예제

Box.class

public class Box<T> {
    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}
  • Box 객체로 정의해 사용한다.

Wildcard.class - 제네릭 메소드만

public class Wildcard {
    static <T> void print(Box<T> box) {
        System.out.println("T = " + box.get());
    }

    static <T extends Animal> void printAbs(Box<T> box) {
        T t = box.get();
        System.out.println("T.getName = " + t.getName());
    }

    static <T extends Animal> T printReturnAbs(Box<T> box) {
        T t = box.get();
        System.out.println("T.getName = " + t.getName());
        return t;
    }

}
  • Wildcard 에서 출력한다. Animal 부모 객체를 미리 정의해두어야한다.

Main.class

public class Main {
    public static void main(String[] args) {
        Box<Object> obj = new Box<>();
        Box<Dog> dog = new Box<>();
        Box<Cat> cat = new Box<>();

        dog.set(new Dog("강아지", 15));

        Wildcard.print(dog);
        Wildcard.printAbs(dog); // 상속 객체 체크
        Dog dogReturn = Wildcard.printReturnAbs(dog);
    }
}
  • 일반 제네릭 쓰듯이 사용할 수 있다.

다음으로 와일드카드를 사용해보자.


제네릭 와일드 카드 예제

wildcard 객체에서 다음 메소드를 추가하도록 한다.

Wildcard.class - 메소드 추가

static void printWildcard(Box<?> box) {
    System.out.println("? = " + box.get());
}
  • 매개변수로 Box<?> 제네릭 타입을 사용하고 있다.
  • 파라미터는 Object 메소드만 사용 가능하다.

제네릭 타입의 와일드 카드 또한 파라미터 제한을 걸 수 있다.

Wildcard.class - 메소드 추가 (파라미터 상한)

static void printWildcardAbs(Box<? extends Animal> box) {
    Animal animal = box.get();
    System.out.println("T.getName = " + animal.getName());
}
  • 제네릭 타입서 extends 키워드를 사용하고 있다.
  • 파라미터는 Animal 메소드가 사용 가능하다.

Wildcard.class - 메소드 추가

static Animal printWildcardReturnAbs(Box<? extends Animal> box) {
    Animal animal = box.get();
    System.out.println("T.getName = " + animal.getName());
    return animal;
}
  • 반환을 Animal로 변경하였다.

Main.class

Wildcard.print(dog);
Wildcard.printWildcard(dog);

Wildcard.printAbs(dog); // 상속 객체 체크
Wildcard.printWildcardAbs(dog);

Dog dogReturn1 = Wildcard.printReturnAbs(dog);
Animal dogReturn2 = Wildcard.printWildcardReturnAbs(dog);

T = Animal{name='강아지', size=15}
? = Animal{name='강아지', size=15}
T.getName = 강아지
T.getName = 강아지
T.getName = 강아지
T.getName = 강아지

  • 메인에서 각각 생성한 메소드를 실행하였다.
  • 와일드카드를 사용한 메소드는 이름을 Whildcard를 붙이고 메소드에는 ? 키워드로 정의되어 있다.
💡
와일드카드는 제네릭 타입 선언하는 것이 아니다.
이미 정의한 제네릭 타입에서 와일드카드를 덧붙인 것이다.

제네릭 메소드와 와일드카드 비교

제네릭 타입을 편리하게 사용하도록 정의가 필요없는(비제한) 와일드카드이다.

예제 코드의 메소드를 살펴보자

static <T> void print(Box<T> box) {
    System.out.println("T = " + box.get());
}

static void printWildcard(Box<?> box) {
    System.out.println("? = " + box.get());
}
  • 두 메소드는 언뜻 비슷해 보인다.
    • 제네릭 메소드로 print 정의하였다.
    • 일반 메소드로 printWildcard 정의하였다.
  • 제네릭 타입 Box<Dog> 선언한 변수가 일반 메소드 인자로 받아 접근하도록 한다.
    • 참고로 상속은 Box<?> 부모인 Object 객체로 모두 받을 수 있다.
    • 즉, <?> 키워드는 <? extends Object> 줄인 것과 같다.
  • <?> 만 사용하고 모든 타입을 받을 수 있으므로 비제한 와일드카드라고 부른다.

와일드카드 단점

static Animal printWildcardReturnAbs(Box<? extends Animal> box) {
    Animal animal = box.get();
    System.out.println("T.getName = " + animal.getName());
    return animal;
}

파라미터만 와일드카드로 제네릭타입 인자만 전달받아 사용하는 일반 메소드이다.
그로인해 제네릭의 장점인 다양한 타입으로 반환하는 방법을 사용할 수 없다.

일반 메소드를 호출한 호출부 코드롤 살펴보자

Animal dogReturn2 = Wildcard.printWildcardReturnAbs(dog);

분명하게 Animal 타입을 선언한 변수에게 와일드카드 인자로 처리하도록 일반 메소드로부터 바인딩하여 할당하고 있다.
개발하는 입장에서 추상적 클래스인 Animal 타입을 사용하고 싶지 않을 것인데, Dog, Cat 으로 타입 변수로 받지 못한다는 단점이 있다.

재차 반복하겠지만, 이런 경우 제네릭 타입을 사용해야한다.


하한 와일드카드

예제를 이어서 호출부 코드를 변경해보도록 한다.

Main.class

public class Main {
    public static void main(String[] args) {
        Box<Object> obj = new Box<>();
        Box<Animal> animal = new Box<>();
        Box<Dog> dog = new Box<>();
        Box<Cat> cat = new Box<>();

        wildcardExt(obj);
        wildcardExt(animal);
        // wildcardExt(dog); 사용불가
        // wildcardExt(cat); 사용불가
    }

    public static void wildcardExt(Box<? super Animal> animal) {
        System.out.println(animal);
    }
}
  • 신규 메소드에 변수에서 제네릭 <?>을 super 키워드로 붙여 선언하였다.
  • 신규 메소드 호출로 Object, Animal 타입 변수로 할당.
    • Dog, Cat 제네릭 변수로는 할당할 수 없음

코드와 같이 Object, Animal 타입은 받고 있으나 그 하위 자식 변수에게는 사용할 수 없도록 한 것을 볼 수 있다.

부모 클래스만 전달 받고 자식 클래스에게는 전달 받지 못하도록 하는 것이 하한 와일드 카드이다.

💡
와일드카드 하한은 제네릭 타입, 제네릭 메소드 <?> 정의에서 사용할 수 없다.

제네릭 메소드, 와일드카드 동작 과정

제네릭 메소드는 다음과 같은 절차를 거친다.

제네릭 메소드 실행 과정

  • Wildcard.print(dog);
    • 호출부에서 제네릭 타입이 파라미터로 전달하게 된다.
  • static <T> void print(Box<T> box) { ... }
    • 제네릭 메소드는 전달 받은 Box 객체 파라미터를 받게 된다.
    • Box 타입이 무엇인 추론을 해주어야 한다.
  • static <Dog> void print(Box<Dog> box) { ... }
    • 제네릭 메소드는 인자를 추론한 결과 Dog가 타입이 결정된다.
  • static void print(Box<Dog> box) { ... }
    • 추론이 완료되었다면 메소드를 실행한다.

와일드카드 동작 과정

  • Wildcard.print(dog);
    • 호출부에서 제네릭 타입이 파라미터로 전달하게 된다.
  • static void print(Box<?> box) { ... }
    • 추론 과정 없이 타입을 Object로 받으므로 바로 실행한다.

와일드 카드 활용처

제네릭 메소드는 타입 추론 과정이 복잡하다. 성능 하락도 예상되므로 단순하게 동작하고 반환이 없는 경우 와일드카드 사용을 권장한다.

만약, 반환 타입이 다형성 있게 인터페이스를 사용한다면 제네릭 메소드로 사용한다.
(계산이 많이 요구하는 구현 코드인 경우 지양해야함)