본문 바로가기

복습

[Java 복습] 와일드 카드

목차

  • 와일드 카드
  • 와일드 카드 실행순서
  • 제네릭 메서드 vs 와일드 카드
  • 상한, 하한 제한
  • 타입 이레이저

 

와일드 카드

이번에는 제네릭 타입을 조금 더 편리하게 사용할 수 있는 와일드 카드(wildcard)에 대해 알아보자.
참고로 와일드카드라는 뜻은 컴퓨터 프로그래밍에서 * , ? 와 같이 하나 이상의 문자들을 상징하는 특수 문자를 뜻한다. 
쉽게 이야기해서 여러 타입이 들어올 수 있다는 뜻이다.

public class Box<T> {

    private T value;

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

    public T get() {
        return value;
    }
}
public class WildcardEx {

    static <T> void printGenericV1(Box<T> box) {
        System.out.println("T = " + box.get());
    }
    // Box<Dog>, Box<Cat>, Box<Object>
    static void printWildcardV1(Box<?> box) {
        System.out.println("? = " + box.get());
    }


    static <T extends Animal> void printGenericV2(Box<T> box) {
        T t = box.get();
        System.out.println("이름V2 = " + t.getName());
    }
    static void printWildcardV2(Box<? extends Animal> box) {
        Animal animal = box.get();
        System.out.println("?2 = " + animal.getName());
    }


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

    static Animal printAndReturnWildcard(Box<? extends Animal> box) {
        Animal animal = box.get();
        System.out.println("이름V3 = " + animal.getName());
        return animal;
    }
}
public class WildcardMain1 {

    public static void main(String[] args) {
        Box<Object> objBox = new Box<>();
        Box<Dog> dogBox = new Box<>();
        Box<Cat> catBox = new Box<>();

        dogBox.set(new Dog("멍멍이", 100));
        WildcardEx.<Dog>printGenericV1(dogBox);
        WildcardEx.printWildcardV1(dogBox);

        System.out.println();
        WildcardEx.<Dog>printGenericV2(dogBox);
        WildcardEx.printWildcardV2(dogBox);
        //WildcardEx.printWildcardV2(objBox); 컴파일 오류


        System.out.println();
        Dog dog = WildcardEx.printAndReturnGeneric(dogBox);
        Animal animal = WildcardEx.printAndReturnWildcard(dogBox);


        //Cat cat = WildcardEx.<Cat>printAndReturnGeneric(catBox); // 타입추론
       // WildcardEx.printGenericV1(catBox); // 타입추론
    }
}

 

T = Animal{name='멍멍이', size=100}
? = Animal{name='멍멍이', size=100}

이름V2 = 멍멍이
?2 = 멍멍이

이름V3 = 멍멍이
이름V3 = 멍멍이

 

참고!!!
와일드 카드는 제네릭 타입이나, 제네릭 메서드를 선언하는 것이 아니다. 

와일드 카드는 이미 만들어진 제네릭 타입을 활용할 때 사용한다.

 

 

비제한 와일드카드

// 이것은 제네릭 메서드이다.
// Box<Dog> dogBox를 전달한다. 타입 추론에 의해 타입 T가 Dog가 된다.
static <T> void printGenericV1(Box<T> box) {
    System.out.println("T = " + box.get());
}

// 이것은 제네릭 메서드가 아니다. 일반적인 메서드이다.
// Box<dog> dogBpx가 전달된다. 와일드카드 ?는 모든 타입을 받을 수 있다.
static void printWildcardV1(Box<?> box) {
    System.out.println("? = " + box.get());
}
  • 두 메서드는 비슷한 기능을 하는 코드이다.
  • 하나는 제네릭 메서드를 사용하고 하나는 일반적인 메서드에 와일드카드를 사용했다.
  • 와일드 카드는 제네릭 타입이나 제네릭 메서드를 정의할 때 사용하는 것이 아니다.
  •  Box<Dog> , Box<Cat> 처럼 타입 인자가 정해진 제네릭 타입을 전달 받아서 활용할 때 사용한다.
  • 와일드카드인 ? 는 모든 타입을 다 받을 수 있다는 뜻이다.
    • 다음과 같이 해석할 수 있다. ? == <? extends Object>
  • 이렇게 ? 만 사용해서 제한 없이 모든 타입을 다 받을 수 있는 와일드카드를 비제한 와일드카드라 한다.
    • 여기에는 Box<Dog> dogBox , Box<Cat> catBox , Box<Object> objBox 가 모두 입력될 수 있다.

 

