본문 바로가기

Java

Java- 제네릭(3) 와일드 카드

 제네릭 클래스간 상속이 불가한 경우 예시

  • Box<Object>와 Box<String>은 상속관계를 형성하지 않는다.
  • Box<Number>와 Box<Integer>은 상속관계를 형성하지 않는다.

 Object와 String이 상속관계를 가진다고 해도 Box<Object>와 Box<String>은 상속관계를 형성하지 않는 별개의 자료형이라는 것을 제네릭(2)에서 설명했다.

 대신 와일드 카드를 이용하면 가능하다.

 

와일드 카드

 와일드 카드 문법

class Box<T>{
    private T ob;

    public T get() {
        return ob;
    }

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

    @Override
    public String toString(){
        return ob.toString();
    }
}
class UnBoxer{
    public static <T> T openBox(Box<T> box){
        return box.get();
    } //제네릭 메소드
    
    public static void peekBox(Box<?> box){
        System.out.println(box);
    } //와일드 카드 사용
}

public class WildcardUnboxer {
    public static void main(String[] args) {
        Box<String> box = new Box<>();
        box.set("So simple string");
        UnBoxer.peekBox(box);
    }
}

 이 코드에서는 peekBox()에 와일드 카드가 사용되었다. 매개변수 Box<?> box에 <?>가 바로 그것이다.

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

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

위의 두 코드는 제네릭이냐 와일드 카드냐를 제외하면 기능적으로 완전히 동일하다. 하지만 와일드 카드를 사용한 경우 좀더 코드가 간결해진다.

 

 와일드 카드의 상한과 하한의 제한

  -와일드 카드의 상한의 제한

 아래 코드를 가지고 쭉 설명을 하겠다. 

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

peekBox()의 인자의 상한을 Number와 Number의 하위 클래스로 제한하고 싶다면 "extends"를 사용하면 된다.

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

이제 peekBox()의 인자는 Number와 하위클래스로 제한이 되었기 때문에 Box<Integer>, Box<Double>...같은 애들만 인자로 들어올 수 있다. 

 

  -와일드 카드의 하한의 제한

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

하한을 제한할 때는 "super"를 사용한다. 이번에는 하한Number로 제한하였다. 즉 Box<T>는 Box<Number>, Box<Object>로 제한이 된다.

 

  -와일드 카드 범위를 제한하는 이유

 그렇다면 도대체 왜 와일드 카드 상한, 하한을 두는 걸까? 궁극적인 이유는 프로그래머의 실수를 방지하고자, 잘못된 경우 컴파일시 오류를 발생시키기 위해서이다.

이게 뭔 개소리인지는 나중에 나온다.

 

  • 상한의 제한은 상한 제한이 된 객체에 값을 저장(set)하는 경우를 방지할 때 사용한다. 
  • 하한의 제한은 하한 제한이 된 객체의 값을 꺼내오는 것(get)을 방지할 때 사용한다.

사실 말로하면 어려우니까 코드로 보자.

class Box<T>{
    T ob;
    void set(T ob){
        this.ob = ob;
    }
    T get(){
        return ob;
    }
}
class Toy{
    @Override
    public String toString(){
        return "I am a toy";
    }
}

class BoxHandler{
    public static void inBox(Box<Toy> box, Toy toy){
        box.set(toy);
    }
    public static void outBox(Box<Toy>box){
        Toy toy = box.get();
        System.out.println(toy);
    }
}

public class WildcardUpperLimit {
    public static void main(String[] args) {
        Box<Toy> box = new Box<>();
        Toy toy = new Toy();
        
        BoxHandler.inBox(box,toy);
        BoxHandler.outBox(box);
    }
}

위와 같은 코드가 있다고 하자. 사실 위의 코드는 정상적인 코드이다. 보면 inBox는 Box<Toy>와 Toy 객체를 받아 Box에 Toy를 넣어주는 기능이고, outBox는 Box<Toy> 객체를 받아 내부의 Toy 객체를 꺼내어 출력해주는 기능을 한다.

 

그런데 만일 프로그래머가 실수를 하여 다음처럼 구현을 한다면 어떨까?

class BoxHandler{
    public static void inBox(Box<Toy> box, Toy toy){
        box.set(toy);
        //Toy t = box.get(); 프로그래머의 실수로 인해 잘못 들어간 코드(잘못되었지만 컴파일 잘됨)
    }
    public static void outBox(Box<Toy>box){
        Toy toy = box.get();
        System.out.println(toy);
        //box.set(new Toy()); 프로그래머의 실수로 인해 잘못 들어간 코드(잘못되었지만 컴파일 잘됨)
    }
}

지금 주석 친 부분은 기능적으로 구현이 잘못된 코드이다.

  inBox는 Toy객체를 받아서 Box에 넣어주는 역할만 하면된다. 그러나 지금 프로그래머가 실수로 inBox 내부에서 get()이 들어갔다. 문제는 이게 문법상의 오류가 아닌 구현을 잘못한거라서 컴파일시 아무 문제 없이 컴파일이 잘된다.

  outBox는 Box의 Toy 객체를 꺼내어 출력하는 역할만 하면 되는데, 이 또한 실수하여 set()을 하고있다. 그리고 이것도 컴파일에 문제가 없다. 

 

