관리 메뉴

나구리의 개발공부기록

제네릭, 타입 매개변수 제한(시작, 다형성 시도, 제네릭 도입과 실패, 타입 매개변수 제한), 제네릭 메서드, 제네릭 메서드 활용, 와일드 카드, 타입 이레이저 본문

인프런 - 실전 자바 로드맵/실전 자바 - 중급 2편

제네릭, 타입 매개변수 제한(시작, 다형성 시도, 제네릭 도입과 실패, 타입 매개변수 제한), 제네릭 메서드, 제네릭 메서드 활용, 와일드 카드, 타입 이레이저

소소한나구리 2025. 2. 2. 18:57
728x90

출처 : 인프런 - 김영한의 실전 자바 - 중급2편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 타입 매개변수 제한

1) 시작

(1) 요구사항

  • 개 병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있는 동물 병원 작성

(2) DogHospital

  • 개 병원은 내부에 Dog 타입을 가지며 개의 이름과 크기를 출력하고 개의 sound()메서드를 호출하는 checkup()메서드와 다른 개와 크기를 비교하는 bigger() 메서드를 가지고 있음
package generic.ex3;

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

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

 

(3) CatHospital

  • 타입만 Cat으로 변경되었을 뿐 DogHospital 완전히 동일함

(4) AnimalHospitalMainV0

  • 요구사항대로 개 병원은 개만 받을 수 있고 고양이 병원은 고양이만 받을 수 있도록 요구사항이 지켜져서 개발되었음
  • 개 병원과 고양이 병원을 각각 별도의 클래스로 만들었기 때문에 개 병원에 고양이를 전달하면 컴파일 오류가 발생함
  • 개 병원에서 bigger()로 다른 개를 비교하는 경우 더 큰 개를 Dog 타입으로 반환함
package generic.ex3;

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}
*/

 

(5) 문제

  • 타입 안정성은 명확하게 지켜졌으나 개 병원과 고양이 병원의 코드는 중복이 많으며 코드 재사용성이 없음

2) 다형성 시도

(1) AnimalHospitalV1

  • Dog, Cat은 Animal이라는 명확한 부모 타입이 있기 때문에 다형성을 사용하여 중복 제거를 시도
  • Animal 타입을 받아서 처리하며, checkup(), getBigger()에서 사용하는 getName(), getSize(), sound()메서드는 모두 Animal 타입이 제공하는 메서드이기 때문에 아무 문제 없이 모두 호출할 수 있음
package generic.ex3;

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

 

(2) AnimalHospitalMainV1

  • 다형성을 통해 AnimalHospitalV1 하나로 개와 고양이를 모두 처리할 수 있어서 코드 재사용성이 좋아졌음
  • 그러나 요구사항과는 다르게 개 병원에 고양이를 전달이 가능해지는 문제가 발생하게 됨
  • bigger()메서드의 반환 타입이 Animal이기 때문에 다운 캐스팅을 해야하고, 실수로 고양이를 입력 후 개를 반환해야하는 상황이라면 캐스팅 예외가 발생함
package generic.ex3;

public class AnimalHospitalMainV1 {
    public static void main(String[] args) {

        AnimalHospitalV1 dogHospital = new AnimalHospitalV1();
        AnimalHospitalV1 catHospital = new AnimalHospitalV1();
        
        // ... 기존 코드 동일 생략

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

        // 문제2: 개 타입 변환 -> 다운 캐스팅 필요
        dogHospital.set(dog);
        Dog biggerDog = (Dog) dogHospital.bigger(new Dog("멍멍이2", 200));
        System.out.println("biggerDog = " + biggerDog);
    }
}

3) 제네릭 도입과 실패

(1) AnimalHospitalV2

  • <T>를 사용하여 제네릭 타입을 선언했지만 기존의 메서드들을 모두 사용할 수 없어 컴파일 오류가 발생함
  • 제네릭 타입을 선언하면 자바 컴파일러 입장에서 T에 어떤 값이 들어올지 예측할 수 없음
  • Animal 타입의 자식이 들어오기를 기대했지만 Animal에 대한 정보가 어디에도 없으므로 어떤 타입이든 모두 들어올 수 있게 되고, 자바 컴파일러는 어떤 타입이 들어올 지 알 수 없는 경우에는 모든 객체의 최종 부모인 Object 타입으로 가정하게되어 Object가 제공하는 메서드만 호출할 수 있게 됨
  • 즉, 원하는 기능을 사용하려면 Animal 타입이 제공하는 기능들이 필요하지만 이 기능들을 모두 사용할 수 없음
