공부/Java

자바 제네릭(Generic) 2편

Stair 2024. 9. 24. 15:35
반응형

저번에 이어서 자바 제네릭을 계속 알아보자. 제네릭이 아직 뭔지 모르겠다면, 이전 포스팅을 참고하도록 하자.

https://surrealcode.tistory.com/74

 

자바 제네릭(Generic) 1편

제네릭을 배우기에 앞서 다음과 같은 코드를 살펴보자혹시라도 래퍼 클래스에 대해 잘 모른다면 이전 글을 참고하는게 좋다.https://surrealcode.tistory.com/59 자바 래퍼 클래스(wrapper class)기본형의

surrealcode.tistory.com

 

 

타입 매개변수 제한

이번에는 동물 병원을 만들어 본다고 가정한다. 요구사항은 다음과 같다

요구사항: 개 병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있어야 한다.

import generic.animal.Dog;

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){
        //왼쪽이 크면 animal 오른쪽이 크면 target 반환
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

1. 개 병원은 내부에 Dog 타입을 가진다.

2. checkup() : 개의 이름과 크기를 출력하고, 개의 sound()메서드를 호출한다.

3. bigger(): 다른 개와 크기를 비교한다. 둘 중 큰 개를 반환한다.

 

(super == 자식 객체를 만들때 부모까지 함께 만들어지고 생성자는 부모 자식 모두 호출해야한다. 자바의 규칙이다.)

(부모의 생성자가 기본 생성자가 아닐 경우 super(~)를통해 정의되어야한다.)

import generic.animal.Cat;

public class CatHospital {
    private Cat animal;

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

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

    public Cat bigger(Cat target){
        //왼쪽이 크면 animal 오른쪽이 크면 target 반환
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

Dog가 아닌 Cat으로 DogHospital과 일치한다.

public class AnimalHospitalMainV0 {

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

        dogHospital.set(dog);
        dogHospital.checkup();

        System.out.println();

        catHospital.set(cat);
        catHospital.checkup();

        //문제1: 개 병원에 고양이 전달.
//        dogHospital.set(cat);
        System.out.println();

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

다음 main을 돌려보면 다음과 같은 결과를 가진다.

 

동물 이름: 멍멍이
동물 크기: 100
멍멍

동물 이름: 냐옹이
동물 크기: 300
냐옹

biggerDog = Animal{name='멍멍쓰', size=200}

 

 

이번에 만든 코드는 처음에 제시한 다음 요구사항을 명확히 잘 지킨다.

요구사항 : 개 병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있어야 한다.

 

여기서는 개 병원과 고양이 병원을 각각 별도의 클래스로 만들었다.

각 클래스별로 타입이 명확하기 때문에 개 병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있다. 따라서 개 병원에 고양이를 전달하면 컴파일 오류가 발생한다.

그리고 개 병원에서 bigger()로 다른 개를 비교하는 경우 더 큰 개를 Dog 타입으로 반환한다.

 

문제 : 코드 재사용을 전혀 하지 못했다. -> 개 병원과 고양이 병원은 중복이 많이 보인다.

장점 : 타입 안정성O -> 타입 안전성은 명확하게 지켜졌다.

 

 

 

이 문제를 다형성을 사용해서 중복을 제거해보도록 해보자.

import generic.animal.Animal;

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){
        //왼쪽이 크면 animal 오른쪽이 크면 target 반환
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

 

Animal 타입을 받아서 처리한다.

checkup, bigger()에서 사용하는 animal.getName(), animal.getSize(), animal.Sound()메서드는 모두 Animal 타입이 제공하는 메서드이다. 따라서 아무 문제 없이 모두 호출할 수 있다.

 

public class AnimalHospitalMainV1 {

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

        dogHospital.set(dog);
        dogHospital.checkup();

        System.out.println();

        catHospital.set(cat);
        catHospital.checkup();

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

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

 

다음의 실행 결과를 보자.

동물 이름: 멍멍이
동물 크기: 100
멍멍

동물 이름: 냐옹이
동물 크기: 300
냐옹

biggerDog = Animal{name='멍멍쓰', size=200}

 

 

언뜻 괜찮아보이지만 다음과 같은 문제가 있다.

코드 재사용성은 높아졌으나 타입 안전성이 떨어졌다.

1. 개 병원에 고양이를 전달하는 문제가 발생한다.

2. Animal 타입을 반환하기 때문에 개를 비교할 시 다운캐스팅을 해야한다.

3. 실수로 고양이를 입력했는데, 개를 반환하는 상황이라면 캐스팅 예외가 발생한다.

 

 

 

그럼 이 문제를 제네릭을 도입해서 해결해보자.

public class AnimalHospitalV2<T> {

    private T animal;

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

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

    public T bigger(T target){
        //왼쪽이 크면 animal 오른쪽이 크면 target 반환
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

이 상태로 진행을 하려 했으나

T의 타입을 메서드를 정의하는 시점에 알 수 없기에 getName, getSize등 메서드 호출이 불가능하다. 여기 코드 어디에도 Animal에 대한 정보는 없다. T에는 타입 인자로 Integer가 들어올수도, Dog가 들어올수도 있다. 그래서 현재는 모든 객체의 부모인 Object의 기능만 사용할 수 있다.

(T가 Animal이라는 보장이 없기 때문이다. 타입 매개변수에는 Intger가 들어올수도, String이 들어올수도 있다.)

 

 

자바 컴파일러는 T에 어떤 타입이 들어올지 알 수 없기 때문에 T를 어떤 타입이든 받을 수 있는 모든 객체의 최종 부모인 Object 타입으로 가정한다. 따라서 Object가 제공하는 메서드만 호출할 수 있다.

원하는 기능을 사용하려면 Animal 타입이 제공하는 기능들이 필요한데, 이 기능을 모두 사용할 수 없다.

 

여기서 추가로 한가지 문제가 더 있다. 바로 동물 병원에 Integer, Object와 같은 동물과 전혀 관계 없는 타입을 인자로 전달할 수 있다는 점이다.

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

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이나 그 자식을 타입 인자로 제한하고 싶다.

이렇게 제한을 두는 것을 타입 매개변수 제한이라고 한다.

 

문제

1. 제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있다.

2. 타입 매개변수를 어떤 타입이든 수용할 수 있기에 Object의 기능만 사용할 수 있다.

-> 해결방안 : 제네릭에 Animal만 들어올 수 있도록 제한한다면 어떻게 될까?

 

 

 

이런 문제를 타입 매개변수를 제한하여 해결할 수 있다.

public class AnimalHospitalV3<T extends Animal> 

T는 최소한 Animal이나 Animal의 자식으로 제한하여 animal의 기능을 다 쓸 수 있도록 하였다.

즉 T의 상한이 Animal로 제한 되는 것이다.

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){
//        왼쪽이 크면 animal 오른쪽이 크면 target 반환
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

 

이렇게 하면 타입 인자로 들어올 수 있는 값이 Animal과 그 자식으로 제한된다.

이제 자바 컴파일러는 T에 입력될 수 있는 값의 범위를 예측할 수 있다.

타입 매개변수 T에는 타입 인자로 Animal, Dog, Cat만 들어올 수 있다. 따라서 이를 모두 수용할 수 있는 Animal을 T의 타입으로 가정해도 문제가 없다.

따라서 Animal이 제공하는 getName(), getSize()같은 기능을 사용할 수 있다.

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

public class AnimalHospitalMainV3 {

    public static void main(String[] args) {
        AnimalHospitalV3<Dog> dogHospital = new AnimalHospitalV3<>();
        AnimalHospitalV3<Cat> catHospital = new AnimalHospitalV3<>();
        Dog dog = new Dog("멍멍이", 100);
        Cat cat = new Cat("냐옹이", 300);


        dogHospital.set(dog);
        dogHospital.checkup();

        System.out.println();

        catHospital.set(cat);
        catHospital.checkup();

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

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

동물 이름: 멍멍이
동물 크기: 100
멍멍

동물 이름: 냐옹이
동물 크기: 300
냐옹

biggerDog = Animal{name='멍멍쓰', size=200}

 

타입 매개변수에 입력될 수 있는 상한을 지정해서 문제를 해결했다.

 

개 병원에 고양이 전달하는 문제 -> 해결

Animal 타입을 반환하기 때문에 다운캐스팅 해야했던 문제 -> 해결

실수로 고양이를 입력했는데 개를 반환할때 Exception 터지던 문제 -> 해결

제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있던 문제 -> 해결

어떤 타입이든 수용할 수 있는 Object로 가정하고, Object의 기능만 사용할 수 있었던 문제 ->해결

 

 

정리

제네릭에 타입 매개변수 상한을 사용해서 타입 안정성을 지키면서 상위 타입의 원하는 기능까지 사용할 수 있었다. 덕분에 코드 재사용과 타입 안정성이라는 두마리 토끼를 동시에 잡을 수 있었다

 

 

 

 

제네릭 메서드

제네릭은 특정 메서드에만 적용이 될 수도 있다.

참고로 앞서 살펴본 제네릭 타입과 지금부터 볼 제네릭 메서드는 둘 다 제네릭을 사용하지만 서로 다른 기능을 제공한다.

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("Object print: " + t);
        return t;
    }

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

    public static void main(String[] args) {
        Integer i = 10;
        Object object =(Integer) GenericMethod.objMethod(i);

        //타입 인자(Type Argument)명시적 전달.
        System.out.println("명시적 타입 인자 전달");
        Integer result = GenericMethod.<Integer>genericMethod(i);
        System.out.println("result = " + result);
        Integer integerValue = GenericMethod.<Integer>numberMethod(10);
        Double doubleValue = GenericMethod.<Double>numberMethod(20.0);
    }
}

 

 

제네릭 타입

정의 : GenericClass<T>

타입 인자 전달 -> 객체를 생성하는 시점 : ex)new GenericClass<String>

 

제네릭 메서드

정의 : <T> T genericMethod(T t)

타입 인자 전달 -> 메서드를 호출하는 시점 : ex)GenericMethod.<Integer>genericMethod(i)

 

1.제네릭 메서드는 클래스 전체가 아니라 특정 메서드 단위로 제네릭을 도입할 때 사용한다.

2. 메네릭 메서드를 정의할 때는 메서드의 반환 타입 왼쪽에 다이아몬드를 사용해서 <T>와 같이 타입 매개변수를 적어준다.

3. 제네릭 메서드는 메서드를 실제 호출하는 시점에 다이아몬드를 사용해서 <Integer>와 같이 타입을 정하고 호줄한다.

 

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

+제네릭 메서드는 인스턴스 메서드와 static 메서드에 모두 적용할 수 있다.

class Box<T>{ //제네릭 타입

    static <V> V staticMethod2(V v){} //static 메서드에 제네릭 메서드 도입

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

}

 

+제네릭 타입은 static 메서드에 타입 매개변수를 사용할 수 없다. 제네릭 타입은 객체를 생성하는 시점에 타입이 정해진다. 그런데 static 메서드는 인스턴스 단위가 아니라 클래스 단위로 작동하기 때문에 제네릭 타입과는 무관하다.

class Box<T>{

    T instanceMethod(T t){}

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

}

 

 

타입 매개변수 제한

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

제너럴 타입처럼 제너럴 메서드도 타입 매개변수를 제한할 수 있다 위 코드의 numberMethod를 보면 Number를 상속받고 있기 때문에 Number와 Number를 상속받는 타입 인자만 사용할 수 있게 된다.

 

 

제네릭 메서드 타입 추론

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

자바 컴파일러는 타입인자의 타입을 알 수 있기 때문에 이런 정보를 통해 자바 컴파일러는 타입 인자를 추론할 수 있다.

Integer result = GenericMethod.<Integer>genericMethod(i);
Integer integerValue = GenericMethod.<Integer>numberMethod(10);
Double doubleValue = GenericMethod.<Double>numberMethod(20.0);

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

 

 

 

이제 앞서 만들었던 AnimalHospital을 제네릭 메서드로 바꾸어 만들어보자.

import generic.animal.Animal;

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){
//        왼쪽이 크면 animal 오른쪽이 크면 target 반환
        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("냐옹이", 100);

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

        Dog bigDog = AnimalMethod.bigger(dog, new Dog("멍멍쓰", 200));
        System.out.println("bigDog = " + bigDog);


    }
}

동물 이름: 멍멍이
동물 크기: 100
멍멍
동물 이름: 냐옹이
동물 크기: 100
냐옹
bigDog = Animal{name='멍멍쓰', size=200}

 

기존 코드와 같이 잘 작동하는 것을 확인할 수 있다.

참고로 제네릭 메서드를 호출할 때 타입 추론을 사용하였다.

 

 

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

여기에 제네릭 타입과 제네릭 메서드의 타입 매개변수를 같은 이름으로 사용하면 어떻게 될까?

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

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

 

T의 타입은 Dog으로 Z의 타입은 Cat으로 바뀌어서 출력이 되는 것을 볼 수 있다.

 

Z였던 타입을 T로 바꾼다면 어떻게 될까?

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

}

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

 

결과는 이전과 동일하다.

제네릭 타입보다 제네릭 메서드가 우선순위를 가진다

따라서 printAndReturn은 제네릭 타입과는 무관하고 제네릭 메서드Cat이 된다.

 

+여기서 적용된 제네릭 메서드의 타입 매개변수 T는 상한이 없다. 따라서 Object로 취급 된다.

 

참고로 프로그래밍에서 이렇게 모호한 것은 좋지 않다.

애매모호할때는 타입의 이름을 꼭 바꿔서 하자.

 

 

 

와일드카드

제네릭 타입을 조금 더 편리하게 사용할 수 있는 wildcard를 자바에서 제공해준다.

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

쉽게 이야기해서 여러 타입이 들어올 수 있다는 뜻이다.

public class Box <T>{
    private T value;

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

    public T get() {
        return value;
    }
}

 

import generic.animal.Animal;

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("이름 = " + 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;
    }
}
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라는 타입이 있으면 그냥 가져다 쓸 때)

 

