쌩로그

김영한의 실전 자바 - 중급 2편 - Sec 03. 제네릭 - Generic2 본문

Language/JAVA

김영한의 실전 자바 - 중급 2편 - Sec 03. 제네릭 - Generic2

.쌩수. 2025. 1. 5. 07:04
반응형

목차

  1. 포스팅 개요
  2. 본론
     2-1. 타입 매개변수 제한1 - 시작
     2-2. 타입 매개변수 제한2 - 다형성 시도
     2-3. 타입 매개변수 제한3 - 제네릭 도입과 실패
     2-4. 타입 매개변수 제한4 - 타입 매개변수 제한
     2-5. 제네릭 메서드
     2-6. 제네릭 메서드 활용
     2-7. 와일드카드1
     2-8. 와일드카드2
     2-9. 타입 이레이저
  3. 요약

1. 포스팅 개요

해당 포스팅은 김영한의 실전 자바 중급 2편 Section 3의 제너릭 - Generic2 에 대한 학습 내용이다.

학습 레포 URL : https://github.com/SsangSoo/inflearn-holyeye-java-mid2
(해당 레포는 완강시 public으로 전환 예정이다.)

2. 본론

2-1. 타입 매개변수 제한1 - 시작

동물 병원을 코드로 만드는데,
개 병원은 개만, 고양이 병원은 고양이만 받을 수 있어야 한다.

// 개 병원
public class DogHospital {  

    private Dog animal;  

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


    // 개의 이름과 크기를 출력 + sound() 메서드 호출  
    public void checkup() {  
        System.out.println("동물 이름: " + animal.getName());  
        System.out.println("동물 크기: " + animal.getSize());  
        animal.sound();  
    }  

    // 다른 개와 크기 비교 > 둘 중에 큰 것을 반환  
    public Dog bigger(Dog target) {  
        return animal.getSize() > target.getSize() ? animal : target;  
    }  

}

// 고양이 병원
public class CatHospital {  

    private Cat animal;  

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


    // 고양이의 이름과 크기를 출력 + sound() 메서드 호출  
    public void checkup() {  
        System.out.println("동물 이름: " + animal.getName());  
        System.out.println("동물 크기: " + animal.getSize());  
        animal.sound();  
    }  

    // 다른 고양이와 크기 비교 > 둘 중에 큰 것을 반환  
    public Cat bigger(Cat target) {  
        return animal.getSize() > target.getSize() ? animal : target;  
    }  

}

// 활용 예제
public class AnimalHospitalMainV0 {  
    public static void main(String[] args) {  
        DogHospital dogHospital = new DogHospital();  
        CatHospital catHospital = new CatHospital();  

        Dog dog = new Dog("멍멍이1", 100);  
        Cat cat = new Cat("냐옹이1", 300);  

        // 개 병원  
        dogHospital.set(dog);  
        dogHospital.checkup();  

        // 고양이 병원  
        catHospital.set(cat);  
        catHospital.checkup();  

        // 문제1: 개 병원에 고양이 전달  
        // dogHospital.set(cat); // 다른 타입 입력 : 컴파일 오류  

        // 문제2 : 개 타입 변환  
        dogHospital.set(dog);  
        Dog biggerDog = dogHospital.bigger(new Dog("멍멍이2", 200));  
        System.out.println("biggerDog = " + biggerDog);  
    }  
}
//결과
동물 이름: 멍멍이1
동물 크기: 100
멍멍
동물 이름: 냐옹이1
동물 크기: 300
냐옹
biggerDog = Animal{name='멍멍이2', size=200}
  • 해당 내용은 요구사항을 명확히 잘 지키고 있다.

문제는 다음과 같다.

  • 코드 재사용이 불가하다
    • 개 병원과 고양이 병원은 중복이 많이 보여지고 있다.
  • 타입 안정성은 가지고 있다.
    • 타입 안정성은 명확히 지켜지고 있다.

2-2. 타입 매개변수 제한2 - 다형성 시도

위의 코드를 다형성을 사용해서 중복을 제거해보자.