package generic.ex3;

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

 

(2) AnimalHospitalMainV2

  • 여기에 추가로 문제가 더있는데, 동물 병원에 Integer, Object 같은 동물과 전혀 관계 없는 타입을 타입 인자로 전달이 가능하게 됨
  • 최소한 Animal이나 그 자식의 타입만으로 제한하고 싶지만 모든 타입이 들어올 수 있도록 변경됨
package generic.ex3;

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

 

(3) 문제 정리와 해결방안

  • 제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있음
  • 타입 매개변수를 어떤 타입이든 수용할 수 있는 Object로 가정하고 Object의 기능만 사용할 수 있게됨
  • 발생한 문제들을 생각해보면 타입 매개변수를 Animal로 제한하지 않았기 때문이므로 만약 타입 인자가 모두 Animal과 그 자식만 들어올 수 있게 제한할 수 있다면 문제를 해결할 수 있음

4) 타입 매개변수 제한

(1) AnimalHospitalV3<T extends Animal>

  • 타입 매개변수 T를 Animal과 그 자식만 받을 수 있도록 제한을 둠
  • T의 상한을 Animal으로 제한을 두었기 때문에 타입 인자로 들어올 수 있는 타입이 Animal과 그 자식으로 제한되어 자바 컴파일러가 T에 입력될 수 있는 값의 범위를 예측할 수 있게 됨
  • 타입 매개변수 T에는 Animal과 그의 자식인 Dog, Cat만 들어올 수 있으므로 이를 모두 수용할 수 있는 Animal을 T의 타입으로 가정해도 문제가 없어지게 되고, Animal이 제공하는 메서드들도 모두 사용할 수 있게 됨
package generic.ex3;

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

 

(2) AnimalHospitalMainV3

  • 타입 매개변수에 입력될 수 있는 상한을 지정하여 기존의 문제를 모두 해결하면서 요구사항을 지킬 수 있게 됨
  • dogHospital.set()에 Dog가 아닌 다른타입을 입력하면 컴파일 오류가 발생하고, bigger()메서드를 사용할 때에도 각 타입에 맞게 타입이 반환됨
  • 거기에 AnimalHospitalV3<Integer>와 같이 동물과 전혀 관계없는 타입 인자를 컴파일 시점에 막을 수 있게 됨
package generic.ex3;

public class AnimalHospitalMainV3 {
    public static void main(String[] args) {

        AnimalHospitalV3<Dog> dogHospital = new AnimalHospitalV3<>();
        AnimalHospitalV3<Cat> catHospital = new AnimalHospitalV3<>();
        
        // Animal, Dog, Cat 외의 타입이 인자로 들어오면 컴파일 오류 발생
//        AnimalHospitalV3<Integer> integerHospital = 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);
    }
}

 

5) 기존 문제와 해결

(1) 타입 안정성이 없는 문제들 해결

  • 개 병원에 고양이를 전달하는 문제
  • Animal 타입을 반환하여 다운 캐스팅을 해야하는 문제
  • 실수로 고양이를 입력했는데 개를 반환하는 상황이 되면 캐스팅 예외가 발생하는 문제

(2) 제네릭 도입 문제들 해결

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

(3) 정리

  • 제네릭에 타입 매개변수 상한을 사용하여 타입 안전성을 지키면서 상위 타입의 원하는 기능까지 사용할 수 있게 되어 코드 재사용과 타입 안전성이라는 두 마리 토끼를 동시에 잡을 수 있게 됨

2. 제네릭 메서드

1) 제네릭 메서드

(1) GenericMethod

  • 제네릭을 클래스에사용하지 않고 메서드에 적용할 수 있는데 이를 제네릭 메서드라고 하며 제네릭 타입과는 다른 기능을 제공함
  • 마찬가지로 타입 매개변수의 상한도 적용할 수 있음
package generic.ex4;

public class GenericMethod {

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

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

    public static <T extends Number> T numberMethod(T t) {
        System.out.println("bound print: " + t);
        return t;
    }
}

 

