[Java] 제네릭 (Generic) 3

22. 제네릭 (Generic)

☑️ 와일드카드

와일드카드란?

  • 컴퓨터 프로그래밍에서 *, ? 와 같이 하나 이상의 문자들을 상징하는 특수 문자이다.
  • 와일드카드는 이미 만들어진 제네릭 타입을 활용하여 사용한다.
public class Box<T> {

    private T value;

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

    public T get() {
        return value;
    }
}
  • 예시를 위해 Box<T>라는 제네릭 타입을 만들었다.

비제한 와일드카드

static void printWildcard(Box<?> box) {
    System.out.println("? = " + box.get());
}
  • 제네릭 타입인 Box<T> Box<Dog>, Box<Cat> 와 같이 사용된다.
  • 와일드카드는 타입 인자가 정해진 제네릭 타입을 전달받아서 활용할 때 사용한다.
  • 와일드카드인 ? 를 사용하면 제한 없이 모든 타입을 다 받을 수 있다.
Box<Dog> dogBox = new Box<>();
dogBox.set(new Dog("멍멍이", 100));

WildcardEx.printWildcard(dogBox); // ? = Animal{name='멍멍이', size=100}
  • 와일드카드를 사용하지 않는 일반 메서드의 경우, 특정시점에 타입 매개변수에 타입 인자를 전달해서 타입을 결정해야 하는 복잡한 과정을 거친다.
  • 하지만 printWildcard() 와 같이 와일드카드인 ? 를 사용한다면 모든 타입을 받을 수 있기 때문에 타입을 결정하거나 복잡하게 작동하지 않는다.

상한 와일드카드

static void printWildcard(Box<? extends Animal> box) {
    Animal animal = box.get();
    System.out.println("이름 = " + animal.getName());
}
  • <? extends Animal> 와 같이 extends를 사용하여 와일드카드에 상한 제한을 둘 수 있다.
  • 위의 코드에서는 Animal 과 그 하위타입만 입력받고, 이외의 타입 입력 시 컴파일 오류가 발생한다.

하한 와일드카드

static void writeBox(Box<? super Animal> box) {
    box.set(new Dog("멍멍이", 100));
}
  • <? super Animal> 와 같이 super 을 사용하여 와일드카드에 하한 제한을 둘 수 있다.
  • 위의 코드에서는 Animal과 그 상위 타입만 입력받을 수 있다.
Box<Object> objBox = new Box<>();
Box<Animal> animalBox = new Box<>();
Box<Dog> dogBox = new Box<>();

writeBox(objBox);
writeBox(animalBox);
// writeBox(dogBox); // 하한이 Animal
  • Animal 타입의 하위 타입인 Box<Dog>는 전달할 수 없고, Box<Animal>과 상위 타입인 Box<Object>는 전달할 수 있다.

권장 방식

static <T extends Animal> T printAndReturnGeneric(Box<T> box) {
    T t = box.get();
    System.out.println("이름 = " + t.getName());
    return t;
}
Box<Dog> dogBox = new Box<>();
Dog dog = WildcardEx.printAndReturnGeneric(dogBox);
  • 제네릭타입을 사용한 printAndReturnGeneric() 의 경우, Box<Dog> 의 타입인 Dog 객체를 명확하게 반환한다.
static Animal printAndReturnWildcard(Box<? extends Animal> box) {
    Animal animal = box.get();
    System.out.println("이름 = " + animal.getName());
    return animal;
}
Box<Dog> dogBox = new Box<>();
Animal animal = WildcardEx.printAndReturnWildcard(dogBox);
  • 하지만, 와일드카드를 사용한 printAndReturnWildcard() 의 경우, Box<Dog> 를 인자로 전달하여도 Animal 타입으로 반환이 된다.
  • 와일드카드는 이미 만들어진 제네릭 타입을 전달받아서 활용하는 것이기 때문에 타입 인자를 통해서 메서드의 타입을 변경할 수 없다.
  • 메서드의 타입을 특정 시점에서 변경하고자 한다면 제네릭 타입이나, 제네릭 메서드를 이용하는 것이 좋다.
  • 이런 경우가 아니라면, 더 단순한 와일드카드 사용을 권장한다고 한다.

☑️ 타입 이레이저

타입 이레이저란?

  • 자바에는 컴파일 전인 .java와 컴파일 이후인 자바 바이트코드 .class가 존재하며, 런타임에는 .class 로 자바를 실행한다.
  • 자바 컴파일러는 컴파일이 끝나면 제네릭과 관련된 정보를 삭제한다.
  • 즉, 제네릭 타입은 컴파일 시점에만 존재하고, 런타임 시에는 제네릭 정보가 지워진다.

상한 제한이 없는 경우

컴파일 전 Main.java

void main() {
    Box<Integer> box = new Box<Integer>();
    box.set(10);
    Integer result = box.get(); 
}
  • 와일드카드에서 예시로 사용한 Box<T> 제네릭 타입에 Integer 타입을 전달한다.

컴파일 후 Box.class

public class Box<Object> {

    private Object value;

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

    public Object get() {
        return value;
    }
}
  • .java 에서 상한 제한 없이 선언된 타입 매개변수 T의 경우, 컴파일 후 .class 에서 Object 로 변환된다.

컴파일 후 Main.class

void main() {
    Box box = new Box();
    box.set(10);
    Integer result = (Integer) box.get(); // 컴파일러가 캐스팅 추가
}
  • box.get() 메서드의 반환 타입은 Object로 변환되었지만, 컴파일러가 자동으로 (Integer) 캐스팅을 삽입하여 타입 호환성을 유지한다.

상한 제한이 있는 경우

  • 예를 들어, .java 에서 AnimalHospital<T extends Animal> 과 같이 Animal 타입으로 상한 제한한다.
  • 컴파일 후에 .classT의 타입 정보가 제거된다 하더라도, 상한으로 지정된 Animal 타입으로 대체되어 Animal 과 관련된 기능을 사용할 수 있다.
  • 또한, 타입 인자로 지정한 타입으로 컴파일러가 다운 캐스팅을 대신 처리해주기 때문에 아무 문제도 발생하지 않는다.

한계

  • 컴파일 이후에는 제네릭의 타입 정보가 존재하지 않는다. 따라서, 런타임에 타입을 활용하는 코드를 작성할 수 없다.
  • 예를 들어, param instanceof T , new T() 는 컴파일 이후 항상 param instanceof Object , new Object() 로 변환된다.
  • 이는 개발자가 의도한 것과 다르기 때문에 자바는 타입 매개 변수에 instanceofnew 를 허용하지 않는다.




김영한의 실전 자바 중급 2편을 참고하였습니다

© 2021. All rights reserved.

yaejinkong의 블로그