//이것은 제네릭 메서드이다.
//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>

이렇게 ?만 사용해서 제한 없이 모든 타입을 다 받을 수 있는 와일드카드를 비제한 와일드카드라 한다.

 

 

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

제네릭 메서드는 타입 매개변수가 존재한다. 그리고 특정 시점에 타입 매개변수에 타입 인자를 전달해서 타입을 결정해야 한다. 이런 과정은 매우 복잡하다.

반면 와일드카드는 일반적인 메서드에 사용할 수 있고, 단순히 매개변수로 제네릭 타입을 받을 수 있는 것 뿐이다. 제네릭 메서드처럼 타입을 결정하거나 복잡하게 작동하지 않는다. 단순히 일반 메서드에 제네릭 타입을 받을 수 있는 매개변수가 하나 있는 것 뿐이다.

 

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

 

제네릭 메서드와 마찬가지로 와일드 카드에도 상한 제한을 둘 수 있다.

Box<? extends Animal>

여기서는 ? extends 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());
}

여기서는 ? 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("이름 = " + 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 = WildcardEx.printAndReturnWildcard(dogBox);

타입이 명확하게 추론이 안되고 그냥 메서드를 가져다 쓰기 때문이다. return도 animal이 나오기 때문에 다운캐스팅을 해주어야 한다.

 

 

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

와일드카드는 이미 만들어진 제네릭 타입을 전달 받아서 활용할 때 사용한다. 따라서 메서드의 타입들을 타입 인자를 통해 변경할 수 없다. 쉽게 이야기 해 일반적인 메서드에 사용한다

 

정리 : 제네릭 타입이나 제네릭 메서드가 꼭 필요한 상황이면 <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<Dog>();
        Box<Cat> catBox = new Box<Cat>();

        //Animal 포함 상위 타입 전달 가능
        writeBox(objBox);
        writeBox(animalBox);

        //하한이 Animal
//        writeBox(dogBox);
//        writeBox(catBox);
    }

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

 

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

 