(2) MethodMain1

  • 제네릭 메서드를 사용하는 코드들로 메서드를 호출할 때 타입을 전달함
package generic.ex4;

public class MethodMain1 {
    public static void main(String[] args) {

        Integer i = 10;
        Object object = GenericMethod.objMethod(i);

        // 타입 인자(Type Argument) 명시적 전달
        Integer result = GenericMethod.<Integer>genericMethod(i);
        Integer integerValue = GenericMethod.<Integer>numberMethod(10);
        Double doubleValue = GenericMethod.<Double>numberMethod(20.0);
    }
}
/* 실행 결과
Object print: 10
generic print: 10
bound print: 10
bound print: 20.0
*/

 

 

(3) 제네릭 타입

  • 정의: GenericClass<T>
  • 타입 인자 전달: 객체를 생성하는 시점

(4) 제네릭 메서드

  • 정의: <T> T genericMethod(T t)
  • 타입 인자 전달: 메서드를 호출하는 시점
  • 제네릭 메서드는 클래스 전체가 아니라 특정 메서드 단위로 제네릭을 도입할 때 사용함
  • 제네릭 메서드를 정의할 때는 메서드의 반환 타입 왼쪽에 다이아몬드를 사용하여 <T>와 같이 타입 매개변수를 적어주며 메서드를 실제 호출하는 시점에 다이아몬드를 사용하여 <Integer>와 같이 타입을 정하고 호출함
  • 즉, 제네릭 메서드의 핵심은 메서드를 호출하는 시점에 타입 인자를 전달해서 타입을 지정하는 것임

(5) 인스턴스 메서드, static 메서드에서의 제네릭 메서드

  • 제네릭 메서드는 인스턴스 메서드와 static 메서드에 모두 적용할 수 있음
  • 제네릭 타입은 static 메서드에 타입 매개변수를 적용할 수 없는데, 제네릭 타입은 객체를 생성하는 시점에 타입이 정해지기 때문에 클래스 단위로 동작하는 static 메서드는 제네릭 타입과 무관하므로 static 메서드에 제네릭을 도입하려면 제네릭 메서드를 사용해야 함
class Box<T> { //제네릭 타입

    static <V> V staticMethod2(V t) {} //static 메서드에 제네릭 메서드 도입
    <Z> Z instanceMethod2(Z z) {}      //인스턴스 메서드에 제네릭 메서드 도입 가능

    T instanceMethod(T t) {}       //가능
    static T staticMethod1(T t) {} //제네릭 타입의 T 사용 불가능
}

 

(6) 타입 매개변수 제한

  • 제네릭 메서드도 제네릭 타입과 마찬가지로 타입 매개변수를 제한할 수 있음
  • numberMethod()의 타입 매개변수를 Number로 제한했기 때문에 Number와 그의 자식만 받을 수 있어 String을 인자로 전달하거나 문자열을 메서드의 매개변수로 입력하면 오류가 발생함

** 참고

  • Integer, Double, Long과 같은 숫자 타입이 Number의 자식임
public static <T extends Number> T numberMethod(T t) {}
//GenericMethod.numberMethod("Hello"); // 컴파일 오류 Number의 자식만 입력 가능

 

(7) 제네릭 메서드 타입 추론

  • 제네릭 메서드를 호출할 때 <Integer>와 같이 타입 인자를 계속 전달하게되면 불편함
  • 자바 컴파일러는 genericMethod()에 전달되는 인자 i의 타입이 Integer라는 것을 알 수 있고, 반환 타입이 Integer result라는 것도 알 수 있으므로, 이런 정보를 통해 자바 컴파일러가 타입 인자를 추론할 수 있어서 타입 인자를 생략할 수 있음
  • 타입을 추론하게 되면 컴파일러가 대신 처리하기 때문에 타입을 전달하지 않는 것 처럼 보이지만 실제로는 타입 인자가 전달된다는 것을 기억하면 됨
// 타입 인자 생략
Integer result2 = GenericMethod.genericMethod(i);
Integer integerValue2 = GenericMethod.numberMethod(10);
Double doubleValue2 = GenericMethod.numberMethod(20.0);

3. 제네릭 메서드 활용

1) 제네릭 타입으로 만든 AnimalHospitalV3을 제네릭 메서드로 변경