public class AnimalHospitalV1 {  

    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 bigger(Animal target) {  
        return animal.getSize() > target.getSize() ? animal : target;  
    }  

}
  • Animal 타입을 받아서 처리한다.
  • 다른 메서드들은 Animal에서 제공하는 기능이므로 다 호출 가능하다.

활용 코드 예제는 다음과 같다.
(이전과 같은데 DogHospital, CatHospital 이 아니라, AnimalHospitalV1 객체를 생성한다.)

public class AnimalHospitalMainV1 {  
    public static void main(String[] args) {  
        AnimalHospitalV1 dogHospital = new AnimalHospitalV1();  
        AnimalHospitalV1 catHospital = new AnimalHospitalV1();  

        Dog dog = new Dog("멍멍이1", 100);  
        Cat cat = new Cat("냐옹이1", 300);  

        // 개 병원  
        dogHospital.set(dog);  
        dogHospital.checkup();  

        // 고양이 병원  
        catHospital.set(cat);  
        catHospital.checkup();  

        // 문제1: 개 병원에 고양이 전달  
         dogHospital.set(cat); // 매개변수 체크 실패 : 컴파일 오류가 발생하지 않는다.  

        // 문제2 : 개 타입 변환  
        dogHospital.set(dog);  
        // dogHospital.set(cat); // 실수로 고양이를 입력했는데, 개를 반환하는 상황이면 캐스팅 예외가 발생한다.
        Dog biggerDog = (Dog) dogHospital.bigger(new Dog("멍멍이2", 200));  
        System.out.println("biggerDog = " + biggerDog);  
    }  
}
  • 코드 재사용성은 증가했다.
    • 다형성을 통해 AnimalHospitalV1 하나로 개와 고양이 모두를 처리한다.
  • 타입 안정성이 보장되지 않는다.
    • 개 병원에 고양이를 전달하는 문제가 발생한다.
    • Animal 타입을 반환한기 때문에 다운 캐스팅을 해야한다.
    • 실수로 고양이를 입력했는데, 개를 반환하는 상황이면 캐스팅 예외가 발생한다.

2-3. 타입 매개변수 제한3 - 제네릭 도입과 실패

제네릭을 도입해서 코드 재사용은 늘리고, 타입 안정성 문제도 해결하자.


임의로 코드를 바꿔봤는데, T로 선언하니, AnimalgetName(), getSize(), sound()Animal의 메서드를 호출하지 못하고 있다.
컴파일 오류가 발생하고 있다.


public class AnimalHospitalV2<T> {  

    private T animal;  

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

    public void checkup() {  
        // T의 타입을 메서드를 정의하는 시점에는 알 수 없다. Object의 기능만 사용가능하다.  

        animal.toString();  
        animal.equals(null);  
        // 컴파일 오류  
        //System.out.println("동물 이름: " + animal.getName());  
        //System.out.println("동물 크기: " + animal.getSize());  
        //animal.sound();    }  

    // 다른 개와 크기 비교 > 둘 중에 큰 것을 반환  
    public T bigger(T target) {  
        // 컴파일 오류  
        //return animal.getSize() > target.getSize() ? animal : target;  
        return null;  
    }  
}
  • 제네릭 타입을 선언했지만, T의 타입을 메서드를 정의하는 시점에는 알 수 없으므로, Object의 기능만 사용가능하도록 했다.
  • Animal 타입의 자식이 들어오기를 기대했지만, 여기 코드 어디에도 Animal에 대한 정보가 없다.
    • T의 타입 인자로 Integer가 들어올 수도 있고, Dog가 들어올 수도 있다.
    • 물론 Object가 들어올 수도 있다.

자바 컴파일러는 어떤 타입이 들어올지 알 수 없기 때문에 T를 어떤 타입이든 받을 수 있는 모든 객체의 최종 부모인 Object 타입으로 가정한다.
그래서 Object가 제공하는 메서드만 호출할 수 있다.
(현재 Animal 타입이 제공하는 기능들이 필요한데, 이 기능을 모두 사용할 수 없는 상태다.)

또한 아래와 같이 동물 병원에 Integer, Object와 같은 동물과 전혀 관계 없는 타입을 타입 인자로 전달할 수 있는 점이다.

public class AnimalHospitalMainV2 {  
    public static void main(String[] args) {  
        AnimalHospitalV2<Dog> dogHospital = new AnimalHospitalV2<>();  
        AnimalHospitalV2<Cat> catHospital = new AnimalHospitalV2<>();  
        AnimalHospitalV2<Integer> integerHospital = new AnimalHospitalV2<>();  
        AnimalHospitalV2<Object> objectHospital = new AnimalHospitalV2<>();  
    }  
}

요구사항에 따라 최소한 Animal 이나 그 자식을 타입 인자로 제한해야 한다.

문제

