[Java] 와일드 카드

이준영·2024년 8월 5일
1

🟫 Java

목록 보기
21/21

요약

1. 와일드카드
2. 2. 와일드 카드의 상한(extends), 하한(super) 제한
3. 와일드 카드 제한의 존재 이유
4. 와일드 카드에 들어갈 클래스 판단하기

1. 와일드카드

📝 와일드 카드와 제네릭 메소드는 기능적으로는 동일하지만, 코드가 길어질 수록 와일드 카드가 간결하기에 와일드 카드를 우선시 하자.


public static void peekBox(Box<?> box){
	System.out.println(box);
}

와일드 카드는 이렇게 ? 로 표현한다. 직관적으로 딱 보면 Box< ?> 의 ? 자리에 어떤것이든 전달 가능한 것처럼 보이고, 실제로도 Box< Intger> Box< String>등의 인스턴스를 인자로 전달 가능하다.

사실상 위 와일드 카드 메소드와 아래 제네릭 메소드와 기능적으로는 전혀 차이가 없다.


public static <T> void peekBox(Box<T> box){
	System.out.println(box);
}
💡 기능적으로 두 메소드는 완전히 동일하지만, 와일드 카드 기반의 메소드 정의가 더 간결하기 때문에 우선시 되어야 한다고 권고 하고 있다.


2. 와일드 카드의 상한(extends), 하한(super) 제한

📝 상한을 의미하는 extends는 꺼내는 것만 가능하고, 하한을 의미하는 super는 넣는 것만 가능하다고 기억하자.


🎯 상한 제한된 와일드 카드(Upper-Bounded Wildcards)


public static void peekBox(Box<? extends Number> box) {
	System.out.println(box);
}
❗️ Box< ? extends Number > 의 의미
? 자리에 Number클래스나 Number클래스 아래로 Number클래스를 상속하는 클래스만 올 수 있다. (? 자리에 Object 클래스는 전달 불가능)

제네릭을 공부할 때 위 그림과 같이 extends 키워드를 통해 T에 전달할 수 있는 클래스를 상한 제한 할 수 있었다. ex) Box< T extends Number >

💡 extends는 와일드 카드에서도 동일하게 적용할 수 있다.

🎯 하한 제한된 와일드 카드(Lower-Bounded Wildcards)

와일드 카드는 extends 이외에도 super(제네릭에는 존재 X)키워드도 존재한다.


public static void peekBox(Box<? super Integer> box) {
	System.out.println(box);
}

? super Integer 로 하한 제한을 걸게 된다면 peekBox()메소드에 인자로 Integer 이거나 Integer가 상속하는 Integer 상위 클래스들을 전달 가능하다.

즉, 위 메소드의 인자로 Box< Integer>, Box< Number> , Box< Object> 등을 전달 가능하다.

💡 Integer는 final로 선언되어있어 상속이 불가능하지만, 만약 Integer를 상속하는 클래스가 존재한다면 Integer 를 상속하는 하위 클래스들은 인자로 전달이 불가능 하다.

3. 와일드 카드 제한의 존재 이유

그래서 와일드카드에 extends, super로 상한, 하한 제한을 거는 이유가 뭘까? 왜 extends, super를 만들었을까?

코드를 작성하다보면 아래와 같이 물건을 꺼내는 메소드에 물건을 넣는 메소드를 호출 한다던지,
반대로 물건을 넣는 메소드에 물건을 꺼내는 메소드를 호출 한다던지 하는 실수가 발생할 수 있다.


public static void outBox(Box<Toy> box) {
        Toy t = box.get();
	box.set(new Toy()); // 박스에 물건 넣기

}

public static void inBox(Box<Toy> box, Toy n) {
        box.set(n);
	Toy t = box.get(); // 박스 물건 꺼내기
}
💡 결론적으로 이러한 실수를 컴파일 에러로 쉽게 잡아낼 수 있도록 상한, 하한 제한을 제공한다.

🎯 상한 제한의 목적

extends로 상한 제한을 하게되면 아래 코드중 box.set()에서 왜 컴파일 에러가 발생할까?

public static void outBox(Box<? extends Toy> box) {
	box.get(); // 꺼내는 것 OK
	box.set(new Toy()); // 넣는 것 ERROR
}

위와 같이 상한 제한을 둘 경우 Toy 이거나 Toy를 상속하는 클래스만 ? 자리에 올 수 있다.

예시를 들자면 아래와 같이 Toy를 상속하는 Car, Robot 클래스가 존재할 때,


class Car extends Toy{}
class Robot extends Toy{}

outBox()메소드의 인자로 Box< Car>, Box< Robot>도 전달 가능하다.
이렇게 될 경우 컴파일러 입장에서는 outBot()메소드에 전달되는 인자가 Box< Car>인지 Box< Robot>인지 Box< Toy> 인지 확인할 방법이 없어 메소드 내부 box.set(new Toy()) 에서 컴파일 에러를 발생시키게 된다.


🎯 하한 제한의 목적


public static void inBox(Box<? super Toy> box, Toy n) {
    box.set(n);
    Toy myToy = box.get(); // 컴파일 에러 발생
}