(1) AnimalMethod

  • checkup(), bigger()의 메서드를 제네릭 메서드로 정의하고 사용하기 쉽게 static으로 변경
  • 두 메서드 모두 Animal을 상한으로 제한함
package generic.ex4;

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

 

(2) MethodMain2

  • 타입 추론을 활용하여 제네릭 메서드를 호출할 때 타입 인자를 생략하고 호출함
  • bigger()메서드를 호출 시 동일한 타입을 메서드의 매개변수로 입력해주어야 정상적으로 호출이 가능하고, 서로 다른 타입을 입력하면 타입 추론에서 컴파일 오류가 발생하여 타입 안정성을 지킬 수 있게됨
  • 제네릭 타입으로 만들었던 코드와 동일한 출력결과를 얻을 수 있는 것을 확인할 수 있음
package generic.ex4;

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

        AnimalMethod.checkup(dog);
        AnimalMethod.checkup(cat);

        Dog targetDog = new Dog("큰 멍멍이", 200);
        Dog bigger = AnimalMethod.bigger(dog, targetDog);
        System.out.println("bigger = " + bigger);
    }
}
/* 실행 결과
동물 이름: 멍멍이
동물 크기: 100
멍멍
동물 이름: 야옹이
동물 크기: 100
야옹
bigger = Animal{name='큰 멍멍이', size=200}
*/

 

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

  • 정적 메서드는 제네릭 메서드만 적용할 수 있지만 인스턴스 메서드는 제네릭 타입과 제네릭 메서드를 둘다 적용할 수 있음
  • 제네릭 타입도 T고 제네릭 메서드도 T라고 되어있으면 제네릭 타입보다 제네릭 메서드가 높은 우선순위를 가지기 때문에 printAndReturn()은 제네릭 타입은 무시가 되고 제네릭 메서드가 적용됨
  • 항상 프로그래밍에서는 구체적인것이 우선순위가 되기 때문에 클래스레벨보다 더 구체적인 메서드 레벨의 단위가 먼저 적용된다고 생각하면 됨
  • 여기서 적용된 제네릭 메서드의 타입 매개변수 T는 상한 제한이 없으므로 Object로 취급이 되기 때문에 제네릭 메서드에서 t.getName()처럼 Animal에 존재하는 메서드는 호출할 수 없게됨
  • 참고로 프로그래밍에서 이렇게 모호하게 작성하는 것은 매우 좋지 않기 때문에 이렇게 이름이 겹치면 제네릭 타입이든, 제네릭 메서드든 무엇이든 둘중에 하나를 이름을 다른것으로 변경하는 것이 좋음
package generic.ex4;

import generic.animal.Animal;

public class ComplexBox <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());
//        t.getName(); // 제네릭 메서드는 <T> 타입이므로 Object로 취급되어 호출 불가능
        return t;
    }
}

package generic.ex4;

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

        ComplexBox<Dog> hospital = new ComplexBox<>();

        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=50}
*/

4. 와일드 카드

1) 와일드 카드

(1) 설명

  • 제네릭 타입을 조금 더 편리하게 사용할 수 있는 기능
  • 컴퓨터 프로그래밍에서 *, ?와 같이 하나 이상의 문자들을 상징하는 특수 문자를 뜻하는데, 이를 제네릭에 적용하면 여러 타입이 들어올 수 있다는 뜻임

(2) Box<T>

  • 단순히 데이터를 넣고 반환할 수 있는 제네릭 타입
package generic.ex5;

public class Box<T> {
    private T value;
    
    public void set(T value) {
        this.value = value;
    }
    
    public T get() {
        return value;
    }
}

 

(3) WildcardEx

  • 제네릭 메서드와 와일드 카드를 비교할 수 있도록 같은 기능을 하나씩 배치
  • 와일드 카드는 ?를 사용하여 정의함
package generic.ex5;

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

 

(4) WildcardMain1

  • 와일드 카드를 사용할 때와 제네릭 메서드를 사용할 때 모두 동일하게 동작하도록 메서드를 정의했으므로 모두 똑같은 기능으로 출력되는 것을 확인할 수 있음
package generic.ex5;

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}
이름 = 멍멍이
이름 = 멍멍이
이름 = 멍멍이
이름 = 멍멍이
*/

 