  • 제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올수 있다.
  • 타입 매개변수를 어떤 타입이든 수용할 수 있는 Object로 가정하고, Object의 기능만 사용할 수 있다.

타입 매개변수를 Animal로 제한하지 않았다.
타입 인자가 모두 Animal 과 그 자식만 들어올 수 있도록 하면 된다.
다음 내용을 보자.

2-4. 타입 매개변수 제한4 - 타입 매개변수 제한

타입 매개변수를 특정 타입으로 제한할 수 있다.

public class AnimalHospitalV3<T extends 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 bigger(T target) {  
        return animal.getSize() > target.getSize() ? animal : target;  
    }  
}
  • <T extends Animal>
    • 타입 매개변수 TAnimal과 그 자식만 받을 수 있도록 제한을 두는 것이다.
    • T의 상한이 Animal이 되는 것이다.

이렇게 하면 타입 인자로 들어올 수 있는 값이 Animal과 그 자식으로 제한된다.
따라서 Animal 이 제공하는 getName(), getSize(), sound()를 사용할 수 있다.

실제로 Animal 과 그 자식 타입이 아니면 컴파일 오류가 발생하고 있다.

활용 예제다.

public class AnimalHospitalMainV3 {  
    public static void main(String[] args) {  
        AnimalHospitalV3<Dog> dogHospital = new AnimalHospitalV3<>();  
        AnimalHospitalV3<Cat> catHospital = new AnimalHospitalV3<>();  

        Dog dog = new Dog("멍멍이1", 100);  
        Cat cat = new Cat("냐옹이1", 300);  

        // 개 병원  
        dogHospital.set(dog);  
        dogHospital.checkup();  

        // 고양이 병원  
        catHospital.set(cat);  
        catHospital.checkup();  

        // 문제1: 개 병원에 고양이 전달  
        //dogHospital.set(cat); // 다른 타입 입력 : 컴파일 오류  

        // 문제2 : 개 타입 변환  
        dogHospital.set(dog);  
        Dog biggerDog = dogHospital.bigger(new Dog("멍멍이2", 200));  
        System.out.println("biggerDog = " + biggerDog);  
    }  
}

// 결과
동물 이름: 멍멍이1
동물 크기: 100
멍멍
동물 이름: 냐옹이1
동물 크기: 300
냐옹
biggerDog = Animal{name='멍멍이2', size=200}

타입 매개변수 제한울 통해서 다음과 같은 문제가 동시에 해결되었다.

  • 타입 안정성 문제
  • 코드 재사용성 문제
  • 제네릭 도입 문제
    • 제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있다.
    • 어떤 타입이든 수용할 수 있는 Object로 가정하고, Object의 기능만 사용할 수 있다.
    • 타입 매개변수를 Animal을 상한으로 제한하여 Animal의 기능을 사용할 수 있게 되었다.

2-5. 제네릭 메서드

제네릭 메서드는 특정 메서드에 제네릭을 적용하도록 한다.
참고록 제네릭 타입제네릭 메서드는 둘다 제네릭을 사용하기는 하지만 서로 다른 기능을 제공한다.

public class GenericMethod {  

    public static Object objMethod(Object obj) {  
        System.out.println("Object print: " + obj);  
        return obj;  
    }  

    public static <T> T genericMethod(T obj) {  
        System.out.println("Object print: " + obj);  
        return obj;  
    }  

}
  • 반환 타입 앞에 다이아몬드로 <T> 와 같은 형식으로 타입 매개변수를 써주면 메서드에서만 한적적으로 사용되는 제네릭 메서드가 된다.
  • 또한 타입 매개변수에 대해 상한과 같이 제한을 걸 수 있다.