같은 이유에서 inBox() 메소드에서는 box.get()을 호출 할 경우 box내부에 담긴 것이 Toy인지 Toy의 상위 부모 클래스들 중 하나인지 컴파일러 입장에서는 확정 지을 수 없고, 그에 따라 myToy라는 참조 변수에 box.get()의 반환 값을 참조할 수 없어 에러를 발생시킨다.

예시를 들자면 아래와 같이 Toy가 상속받는 Plastic 이라는 클래스가 존재할 때,


class Plastic {}
class Toy extends Plastic {}

inBox() 메소드의 인자로 Box< Toy>, Box< Plastic> 를 전달 가능하다.
box.get()을 호출할 때 전달되는 인자에 따라 Box< Toy> 가 반환될 수도, Box< Plastic> 이 반환 될 수도 있게된다.
컴파일러 입장에선 반환값을 확정할 수 없기에, Toy myToy = box.get(); 에서 에러가 발생하게 된다. (Plastic이 반환될 경우 Toy 참조변수에 참조 불가능)

💡 정리!

Box< ? extends Toy > box 대상으로는 box.set()등을 통해 넣는 것이 불가능하다.
-> 꺼내기만 가능
Box< ? super Toy > box 대상으로는 box.get()을 통해 꺼내는 것이 불가능하다.
-> 넣기만 가능

상한, 하한 제한의 좋은예시

class BoxContentsMover {
    // from에 저장된 내용물을 to로 이동
    // super를 통해 to에서는 꺼내기 제한, extends를 통해 from에서는 넣기 제한
    public static void moveBox(Box<? super Toy> to, Box<? extends Toy> from) {
        to.set(from.get()); 
        from.set(to.get()); // 컴파일 에러 발생
    }
}


4. 와일드 카드에 들어갈 클래스 판단하기


class BoxHandler{
    public static void outBox(Box<? extends Toy> box){
        Toy t = box.get(); // 상자에서 꺼내기
        System.out.println(t);
    }
    public static void inBox(Box<? super Toy> box, Toy n){
        box.set(n);        // 상자에 넣기
    }
}

이렇게 해석 ❌

  • 위 메소드의 인자 Box< ? extends Toy > box 를 보고 어떤게 들어가야할지 해석할 때, ? 니까 아무거나 들어가도 되고, 그다음 extends가 뭘 의미하지? 이렇게 접근하면 해석이 어렵다.

이렇게 해석 ⭕️

  • 인자에 Box<? extends Toy> box를 보면 Box의 클래스로 Toy가 전달될 수 있겠구나 부터 생각을 해야한다.
    그 이후에 extends가 있으면 꺼내기만 가능하겠구나, super 가 있으면 넣기만 가능하겠구나 라고 해석해야 한다.

🎯 해당 메소드의 오버로딩 가능 여부


// 다음 두 메소드는 오버로딩 불가 X
public static void outBox(Box<? extends Toy> box){}
public static void outBox(Box<? extends Robot> box){}

// 다음 두 메소드는 두번째 매개변수가 다르기에 오버로딩 가능
public static void inBox(Box<? extends Toy> box, Toy t){}
public static void inBox(Box<? extends Robot> box, Robot r){}

언뜻 보기에 오버로딩이 되어야 할것 같지만, 컴파일러는 outBox()메소드의 경우 이렇게 오버로딩이 불가능하다고 경고 메세지를 띄운다.

'outBox(Box<? extends Toy>)' clashes with 'outBox(Box<? extends Robot>)'; both methods have same erasure

컴파일 과정에서 에러를 띄우는 이유는 자바의 Type Erase 때문인데, 자바가 발전해가면서 예전 레거시 코드들을 염두해 두고 개발되기 때문이다.

Type Erase

자바는 예전 레거시 코드들을 버리지 않고 코드가 돌아갈 수 있도록 개발하고 있기에 Box<? extends Toy>를 컴파일 하게되면 <? extends Toy> 부분을 지우고 예전 레거시 코드들도 이해할 수 있도록 다르게 표현하여 컴파일 한다고 한다.

따라서 Type Erase 가 일어나면
outBox(Box box)
outBox(Box box) 로 두 메소드가 동일한 매개변수를 가지는 것처럼 이해하여 컴파일 에러가 발생한다.

💡 이러한 문제를 해결하기 위해 아래와 같이 제네릭이 사용된다.

🎯 와일드 카드 선언을 갖는 메소드를 제네릭으로


public static  void outBox(Box<? extends Toy> box){}
public static  void outBox(Box<? extends Toy> Robot){}

제네릭을 사용하여 불가능했던 위 코드의 오버로딩을 아래와 같이 구현할 수 있다.


public static <T> void outBox(Box<? extends T> box){}

이렇게 와일드카드 기반 일반 메소드를 제네릭기반으로 바꾸었을떄 원하던 메소드 오버로딩을 코드로 구현할 수 있다.

💡 이제 위 제네릭 메소드의 인자를 보면 당황하지말고, Box에 제네릭 T 가 선언되어있으니 아무 클래스나 들어갈 수 있고, extends로 상한 제한 되어있으니 꺼내기만 가능하겠구나. 라고 판단하면 된다.





참고
profile
작은 걸음이라도 꾸준히

0개의 댓글