** 참고

  • 와일드카드는 제네릭 타입이나 제네릭 메서드를 선언하는 것이 아니라 이미 만들어진 제네릭 타입을 활용할 때 사용하는 것임

(5) 비제한 와일드 카드

  • 두 메서드는 비슷한 기능을 하는 코드이지만 하나는 제네릭 메서드를 사용하고 하나는 일반적인 메서드에 와일드카드를 사용한 것임
  • 와일드 카드는 제네릭 타입이나 제네릭 메서드를 정의할 때 사용하는 것이 아니라 Box<Dog>, Box<Cat> 처럼 타입 인자가 정해진 제네릭 타입을 전달 받아서 활용할 때 사용함
  • 와일드 카드인 ?는 모든 타입을 다 받을 수 있다는 뜻으로 ?는 <? extends Object>처럼 해석할 수 있으며 아래의 printWildcardV1(Box<?> box) 메서드에서 box.get()의 반환타입을 받아보면 Object 타입으로 반환 되는 것을 확인할 수 있음
  • 이렇게 ?만 사용하여 제한 없이 모든 타입을 다 받을 수 있는 와일드카드를 비제한 와일드 카드라고 함
// 제네릭 메서드, Box<Dog> dogBox를 전달하면 타입 추론에 의해 타입 T가 Dog가 됨
static <T> void printGenericV1(Box<T> box) {
    System.out.println("T = " + box.get());
}

// 제네릭 메서드가 아닌 일반적인 메서드, 와일드 카드 ?는 모든 타입을 받을 수 있음
static void printWildcardV1(Box<?> box) {
    System.out.println("? = " + box.get());
    Object object = box.get();
}

 

(6) 제네릭 메서드 vs 와일드 카드

  • 제네릭 메서드 실행 예시: 인자 전달 -> 제네릭 타입 결정(타입 추론) -> 타입 인자 결정 -> 최종 실행
  • 와일드 카드 실행 예시: 인자 전달 -> 전달 된 인자로 메서드 실행
  • 제네릭 메서드는 타입 매개변수가 존재하고 특정 시점에 타입 매개변수에 타입인자를 전달하여 타입을 결정해야하는 등, 과정이 와일드카드에 비해 복잡함
  • 와일드 카드는 일반적인 메서드에 사용하며 단순히 매개변수로 제네릭 타입을 받을 수 있는 것이므로 제네릭 메서드처럼 타입을 결정하거나 복잡하게 작동하지 않음
  • 제네릭 타입이나 제네릭 메서드를 정의하는 꼭 필요한 상황이 아니라면 더 단순한 와일드카드 사용을 권장함

(7) 상한 와일드카드

  • 제네릭 메서드와 마찬가지로 와일드카드에도 상한 제한을 둘 수 있으며 여기서는 ? extend Animal로 지정하였음
  • 이렇게 되면 Animal과 그 하위 타입만 입력받을 수 있으므로 다른 타입을 입력하면 컴파일 오류가 발생하게 됨
  • box.get()을 통해서 꺼낼 수 있는 타입의 최대 부모는 Animal이 되므로 Animal 타입으로 조회할 수 있고 그 기능을 호출 할 수 있음
static <T extends Animal> void printGenericV2(Box<T> box) {
    T t = box.get();
    System.out.println("이름 = " + t.getName());
}

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

 

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

  • 와일드 카드는 제네릭을 정의할 때 사용하는 것이 아니고 타입 인자가 전달된 제네릭 타입을 활용할 때 사용되므로 반환타입이 있는 경우에는 제네릭 타입이나 제네릭 메서드를 사용해야 문제를 해결할 수 있음
  • 제네릭 메서드를 활용한 printAndReturnGeneric(dogBox)메서드를 호출한 경우 정확하게 전달한 타입을 명확하게 반환하지만, printAndReturnWildcard()의 경우 전달한 타입을 명확하게 반환하지 않고 Animal 타입으로 반환하고 있음
  • 메서드의 타입들을 특정 시점에 변경하려면 제네릭 타입이나 제네릭 메서드를 사용해야 인자로 전달된 타입으로 타입을 결정 지을 수 있음
  • 와일드카드는 이미 만들어진 제네릭 타입을 전달받아서 활용할 때 사용하기 때문에 메서드의 타입들을 타입 인자를 통해 변경할 수가 없고 단순히 일반 메서드에 사용한다고 생각하면 됨
  • 제네릭 타입이나 제네릭 메서드가 꼭 필요한 상황에서만 <T>를 사용하고, 그렇지 않은 일반적인 상황에서는 와일드 카드를 사용하는 것을 권장함
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;
}

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

 

