Java

제네릭(Generic) 타입 (+ 와일드카드, 이레이저)

밍구밍구밍 2024. 5. 12. 17:32

※ 제네릭의 핵심 : 사용할 타입을 미리 결정하지 않는다. 클래스 내부에서 사용하는 타입을 클래스를 정의하는 시점에 결정하는 것이 아니라 실제 사용하는 생성 시점에 타입을 결정한다.

 

1. 매서드와 제네릭 타입의 매개변수 개념

1) 메서드 :

- 메서드의 매개변수는 사용할 값에 대한 결정을 나중으로 미루는 것

- 메서드는 매개변수에 인자를 전달해서 사용할 값을 결정한다.

 

2) 제네릭타입 :

- 제네릭 타입 매개변수는 사용할 타입에 대한 결정을 나중으로 미루는 것

- 제네릭 클래스는 타입 매개변수에 타입 인자를 전달해서 사용할 타입을 결정한다.

public static void main(String[] args) {

    GenericBox<Integer> integerBox = new GenericBox<>(); // Integer : 타입 인자
public class GenericBox<T> {

    private T value; // T : 타입 매개변수

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

    public T get() {
        return value;
    }
}

 

** 제네릭 클래스에서 타입 매개변수 <> 선언 시 아래와 같은 관례 존재

 

2. 타입 매개변수 제한

 

1) 타입 매개변수를 Object 객체 타입으로 선언 하게 되면 아래와 같이 사용할 수 있는 메서드가 제한 된다.

public class AnimalHospitalV2<T> {

    private T animal; // 매개변수를 object 타입으로 선언 시

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

    public void checkup() {
        // T의 타입을 메서드를 정의하는 시점에는 알 수 없다. Object 기능만 사용한다. (toString(), hasCode() 등)
        animal.equals(null);
        animal.toString();
//        System.out.println("동물 이름 : " + animal.getName()); // getName() 메서드를 사용 할 수 없다. object 타입이기 때문
//        System.out.println("동물 크기 : " + animal.getSize()); // getSize() 메서드를 사용 할 수 없다. object 타입이기 때문
//        animal.sound();

    }

 

public class AnimalHospitalV3<T extends Animal> {

}

 

2) 타입 매개변수 제한 시

public class AnimalHospitalV3<T extends Animal> {
    
    private T animal; 
    
    public void set(T animal) {
        this.animal = animal;
    }

    public void checkup() {

        // <T> 가 Animal 를 상속 받았기 때문에 Animal 과 Animal 의 서브 클래스 기능들을 사용 할 수 있다.
        System.out.println("동물 이름 : " + animal.getName()); 
        System.out.println("동물 크기 : " + animal.getSize()); 
        animal.sound();

    }
  • 자바 컴파일러는 T 가 Animal 을 상속 받았기 때문에 T에 입력될 수 있는 값의 범위를 예측 할 수 있다.
  • 타입 매개변수 T 에는 타입 인자로 Animal, Dog, Cat 만 들어올 수 있다. 따라서 이를 모두 수용할 수 있는 Animal 을 T의 타입으로 가정해도 문제가 없어진다.
  • 따라서 getName() 과 같은 기능을 사용 가능해진다.

추가적으로 당연히 다른 타입이 들어 올 수 없다. (Animal 과 관련된 객체만 들어 올 수 있다.)

public static void main(String[] args) {

    AnimalHospitalV3<Dog> dogHospital = new AnimalHospitalV3<>();
    AnimalHospitalV3<Cat> catHospital = new AnimalHospitalV3<>();
    // AnimalHospitalV3<Integer> integer = new AnimalHospitalV3<Integer>(); 다른 타입이 들어 올 수 없다.

 

** 제네릭 메서드 & 메서드 타입 제한

1) 제네릭 메서드 선언

// 제네릭 타입 메서드 선언 <T>
public static <T> T genericMethod(T t) {
    System.out.println("Generic print : " + t);
    return t;
}
  • 제네릭 메서드를 선언하면 해당 메서드 호출 시 타입을 결정 할 수 있다.
  • 예를 들어, 메인 메서드에서 genericMethod() 호출을 할 때 아래 코드와 같이 타입을 Integer 또는 String 으로 결정이 가능 하다. (제약 조건 없음)
Integer result = GenericMethod.<Integer>genericMethod(i); // 메서드 호출 시점에, Integer 타입으로 변경해서 i 출력
Integer integerValue = GenericMethod.<Integer>numberMethod(10);
Double doubleValue = GenericMethod.<Double>numberMethod(20.0);

 

2) 메서드 타입 제한

  • 제네릭 메서드를 생성시점에 타입을 제한하여 사용할 수 있다.
  • 아래와 같이 제네릭 타입에 extends Number 를 하게 되면 외부에서 해당 메서드를 호출 할 때 Number 객체와 관련된 int, double 등밖에 사용 할 수 없다 (String 사용 불가)
// 제네릭 메서드 타입 제한 <T extend Number>
public static <T extends Number> T numberMethod(T t) {
    System.out.println("Bound print : " + t);
    return t;
}

 

※ 제네릭 메서드 활용

public static <T extends Animal> void checkup(T t) {

    // <T> 가 Animal 를 상속 받았기 때문에 Animal 과 Animal 의 서브 클래스 기능들을 사용 할 수 있다.
    System.out.println("동물 이름 : " + t.getName());
    System.out.println("동물 크기 : " + t.getSize());
    t.sound();

}

 

- 아래 코드에서 매개변수 타입을 T 를 Animal 로 정의한 클래스를 생성