활용 예

public class MethodMain1 {  

    public static void main(String[] args) {  
        Integer i = 10;  
        Object object = GenericMethod.objMethod(i); // 반환시 Object -> 캐스팅 필요  


        // 타입 인자(Type Argument) 명시적 전달  
        System.out.println("명시적 타입 인자 전달");  
        Integer result = GenericMethod.<Integer>genericMethod(i);  
        Integer integerValue = GenericMethod.<Integer>numberMethod(10);  
        Double doubleValue = GenericMethod.<Double>numberMethod(20.0);  
    }  
}
  • 제네릭 메서드는 메서드를 호출할 때 타입을 지정할 수 있다.
  • 위의 예시와 같이 호출 메서드 앞에 타입 인자를 전달하면 된다.

만약 타입을 지정했는데, 다른 타입을 넣게 되면?

Integer 타입을 타입 인자로 넣고, double을 집어넣었는데, 컴파일 오류가 발생한다.


제네릭 타입과 제네릭 메서드

  • 제네릭 타입
    • 타입 인자 전달 : 객체를 생성하는 시점
    • 예) new GenericCalss<String>
  • 제네릭 메서드
    • 타입 인자 전달 : 메서드를 호출하는 시점
    • 예) GenericMethod.<Integer>genericMethod(1)

제네릭 메서드

  • 제네릭 메서드는 클래스 전체가 아니라 특정 메서드 단위로 제네릭을 도입할 때 사용한다.
  • 제네릭 메서드를 정의할 때는 메서드의 반환 타입 왼쪽에 다이아몬드를 사용해서 <T>와 같이 타입 매개변수를 적어준다.
  • 제네릭 메서드는 메서드를 실제 호출하는 시점에 다이아몬드를 사용해서 <Integer>와 같이 타입을 정하고 호출한다.

제네릭 메서드의 핵심은 메서드를 호출하는 시점에 타입 인자를 전달해서 타입을 지정하는 것이다.
따라서 타입을 지정하면서 메서드를 호출한다.

제네릭 메서드의 핵심은 메서드를 호출하는 시점에 타입 인자를 전달해서 타입을 지정하는 것이다.
따라서 타입을 지정하면서 메서드를 호출한다.

인스턴스 메서드, static 메서드

  • 제네릭 메서드는 인스턴스 메서드와 static 메서드에 모두 적용할 수 있다.
static <V> V staticMethod2(V v) {} // static 메서드에 제네릭 메서드 도입
<Z> Z instanceMethod2(Z z) {} // 인스턴스 메서드에 제네릭 메서드 도입 가능

참고

  • 제네릭 타입static 메서드에 타입 매개변수를 사용할 수 없다.
  • 제네릭 타입객체를 생성하는 시점에 타입이 정해진다.
  • static 메서드는 인스턴스 단위가 아니라 클래스 단위로 작동하기 때문에 제네릭 타입과는 무관한다.
  • static 메서드에 제네릭을 도입하려면 제네릭 메서드를 사용해야 한다.
class Box<T> {
    T instanceMethod(T t) {} // 가능
    static T staticMethod1(T t) {} // 제네릭 타입의 T 사용 불가능
    static <V> V staticMethod1(V v) {} // 가능
}

제네릭 메서드의 타입 매개변수 제한

제네릭 메서드도 제네릭 타입과 마찬가지로 타입 매개변수를 제한할 수 있다.

public static <T extends Number> T numberMethod(T t) {}

위 코드는 타입 매개변수를 Number 로 제한했다.
따라서 Number와 그 자식만 받을 수 있다.
참고로 Integer, Double, Long과 같은 숫자 타입이 Number의 자식이다.

제네릭 메서드 타입 추론

제네릭 메서드를 호출할 때 <Integer> 와 같이 타입 인자를 계속 전달하는 것은 매우 불편하다.

...
Integer result1 = GenericMethod.<Integer>genericMethod(i);  
Integer integerValue1 = GenericMethod.<Integer>numberMethod(10);  
Double doubleValue1 = GenericMethod.<Double>numberMethod(20.0);  