writeBox(objBox);  -> 허용
writeBox(animalBox);  -> 허용
        //하한이 Animal
writeBox(dogBox);  -> 허용
writeBox(catBox);  -> 허용

 

 

 

 

타입 이레이저(Type Eraser)

이레이저는 지우개 라는 뜻이다.

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

 

-> 자바의 제네릭은 단순하게 생각하면 개발자가 직접 캐스팅하는 코드를 컴파일러가 대신 추가해 주는 것이다. 자바의 제네릭 타입은 컴파일 시점에만 존재하고, 런타임 시에는 제네릭 정보가 지워지는데, 이것을 타입 이레이저라 한다.

 

 

타입 이레이저의 한계 : 컴파일 이후에는 제네릭 타입 정보가 존재하지 않기 때문에 다음과 같은 오류가 발생한다.

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

 

여기서 T는 런타임에 모두 Object가 되어버린다.

intstanceof는 항상 Object와 비교하게 된다. 이렇게 되면 항상 참이 반환되는 문제가 발생한다. 자바는 이런 문제 때문에 타입 매개변수에 instanceof를 허용하지 않는다.

new T는 항상 new Object가 되어버리기 때문에 개발자가 의도한 것과 달라진다. 따라서 new를 허용하지 않는다.

 

 

 

 

정리

실무에서 직접 제네릭을 사용해서 무언가를 설계하거나 만드는 일은 드물다. 그것보다는 대부분 이미 제네릭을 통해 만들어진 프레임워크나 라이브러리들을 사용하는 경우가 훨씬 많다. 그래서 만들어진 코드의 제네릭을 읽고 이해하는 정도면 충분하다.

지금까지 학습한 정도면 실무에 필요한 제네릭은 충분히 이해했다고 볼 수 있다.

 

제네릭은 이후에 설명하는 컬렉션 프레임워크에서 가장 많이 사용된다. 따라서 컬렉션 프레임워크를 통해서 제네릭이 어떻게 활용되는지 자연스럽게 학습할 수 있다.

반응형