결국 둘다 잘못된 코드를 썼음에도 불구하고 컴파일에는 문제가 없어 오류를 발견하기 어렵다는 문제가 있다.

 

이러한 문제를 해결하기 위해서 상한과 하한을 제한하는 것이다.  먼저 상한의 제한을 보자.

 

  - 와일드 카드 상한(extends)을 제한하여 get( )만 사용하기

set( )과 get( )중 상한을 제한하면 set( )을 사용할 수 없다.

public static void outBox(Box<? extends Toy>box){
    Toy toy = box.get();
    System.out.println(toy);
    
    //box.set(new Toy()); --> 컴파일 에러 발생
}

 <? 자리에 들어올 수 있는 클래스 타입>

  1. Toy
  2. Toy의 하위 클래스

 

 매개변수 box의 제네릭 타입을 와일드 카드와 extends를 사용해 Toy로 상한을 제한하였다. 이 경우 set( ) 사용시 오류가 발생한다.

 

 Box<T> box의 T로 올 수 있는 타입 인자들은 "Toy와 Toy의 하위클래스들"이다. 예를 들어 Toy의 하위클래스로 Car라는 클래스가 있다고 하자.

class Car extends Toy{ ... }

즉 Box의 멤버변수 T ob의 타입이 Toy와 Toy의 하위클래스가 될 수 있다는 것인데, 만일 T가 Car인 경우 set( ) 구문은 문제가 된다.

/* T가 Car일 경우 */

box.set(new Toy()); ...(a)
Car ob = new Toy(); ...(b)  //ob는 Box의 멤버변수 T ob의 타입이 Car가 된 경우

 지금 T가 Car인 경우 a와 B는 동일한 의미를 지닌다.  다시말해 하위클래스 객체에 상위클래스 객체를 대입하는 것이다. 이는 문법적으로 문제가 된다.

 결국 outBox의 멤버변수 Box<? extends Toy> box의 ?가 항상 Toy라면 괜찮지만 하위 클래스가 들어올 가능성도 존재하기 때문에 컴파일러는 해당 가능성을 방지하고자 컴파일 오류를 발생시킨다.

 

 - 일반화

상한이 제한되는 경우애는 set()은 사용못한다. == get()만 허용한다.

 

  - 와일드 카드 하한(super)을 제한하여 set( )만 사용하기

public static void inBox(Box<? super Toy> box, Toy toy){
    box.set(toy);
    //Toy t = box.get(); --> 컴파일 오류 발생
}

 하한의 제한은 상한의 제한과 정반대이다. 상한이 get()만 가능했다면 하한은 set()만 가능하다. 

하한이 제한된 경우엔 Box<? super Toy> box의 ?에 Toy나 Toy의 상위 클래스만 올 수 있다.

 

<?자리에 들어올 수 있는 클래스 타입> 

  1. Toy
  2. Toy의 상위 클래스

 예를 들어 Toy의 상위 클래스로 plastic이 있다고 하자.

class Plastic{ ... }
class Toy extends Plastic { ... }

 이런 상황에서 inBox에 ?는 Toy 거나 Plastic이다. 그리고 만일 Plastic이라면 Toy t = box.get();은 문법적으로 문제가 된다.

/*만일 ?가 Toy의 상위 클래스 Plastic이라면*/

Toy t = box.get();   ...(a)
Toy t = Plastic ob;  ...(b)

 ?가 Plastic이라면 위의 (a)와 (b)는 문법적으로 동일하다. 결국 (b)를 보면 하위 클래스 객체에 상위 클래스 객체를 대입하는 꼴이 되어서 컴파일 에러가 발생한 것이다.

즉, 이 경우에는 get()을 하는데는 문제가 없지만 get()으로 꺼낸 객체를 참조변수에 담는 과정에서 컴파일 에러가 발생하는 것이다.

 

 결국 이 경우도 상한을 제한하는 경우와 마찬가지로 ?의 타입을 고정해놓을 수 없는 문제 때문에 잘못된 타입이 들어올 가능성을 배제할 수 없다. 따라서 컴파일러는 컴파일 오류를 발생시키는 것이다.

 

 - 일반화

하한이 제한되는 경우애는 get()은 사용못한다. == set()만 허용한다.

 


결론

- 상한 제한

  • extends 사용
  • set( ) 사용 불가
  • get( ) 가능

- 하한 제한

  • super 사용
  • get( ) 사용 불가
  • set( ) 가능

 

 

'Java' 카테고리의 다른 글

Java- 사용자 지정 예외와 예외처리  (0) 2023.12.26
Java- 제네릭(2)  (0) 2023.01.27
Java- 제네릭(1)  (0) 2023.01.07
Java- Arrays 클래스  (0) 2023.01.06
Java- Random 클래스, 난수 생성  (0) 2023.01.06