Integer result2 = GenericMethod.genericMethod(i);  
Integer integerValue2 = GenericMethod.numberMethod(10);  
Double doubleValue2 = GenericMethod.numberMethod(20.0);
...

xxx1xxx2 를 보면 전달되는 인자의 값을 통해서 타입을 추론한다.

  • 자바 컴파일러는 genericMethod() 에 전달되는 인자 i의 타입이 Integer라는 것을 알 수 있다.
  • 반환 타입이 Integer result라는 것도 알 수 있다.
  • 이런 정보를 통해 자바 컴파일러는 타입 인자를 추론할 수 있다.

활용 최종 코드다.

public class MethodMain1 {  

    public static void main(String[] args) {  
        Integer i = 10;  
        Object object = GenericMethod.objMethod(i); // 반환시 Object -> 캐스팅 필요  


        // 타입 인자(Type Argument) 명시적 전달  
        System.out.println("명시적 타입 인자 전달");  
        Integer result1 = GenericMethod.<Integer>genericMethod(i);  
        Integer integerValue1 = GenericMethod.<Integer>numberMethod(10);  
        Double doubleValue1 = GenericMethod.<Double>numberMethod(20.0);  

        System.out.println("타입 추론");  
        Integer result2 = GenericMethod.genericMethod(i);  
        Integer integerValue2 = GenericMethod.numberMethod(10);  
        Double doubleValue2 = GenericMethod.numberMethod(20.0);  
    }  
}

// 결과
Object print: 10
명시적 타입 인자 전달
generic print: 10
bound print: 10
bound print: 20.0
타입 추론
generic print: 10
bound print: 10
bound print: 20.0

2-6. 제네릭 메서드 활용

AnimalHospitalV3의 주요 기능을 제네릭 메서드로 다시 만들어본다.

public class AnimalMethod {  

    public static <T extends Animal> void checkup(T t) {  
        System.out.println("동물 이름: " + t.getName());  
        System.out.println("동물 크기: " + t.getSize());  
        t.sound();  
    }  


    // 다른 개와 크기 비교 > 둘 중에 큰 것을 반환  
    public static <T extends Animal> T bigger(T t1, T t2) {  
        return t1.getSize() > t2.getSize() ? t1 : t2;  
    }  
}

// 활용 예
public class MethodMain2 {  

    public static void main(String[] args) {  
        Dog dog = new Dog("멍멍이", 100);  
        Cat cat = new Cat("냐옹이", 200);  

        // 타입 매개변수 추론  
        AnimalMethod.checkup(dog);  
        AnimalMethod.checkup(cat);  

        Dog targetDog = new Dog("큰 멍멍이", 200);  
//        Animal bigger = AnimalMethod.bigger(dog, cat); // dog와 cat을 비교하니 반환타입이 Animal로 나옴.  
        Dog bigger = AnimalMethod.bigger(dog, targetDog);  
        System.out.println("bigger = " + bigger);  
    }  
}

// 결과
동물 이름: 멍멍이
동물 크기: 100
멍멍
동물 이름: 냐옹이
동물 크기: 200
냐옹
bigger = Animal{name='큰 멍멍이', size=200}

제네릭 메서드를 호출할 때 타입 추론을 사용했다.

제네릭 타입과 제네릭 메서드의 우선순위

정적 메서드는 제네릭 메서드만 적용할 수 있지만, 인스턴스 메서드는 제네릭 타입도 제네릭 메서드도 둘다 적용할 수 있다.
여기에 제네릭 타입과 제네릭 메서드의 타입 매개변수를 같은 이름으로 사용하면 어떻게 될까?

public class CompleBox<T extends Animal> {  

    private T animal;  

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

    public <T> T printAndReturn(T t) {  
        System.out.println("animal.className: " + animal.getClass().getName());  
        System.out.println("t.className: " + t.getClass().getName());  
        return t;  
    }  
}

// 활용 코드
public class MethodMain3 {  

    public static void main(String[] args) {  
        Dog dog = new Dog("멍멍이", 100);  
        Cat cat = new Cat("냐옹이", 200);  

        CompleBox<Dog> hospital = new CompleBox<>();  
        hospital.set(dog);  

        Cat returnCat = hospital.printAndReturn(cat);  
        System.out.println("returnCat = " + returnCat);  
    }  
}