제네릭 메서드 실행 예시

//1. 전달
printGenericV1(dogBox)

//2. 제네릭 타입 결정 dogBox는 Box<Dog> 타입, 타입 추론 -> T의 타입은 Dog
static <T> void printGenericV1(Box<T> box) {
    System.out.println("T = " + box.get());
}

//3. 타입 인자 결정
static <Dog> void printGenericV1(Box<Dog> box) {
    System.out.println("T = " + box.get());
}

//4. 최종 실행 메서드
static void printGenericV1(Box<Dog> box) {
    System.out.println("T = " + box.get());
}

 

 

와일드 카드 실행 예시

//1. 전달
printWildcardV1(dogBox)

//이것은 제네릭 메서드가 아니다. 일반적인 메서드이다.
//2. 최종 실행 메서드, 와일드카드 ?는 모든 타입을 받을 수 있다.
static void printWildcardV1(Box<?> box) {
    System.out.println("? = " + box.get());
}

 

 

제네릭 메서드 vs 와일드카드


printGenericV1( ) 제네릭 메서드를 보자. 제네릭 메서드에는 타입 매개변수가 존재한다. 

그리고 특정 시점에 타입 매개변수에 타입 인자를 전달해서 타입을 결정해야 한다. 이런 과정은 매우 복잡하다.
반면에 printWildcardV1( ) 메서드를 보자. 와일드카드는 일반적인 메서드에 사용할 수 있고, 단순히 매개변수로
제네릭 타입을 받을 수 있는 것 뿐이다. 제네릭 메서드처럼 타입을 결정하거나 복잡하게 작동하지 않는다. 

"단순히 일반 메서드에 제네릭 타입을 받을 수 있는 매개변수가 하나 있는 것 뿐이다."
제네릭 타입이나 제네릭 메서드를 정의하는게 꼭 필요한 상황이 아니라면, 더 단순한 와일드카드 사용을 권장한다.

 

 

 

 

 

상한 와일드 카드

static <T extends Animal> void printGenericV2(Box<T> box) {
    T t = box.get();
    System.out.println("이름V2 = " + t.getName());
}
static void printWildcardV2(Box<? extends Animal> box) {
    Animal animal = box.get();
    System.out.println("?2 = " + animal.getName());
}

 

  • 제네릭 메서드와 마찬가지로 와일드카드에도 상한 제한을 둘 수 있다.
  • 여기서는 ? extends Animal 을 지정했다.
  • Animal 과 그 하위 타입만 입력 받는다. 만약 다른 타입을 입력하면 컴파일 오류가 발생한다.
  • box.get( ) 을 통해서 꺼낼 수 있는 타입의 최대 부모는 Animal 이 된다. 따라서 Animal 타입으로 조회할 수있다.
  • 결과적으로 Animal 타입의 기능을 호출할 수 있다.

 

 

 

타입 매개변수가 꼭 필요한 경우

와일드카드는 제네릭을 정의할 때 사용하는 것이 아니다. 

Box<Dog> , Box<Cat> 처럼 타입 인자가 전달된 제네릭 타입을 활용할 때 사용한다. 

따라서 다음과 같은 경우에는 제네릭 타입이나 제네릭 메서드를 사용해야 문제를 해결할 수 있다.

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


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

 

printAndReturnGeneric() 은 다음과 같이 전달한 타입을 명확하게 반환할 수 있다. 