(9) 하한 와일드 카드

  • 와일드카드는 상한 뿐만 아니라 하한도 지정할 수 있음
  • writeBox(Box<? super Animal> box)처럼 와일드 카드에 Animal 타입을 포함한 Animal타입의 상위 타입만 입력 받을 수 있음
  • 즉, 하한을 Animal로 제한 했기 때문에 writeBox() 메서드를 호출할 때 Animal의 하위 타입인 dogBox와 catBox를 전달하려고 하면 컴파일 오류가 발생함
  • 참고로 제네릭 타입이나 제네릭 메서드에는 사용할 수 없고 와일드 카드에만 하한을 적용할 수 있음
package generic.ex5;

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}
*/

5. 타입 이레이저

1) 타입 이레이저

(1) 설명

  • 이레이저(eraser)의 뜻인 지우개 처럼 제네릭은 자바 컴파일 단계에서만 사용되고 컴파일 이후에는 제네릭 정보가 삭제됨
  • 컴파일 전인 .java에는 제네릭 타입 매개변수가 존재하지만, 컴파일 이후인 자바 바이트코드 .class에는 타입 매개변수가 존재하지 않음
  • 아래에 설명하는 코드는 100% 정확한 코드는 아니고 대략 이런 방식으로 동작한다고 보여주는 코드임

(2) 제네릭 타입 선언 - GenericBox.java

public class GenericBox<T> {

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

 

(3) 제네릭 타입에 Integer 타입 인자 전달 - Main.java

  • 이렇게 하면 자바 컴파일러는 컴파일 시점에 타입 매개변수와 타입 인자를 포함한 제네릭 정보를 활용하여 new GenericBox<Integer>()에 대해 선언된 제네릭 박스를 아래처럼 이해하게 됨
void main() {
    GenericBox<Integer> box = new GenericBox<Integer>();
    box.set(10);
    Integer result = box.get();
}
public class GenericBox<Integer> {

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

 

(4) 컴파일 후 - GenericBox.class, Main.class

  • 컴파일이 모두 끝나면 자바는 제네릭과 관련된 정보를 삭제하며 .class에 생성된 정보는 아래와 같음
  • 상한 제한 없이 선언한 타입 매개변수 T는 Object로 변환되고, 자바 컴파일러가 제네릭 타입에서 타입 인자로 지정한 Integer로 캐스팅하는 코드를 추가해줌
  • 이렇게 추가된 코드들은 자바 컴파일러가 이미 검증하고 추가했기 때문에 문제가 발생하지 않음
public class GenericBox {

    private Object value;

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

    public Object get() {
        return value;
    }
}
void main() {
    GenericBox box = new GenericBox();
    box.set(10);
    Integer result = (Integer) box.get(); //컴파일러가 캐스팅 추가
}

 

(5) 타입 매개변수 제한의 경우

  • 컴파일 전에 적용한 타입 매개변수 T가 컴파일 후에는 상한으로 지정된 Animal 타입으로 대체되기 때문에 타입 정보가 제거되어도 Animal 타입의 메서드를 사용하는데 아무런 문제가 없음
  • 반환 받는 부분도 자바 컴파일러가 타입 인자로 지정한 Dog로 다운 캐스팅하는 코드를 자동으로 넣어줌
// AnimalHospitalV3.java - 컴파일 전
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.class - 컴파일 후
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;
    }
}


// 사용코드 컴파일 전,후
Dog dog = animalHospitalV3.getBigger(new Dog());        // 컴파일 전
Dog dog = (Dog) animalHospitalV3.getBigger(new Dog());  // 컴파일 후

 

(6) 정리

  • 자바의 제네릭은 단순하게 생각하면 개발자가 직접 캐스팅 하는 코드를 컴파일러가 대신 처리해주는 것이라고 보면 됨
  • 자바는 컴파일 시점에 제네릭을 사용한 코드에 문제가 없는지 완벽하게 검증하기 때문에 자바 컴파일러가 추가하는 다운 캐스팅에는 문제가 발생하지 않음
  • 자바의 제네릭 타입은 컴파일 시점에만 존재하고, 런타임 시에는 제네릭 정보가 지워지는데 이것을 타입 이레이저라고 함