// 결과
animal.className: generic.animal.Dog
t.className: generic.animal.Cat
returnCat = Animal{name='냐옹이', size=200}

제네릭 타입과 제네릭 메서드를 둘 다 선언했다.
결과를 보다시피 제네릭 메서드가 제네릭 타입보다 우선순위가 높다.

그리고 제네릭 타입과 제네릭 메서드 둘 다 T 를 선언했는데, 두 개의 타입은 서로 무관하다.
때문에 제네릭 메서드에 상한을 설정하지 않았기 때문에 Object로 취급된다.

그리고 프로그래밍에서 모호한 것은 좋지 않다.
이름이 겹치면, 이름이 겹치치 않도록 해야한다.

public class CompleBox<T extends Animal> {  

    private T animal;  

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

    public <Z> Z printAndReturn(Z z) {  
        System.out.println("animal.className: " + animal.getClass().getName());  
        System.out.println("t.className: " + z.getClass().getName());  
        return z;  
    }  
}

제네릭 메서드의 TZ 로 바꿨다.

2-7. 와일드카드1

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());  
    }  


    // 와일드카드 사용  
    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("이름 = " + t.getName());  
    }  

    // 와일드카드 사용  
    static <T extends Animal> void printWildcardV2(Box<? extends Animal> box) {  
        Animal animal = box.get();  
        System.out.println("이름 = " + animal.getName());  
    }  


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


    // 와일드카드 사용  
    static Animal printAndReturnWildcard(Box<? extends Animal> box) {  
        Animal animal = box.get();  
        System.out.println("이름 = " + 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.printGenericV1(dogBox);  
        WildcardEx.printWildcardV1(dogBox);  

        WildcardEx.printGenericV2(dogBox);  
        WildcardEx.printWildcardV2(dogBox);  

        Dog dog = WildcardEx.printAndReturnGeneric(dogBox);  
        Animal animal = WildcardEx.printAndReturnWildcard(dogBox);  
    }  
}

// 결과
T = Animal{name='멍멍이', size=100}
? = Animal{name='멍멍이', size=100}
이름 = 멍멍이
이름 = 멍멍이
이름 = 멍멍이
이름 = 멍멍이
  • 와일드카드는 ? 를 사용해서 정의한다.
  • 와일드카드는 제네릭 타입이나, 제네릭 메서드를 선언하는 것이 아니다.
    • 와일드카드는 이미 만들어진 제네릭 타입을 활용할 때 사용한다.

비제한 와일드카드

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


// 와일드카드 사용  
// 일반적인 메서드
// Box<Dog> dogBox를 전달한다. 와일드 카드 ?는 모든 타입을 받을 수 있다.
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가 모두 입력될 수 있다.

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

  • 제네릭 타입이나 제네릭 메서드를 정의하는 게 꼭 필요한 상황이 아니라면, 더 단순한 와일드카드 사용을 권장

2-8. 와일드카드2

상한 와일드카드

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

// 와일드카드 사용  
static <T extends Animal> void printWildcardV2(Box<? extends Animal> box) {  
    Animal animal = box.get();  
    System.out.println("이름 = " + animal.getName());  
}
  • 제네릭 메서드와 마찬가지로 와일드카드에도 상한 제한을 둘 수 있다.
  • 여기서는 ? extends Animal을 지정했다.
    • Animal과 그 하위 타입만 입력 받는다. 만약 다른 타입을 입력하면 컴파일 오류가 발생한다.
  • box.get()을 통해서 꺼낼 수 있는 타입의 최대 부모는 Animal이다. 따라서 Animal 타입으로 조회할 수 있다.
  • 결과적으로 Animal타입의 기능을 호출할 수 있다.

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

  • 와일드카드는 제네릭을 정의할 때 사용하는 것이 아니다.
  • 인자가 전달된 제네릭 타입을 활용할 때 사용한다.
  • 다음과 같은 경우에는 제네릭 타입이나 제네릭 메서드를 사용해야 문제를 해결할 수 있다.
static <T extends Animal> T printAndReturnGeneric(Box<T> box) {  
    T t = box.get();  
    System.out.println("이름 = " + t.getName());  
    return t;  
}