Dog dog = WildcardEx.printAndReturnGeneric(dogBox);


반면에 printAndReturnWildcard() 의 경우 전달한 타입을 명확하게 반환할 수 없다. 여기서는 Animal 타입으로 반환한다. 

Animal animal = WildcardEx.printAndReturnWildcard(dogBox);


메서드의 타입들을 특정 시점에 변경하려면 제네릭 타입이나, 제네릭 메서드를 사용해야 한다.
와일드카드는 이미 만들어진 제네릭 타입을 전달 받아서 활용할 때 사용한다. 

따라서 메서드의 타입들을 타입 인자를 통해 변경할 수 없다.

쉽게 이야기해서 일반적인 메서드에 사용한다고 생각하면 된다.

 

 

정리

제네릭 타입이나 제네릭 메서드가 꼭 필요한 상황이면 <T> 를 사용하고

그렇지 않은 상황이면 와일드카드를 사용하는 것을 권장한다.

 

 

하한 와일드 카드

public class WildcardMain2 {

    public static void main(String[] args) {
        Box<Object> objBox = new Box<>();
        Box<Animal> animalBox = new Box<>();
        Box<Dog> dogBox = new Box<>();
        Box<Cat> catBox = new Box<>();

        // Animal 포함 상위 타입 전달 가능
        writeBox(objBox);
        writeBox(animalBox);
        // writeBox(dogBox); // 하한이 Animal
        // writeBox(catBox); // 하한이 Animal

        Animal animal =  animalBox.get();
        System.out.println("animal = " + animal);
        Animal animal2 = (Animal)objBox.get();
    }

    static void writeBox(Box<? super Animal> box) {
        box.set(new Dog("멍멍이", 100));
    }
}
animal = Animal{name='멍멍이', size=100}

 

Box<? super Animal> box

이 코드는 ? 가 Animal 타입을 포함한 Animal 타입의 상위 타입만 입력 받을 수 있다는 뜻이다.

 

Box<Object> objBox : 허용
Box<Animal> animalBox : 허용
Box<Dog> dogBox : 불가
Box<Cat> catBox : 불가


하한을 Animal 로 제한했기 때문에 Animal 타입의 하위 타입인 Box<Dog> 는 전달할 수 없다.

 

 

 

타입 이레이저

이레이저(eraser)는 지우개라는 뜻이다.
제네릭은 자바 컴파일 단계에서만 사용되고, 컴파일 이후에는 제네릭 정보가 삭제된다. 제네릭에 사용한 타입 매개변수
가 모두 사라지는 것이다. 쉽게 이야기해서 컴파일 전인 .java 에는 제네릭의 타입 매개변수가 존재하지만, 컴파일 이
후인 자바 바이트코드 .class 에는 타입 매개변수가 존재하지 않는 것이다.

100% 정확한 코드는 아니고 대략 이런 방식으로 작동한다고 이해하면 충분하다.

 

제네릭 타입 선언

public class GenericBox<T> {

    private T value;

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

    public T get() {
        return value;
    }
}

 

 

지네릭 타입에 Integer 타입 인자 전달

Main.java

 void main() {
    GenericBox<Integer> box = new GenericBox<>();
    box.set(10);
    Integer result = box.get();
}

 

이렇게 하면 자바 컴파일러는 컴파일 시점에 타입 매개변수와 타입 인자를 포함한 제네릭 정보를 활용해서 

GenericBox<Interger>( ) 에 대해 다음과 같이 이해하고 컴파일 한다.

public class GenericBox<Integer> {

    private Integer value;

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

    public Integer get() {
        return value;
    }
}

 

컴파일이 모두 끝나면 자바는 제네릭과 관련된 정보를 삭제한다. 이때 .class에 생성된 정보는 다음과 같다.

 

컴파일 후

GenericBox.class

public class GenericBox {

    private Object value;

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

    public Object get() {
        return value;
    }
}

 

상한 제한 없이 선언한 타입 매개변수 T는 Object로 변환된다.

 