(7) 타입 이레이저 방식의 한계

  • 컴파일 이후에는 제네릭의 타입 정보가 존재하지 않기 때문에 런타임에 타입을 활용하는 코드들은 사용할 수 없음
  • T는 런타임에 모두 Object가 되어버리면 instanceof T는 항상 Object와 비교하게되고 항상 참이 반환되어버리고, new T는 항상 new Object가 되어버림
  • 이렇게 동작하는 것은 개발자가 의도하는 것과 다르게 동작하게 되는 것이므로 자바는 타입 매개변수에 instanceof와 new를 허용하지 않음
// 소스 코드
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(); // 오류
    }
}

6. 문제와 풀이

1) 준비

(1) 준비 코드

  • BioUnit은 유닛들의 부모 클래스
  • BioUnit의 자식 클래스로 Marine, Zealot, Zergling이 있음
더보기

BioUnit

package generic.test.ex3.unit;

public class BioUnit {
    
    private String name;
    private int hp;

    public BioUnit(String name, int hp) {
        this.name = name;
        this.hp = hp;
    }

    public String getName() {
        return name;
    }

    public int getHp() {
        return hp;
    }

    @Override
    public String toString() {
        return "BioUnit{" +
                "name='" + name + '\'' +
                ", hp=" + hp +
                '}';
    }
}

 

Marine, Zealot, Zergling

package generic.test.ex3.unit;

public class Marine extends BioUnit {

    public Marine(String name, int hp) {
        super(name, hp);
    }
}

package generic.test.ex3.unit;

public class Zealot extends BioUnit{

    public Zealot(String name, int hp) {
        super(name, hp);
    }
}

package generic.test.ex3.unit;

public class Zergling extends BioUnit{

    public Zergling(String name, int hp) {
        super(name, hp);
    }
}

2) 제네릭 메서드와 상한

(1) 문제 설명

  • 다음 코드와 실행 결과를 참고하여 UnitUtil 클래스를 생성
  • UnitUtil.maxHp()메서드의 조건은 아래와 같음
  • 1. 두 유닛을 입력 받아서 체력이 높은 유닛을 반환하고, 체력이 같은 경우 둘 중 아무나 반환해도 됨
  • 2. 제네릭 메서드를 사용해야 함
  • 3. 입력하는 유닛의 타입과 반환하는 유닛의 타입이 같아야 함
더보기
package generic.test.ex3;

import generic.test.ex3.unit.Marine;
import generic.test.ex3.unit.Zealot;

public class UnitUtilTest {
    public static void main(String[] args) {
        Marine m1 = new Marine("마린1", 40);
        Marine m2 = new Marine("마린2", 50);
        Marine resultMarine = UnitUtil.maxHp(m1, m2);
        System.out.println("resultMarine = " + resultMarine);
        
        Zealot z1 = new Zealot("질럿1", 100);
        Zealot z2 = new Zealot("질럿2", 150);
        Zealot resultZealot = UnitUtil.maxHp(z1, z2);
        System.out.println("resultZealot = " + resultZealot);
    }
}

 

실행 결과

resultMarine = BioUnit{name='마린2', hp=50}

resultZealot = BioUnit{name='질럿2', hp=150}

 

(2) 정답

더보기
package generic.test.ex3;

public class UnitUtil {
    
    public static <T extends BioUnit> T maxHp(T t1, T t2) {
        return t1.getHp() > t2.getHp() ? t1 : t2;
    }
}

3) 제네릭 타입과 상한

(1) 문제 설명

  • 다음 코드와 실행 결과를 참고하여 Shuttle 클래스를 생성
  • Shuttle 클래스의 조건은 아래와 같음
  • 1. 제네릭 타입을 사용해야 함
  • 2. showInfo() 메서드를 통해 탑승한 유닛의 정보를 출력
더보기
package generic.test.ex3;

import generic.test.ex3.unit.Marine;
import generic.test.ex3.unit.Zealot;
import generic.test.ex3.unit.Zergling;