// 와일드카드 사용  
static Animal printAndReturnWildcard(Box<? extends Animal> box) {  
    Animal animal = box.get();  
    System.out.println("이름 = " + 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);  

    }  

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

// 결과
animal = Animal{name='멍멍이', size=100}

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

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

참고로 하한은 제네릭 타입, 제네릭 메서드에슨 사용할 수 없고, 와일드카드에만 사용할 수 있다.

2-9. 타입 이레이저

  • 제네릭은 자바 컴파일 단계에서만 사용되고, 컴파일 이후에는 제네릭 정보가 삭제된다.
  • 제네릭에 사용한 타입 매개변수가 모두 사라진다.
  • 컴파일 전인 .java에는 제네릭의 타입 매개변수가 존재하지만, 컴파일 이후인 자바 바이트코드 .class 에는 타입 매개변수가 존재하지 않는 것이다.

다음과 같은 방식으로 동작한다.
(100% 정확한 건 아니다. 대략 이런 방식임.)

제네릭 타입 선언

public class GenericBox<T> {

    private T value;

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

    public T get() {
        return value;
    }

}

제네릭 타입을 선언했다.

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

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

이렇게 하면 자바 컴파일러는 컴파일 시점에 타입 매개변수와 타입 인자를 포함한 제네릭 정보를 포함해서 new GenericBox<Integer>() 에 대해 다음과 같이 이해한다.

그래서 다음과 같이 제네릭 타입 TInteger 가 된다.

public class GenericBox<Integer> {

    private Integer value;

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

    public Integer get() {
        return value;
    }

}

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

컴파일 후

public class GenericBox {

    private Object value;

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

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

타입 매개변수 제한의 경우

다음과 같이 타입 매개변수를 제한하면 제한한 타입으로 코드를 변경한다.

컴파일 전

public class AnimalHospitalv3<T extends 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 {

    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) animalHospital.getBigger(new Dog());
  • 반환 받는 부분을 Animal 로 받으면 안되기 때문에 자바 컴파일러가 타입 인자로 지정한 Dog로 캐스팅하는 코드를 넣어준다.

자바의 제네릭은 단순하게 생각하면 개발자가 직접 캐스팅하는 코드를 컴파일러가 대신 처리해주는 것이다.
자바는 컴파일 시점에 제네릭을 사용한 코드에 문제가 없는지 완벽하게 검증하기 때문에 자바 컴파일러가 추가하는 다운 캐스팅에는 문제가 발생하지 않는다.

자바의 제네릭 타입은 컴파일 시점에만 존재하고, 런타임 시에는 제네릭 정보가 지워지는 것타입 이레이저라 한다.

타입 이레이저 방식의 한계

컴파일 이후에는 제네릭의 타입 정보가 존재하지 않는다.
.class로 자바를 실행하는 런타임에는 우리가 지정한 Box<Integer>, Box<String 의 타입 정보가 모두 제거된다.

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

class EraserBox<T> {

    public boolean instanceCheck(Object param) {
        return param instanceof T; // 오류
    }

    pucblic void create() {
        return new T(); // 오류
    }

}

런타임

class EraserBox {

    public boolean instanceCheck(Object param) {
        return param instanceof Object; // 오류
    }

    public void create() {
        return new Obect(); // 오류
    }

}

컴파일 오류가 난다.

  • 여기서는 T 는 런타임에 모두 Object 가 되어버린다.
  • instanceof 는 항상 Object와 비교하게 된다.
    • 이렇게 되면 항상 참이 반환되는 문제가 발생한다.
    • 자바는 이런 문제 때문에 타입 매개변수에 instanceof 를 허용하지 않는다.
  • new T 는 항상 new Object 가 되어버린다.
    • 개발자가 의도한 것과는 다르다.
    • 자바는 타입 매개변수에 new 를 허용하지 않는다.

3. 요약

  • 타입 매개변수와 타입 매개변수에 대한 다형성
    • 타입 상한
  • 제네릭 메서드
  • 와일드카드
    • 와일드카드 상한
    • 와일드카드 하한
  • 타입 이레이저

에 대해 학습해 보았다.

728x90
Comments