Main.class

 void main() {
    GenericBox box = new GenericBox();
    box.set(10);
    Integer result = (Integer)box.get(); // 컴파일러가 캐스팅 추가
}
  • 값을 반환 받는 부분을 Object 로 받으면 안된다. 
  • 자바 컴파일러는 제네릭에서 타입 인자로 지정한 Integer로 캐스팅하는 코드를 추가해준다.
  • 이렇게 추가된 코드는 자바 컴파일러가 이미 검증하고 추가했기 때문에 문제가 발생하지 않는다.

 

타입 매개변수 제한의 경우

public class AnimalHospitalV3<T extends Animal> { // Animal ~ Animal 자손들

    private T animal;

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

    public void checkUp() {
        System.out.println("동물 이름: " + animal.getName());
        System.out.println("동물 크기: " + animal.getSize());
        animal.sound();
    }

    public T getBigger(T target) {
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

 

 

//사용 코드 예시
AnimalHospitalV3<Dog> hospital = new AnimalHospitalV3<>();
...
Dog dog = animalHospitalV3.getBigger(new Dog());

 

 

컴파일 후

public class AnimalHospitalV3<Animal> { // Animal ~ Animal 자손들

    private Animal animal;

    public void set(Animal animal) {
        this.animal = animal;
    }

    public void checkUp() {
        System.out.println("동물 이름: " + animal.getName());
        System.out.println("동물 크기: " + animal.getSize());
        animal.sound();
    }

    public Animal getBigger(Animal target) {
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

T 의 타입 정보가 제거되어도 상한으로 지정한 Animal 타입으로 대체되기 때문에 Animal 타입의 메서드를 사용하는데는 아무런 문제가 없다.

 

//사용 코드 예시
AnimalHospitalV3 hospital = new AnimalHospitalV3();
...
Dog dog = (Dog) animalHospitalV3.getBigger(new Dog());


반환 받는 부분을 Animal 로 받으면 안되기 때문에 자바 컴파일러가 타입 인자로 지정한 Dog 로 캐스팅하는 코드를 

넣어준다. 자바의 제네릭은 단순하게 생각하면 개발자가 직접 캐스팅 하는 코드를 컴파일러가 대신 처리해주는 것이다. 

자바는 컴파일 시점에 제네릭을 사용한 코드에 문제가 없는지 완벽하게 검증하기 때문에 자바 컴파일러가 추가하는

다운 캐스팅에는 문제가 발생하지 않는다.자바의 제네릭 타입은 컴파일 시점에만 존재하고, 런타임 시에는 제네릭 정보가 지워지는데, 이것을 타입 이레이저라 한다.

 

 

타입 이레이저 방식의 한계


컴파일 이후에는 제네릭의 타입 정보가 존재하지 않는다. 

.class 로 자바를 실행하는 런타임에는 우리가 지정한 Box<Integer> , Box<String> 의 타입 정보가 모두 제거된다. 

 

따라서 런타임에 타입을 활용하는 다음과 같은 코드는 작성할 수 없다

public class EraserBox<T> {
    
    public boolean instanceCheck(Object param) {
        return param instanceof T; // 컴파일 오류
    }
    
    public void create() {
        return new T(); // 컴파일 오류
    }
}

 

 

런타임

class EraserBox {
    public boolean instanceCheck(Object param) {
    return param instanceof Object; // 오류
 }
 
 public void create() {
    return new Object(); // 오류
    }
}
  • 여기서 T 는 런타임에 모두 Object 가 되어버린다.
  • instanceof 는 항상 Object 와 비교하게 된다. 이렇게 되면 항상 참이 반환되는 문제가 발생한다. 
  • 자바는 이런 문제 때문에 타입 매개변수에 instanceof 를 허용하지 않는다.
  • new T 는 항상 new Object 가 되어버린다.
  • 개발자가 의도한 것과는 다르다. 따라서 자바는 타입 매개변수에 new 를 허용하지 않는다.