public class ShuttleTest {
    public static void main(String[] args) {
        Shuttle<Marine> shuttle1 = new Shuttle<>();
        shuttle1.in(new Marine("마린", 40));
        shuttle1.showInfo();
        
        Shuttle<Zergling> shuttle2 = new Shuttle<>();
        shuttle2.in(new Zergling("저글링", 35));
        shuttle2.showInfo();
        
        Shuttle<Zealot> shuttle3 = new Shuttle<>();
        shuttle3.in(new Zealot("질럿", 100));
        shuttle3.showInfo();
    }
}

 

실행 결과

이름: 마린, HP: 40

이름: 저글링, HP: 35

이름: 질럿, HP: 100

 

(2) 정답

더보기
package generic.test.ex3;

public class Shuttle<T extends BioUnit> {

    private T unit;

    public void in(T unit) {
        this.unit = unit;
    }

    public T out() {
        return unit;
    }

    public void showInfo() {
        System.out.println("이름: " + unit.getName() + ", HP: " + unit.getHp());
    }
}

4) 제네릭 메서드와 와일드 카드

(1) 문제 설명

  • 앞서 만든 문제에서 만든 Shuttle을 활용하고 다음 코드와 실행 결과를 참고하여 UnitPrinter 클래스를 생성
  • UnitPrinter 클래스의 조건은 다음과 같음
  • 1. UnitPrinter.printV1()은 제네릭 메서드로 구현해야함
  • 2. UnitPrinter.printV2()는 와일드카드로 구현해야 함
  • 이 두 메서드는 셔틀에 들어있는 유닛의 정보를 출력함
더보기
package generic.test.ex3;

import generic.test.ex3.unit.Marine;
import generic.test.ex3.unit.Zealot;
import generic.test.ex3.unit.Zergling;

public class UnitPrinterTest {
    
    public static void main(String[] args) {
        Shuttle<Marine> shuttle1 = new Shuttle<>();
        shuttle1.in(new Marine("마린", 40));
        
        Shuttle<Zergling> shuttle2 = new Shuttle<>();
        shuttle2.in(new Zergling("저글링", 35));
        
        Shuttle<Zealot> shuttle3 = new Shuttle<>();
        shuttle3.in(new Zealot("질럿", 100));
        
        UnitPrinter.printV1(shuttle1);
        UnitPrinter.printV2(shuttle1);
    }
}

 

실행 결과

이름: 마린, HP: 40

이름: 마린, HP: 40

(2) 정답

더보기
package generic.test.ex3;

public class UnitPrinter {

    static <T extends BioUnit> void printV1(Shuttle<T> shuttle) {
        T t = shuttle.out();
        System.out.println("이름: " + t.getName() + ", HP: " + t.getHp());
    }

    static void printV2(Shuttle<?> shuttle) {
        BioUnit unit = shuttle.out();
        System.out.println("이름: " + unit.getName() + ", HP: " + unit.getHp());
    }
}

5) 정리

  • 실무에서 직접 제네릭을 사용해서 무언가를 설계하거나 만드는 일은 드물며 대부분 이미 제네릭을 통해 만들어진 프레임워크나 라이브러리들을 가져다 사용하는 경우가 훨씬 많음
  • 그래서 이미 만들어진 코드의 제네릭을 읽고 이해하는 정도면 충분하며 실무에서 직접 제네릭을 사용하더라고 어렵고 복잡하게 사용하는 것이 아니라 보통 단순하게 사용하므로 지금까지 학습한 정도면 실무에서 필요한 제네릭은 충분이 이해했다고 볼 수 있음
  • 제네릭은 지금까지 설명한 내용보다 더 복잡하고 더 어려운 공변(covariant), 반변(contravariant)과 같은 개념들도 있는데, 이런 개념들을 이해하면 와일드카드가 존재하는 이유도 더 깊이있게 알 수 있음
  • 그러나 제네릭을 사용해서 매우 복잡한 라이브러리나 프레임워크를 직접 설계하지 않는 이상 이런 개념들을 꼭 이해할 필요는 없음
  • 이러한 부분은 실무에서 많은 경험을 쌓고 본인이 필요하다고 느껴질 때 따로 공부하는 것을 권장함
  • 제네릭은 이후에 설명하는 컬렉션 프레임워크에서 가장 많이 사용되기 때문에 컬렉션 프레임워크를 통해서 제네릭이 어떻게 활용되는지 자연스럽게 학습할 수 있음
728x90