- 별도의 pringAndReturn() 이라는 메서드를 Z 타입으로 생성하였고, animal.class 관련 기능과 z.class 관련 기능을

추가하였다.

import generic.animal.Animal;

public class ComplexBox <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("z.className : " + z.getClass().getName());
        return z;

    }
}

T 타입과 Z 타입을 각각 호출 하였을 때 어떻게 활용되는지 아래 코드를 보면 확인할 수 있다.

import generic.animal.Cat;
import generic.animal.Dog;

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

Step 1. 첫번째로 ComplexBox<> 의 타입을 Dog 제네릭 타입을 받는 hospital 객체를 생성 하였다. 그리고 hopital 객체의 메서드 set(dog) 를 호출 하였고, 현재 T(animal) 매개타입은 Dog 가 된다. 

 

Step 2. 두번쨰로 해당 hospital 을 Cat 으로 재정의하고 printAndReturn 메서드를 호출 함과 동시에 returnCat 이라는 인스턴스로 재정의 하였다. 

 

※ 출력 결과

- T 타입은 Step1 에서 Dog로 선언 하였기 때문에 클래스 이름이 Dog 으로 출력 되는 것을 확인 할 수 있다.

- Z 타입은 printAndReturn 메서드를 호출함과 동시에 매개타입은 Cat 으로 선언 하였기 때문에 Cat 으로 출력 된다.

그렇게 때문에 returnCat 이라는 인스턴스는 Cat 의 파라미터를 담을 수 밖에 없게 된다. (Dog 불가)

 

※ 정리 : 

1) 정적 메서드는 메서드만 적용할 수 있지만, 인스턴스 메서드는 제네릭 타입도 제네릭 메서드도 둘 다 적용할 수 있다. 

 

2) 제네릭 타입과 제네릭 메서드의 우선 순위를 살펴보면 T를 Dog 타입으로 정의하였지만 별도의 메서드를 Cat 타입으로 정의 하였을 때, 해당 메서드 인스턴스로 Dog 타입이 들어갈 수 없는 것을 알 수 있다. 결과 적으로 제네릭 메서드가 제네릭 타입보다 우선순위를 갖는 것을 확인 할 수 있다.

 

3) 제네릭 메소드와 타입 추론 : Java 7 이후부터는 제네릭 메소드를 사용할 때 타입 인수를 생략하고 컴파일러가 컨텍스트로부터 타입을 추론할 수 있다. 이는 코드를 간결하게 작성할 수 있게 해준다.

 

3. 와일드 카드 (제네릭 타입을 편리 하게 사용)

- 와일드 카드의 뜻은 컴퓨터 프로그래밍에서 *, ? 와 같이 하나 이상의 문자들을 상징하는 특수 문자를 뜻한다.

(여러 타입이 들어올 수 있다는 뜻)

 

public class Box<T> {

    private T value;

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

    public T get() {
        return value;
    }
}

 

// 와일드카드 메서드 생성 <?> : 어떤 것도 다 들어올 수 있다는 뜻 (Bob<Dog>, Box<Cat>, Box<Object>)
static void printWildcardV1(Box<?> box) {
    System.out.println("? = " + box.get());
}

 

아래 각각 제네릭타입과 와일드카드 메서드를 생성 하였다.

// 1. 제네릭타입 메서드 생성
static <T extends Animal> void printGenericV2(Box<T> box) {
    T t = box.get();
    System.out.println("이름 = " + t.getName());
}

// 2. 와일드카드 메서드 생성 <? extends Animal> 제한
static void printWildcardV2(Box<? extends Animal> box) {
    Animal animal = box.get();
    System.out.println("이름 = " + animal.getName());
}

 

- 와일드카드의 메서드를 생성하는 방법은 <?> 를 사용하여 생성하고, 호출 시 제네릭타입과 동일한 기능할 가지게 된다.

- 그러나, 와일드카드는 제네릭 타입이나, 제네릭 메서드를 선언하는 것이 아니다. 와일드카드는 이미 만들어진 제네릭 타입을 활용할 때 사용한다.

 

 

- 위의 내용과 같이 제네릭 타입 메서드와 와일드카드 메서드는 서로 비슷한 부분이 많지만 제네릭 타입을 반드시 사용해야 하는 경우가 있다. 

 

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

- 메서드 타입들을 특정 시점에 변경하려면 제네릭 타입 or 제네릭 메서드를 사용해야 한다.

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

 

※ 정리 : 제네릭 타입이나 제네릭 메서드가 꼭 필요한 상황이면 <T> 를 사용하고, 그렇지 않은 상황이면 와일드 카드 사용 권장.

 

추가 : 와일드카드 상한/하한 지정 가능 : <? super Animal>

public class WildcardMain2 {

    public static void main(String[] args) {

        Box<Object> objBox = new Box<Object>();
        Box<Animal> animalBox = new Box<>();
        Box<Dog> dogBox = new Box<Dog>();
        Box<Cat> catBox = new Box<Cat>();
        
        // Animal 포함 상위 타입 전달 가능
        writeBox(objBox);
        writeBox(animalBox);
        writeBox(dogBox); // 컴파일 오류 Dog < Animal
        writeBox(catBox); // 컴파일 오류 Cat < Animal
        
    }
    
    // <? 가 Animal 보다 같거나 위에 있어야 호출 가능>
    static void writeBox(Box<? super Animal> box) {
        box.set(new Dog("멍멍이", 100));
    }
}

 

4. 타입 이레이저(eraser)

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

 

* 이레이저 방식의 한계

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

 

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