☕ Java/이론

[Java] 제네릭3 (와일드 카드)

an2z 2022. 1. 17. 18:07

와일드 카드

💡 제네릭 메소드 vs 일반 메소드 

// 제네릭 메소드 정의
public static <T> void peekBox(Box<T> box) {
    System.out.println(box);
}

// 일반 메소드 정의
public static void peekBox(Box<Object> box) {
    System.out.println(box);
}
  • 제네릭 메소드의 Box<T>의 T에는 Box<String>의 인스턴스, Box<Integer>의 인스턴스를 모두 인자로 전달 가능하다.
  • 일반 메소드의 Box<Object>는 매개변수화 타입으로, Box<String>의 인스턴스, Box<Integer>의 인스턴스를 인자로 전달 가능할 것 같지만 불가능하다.
    • Box<Object>와 Box<String>, Box<Integer>는 상속 관계를 형성하지 않는다.
    • Object와 String과 Integer는 상속 관계이지만, Box<타입이름>은 매개변수화 타입으로 또 하나의 타입이기 때문에 상속관계가 이어지지 않는다.

 

💡 제네릭 메소드 vs 와일드 카드

// 제네릭 메소드 정의
public static <T> void peekBox(Box<T> box) {
    System.out.println(box);
}

// 와일드 기반 메소드 정의
public static void peekBox(Box<?> box) {
    System.out.println(box);
}
  • 와일드 카드는 키워드 ?를 사용해 메소드의 매개변수를 선언한다.
  • 타입 인자로 무엇이든 올 수 있다는 기능적 측면으로는 제네릭 메소드와 동일하다.
  • 제네릭 메소드보다 와일드카드 기반 메소드 정의가 더 간결하다는 장점이 있다.
    • 간결함 때문에 제네릭 메소드보다 와일드 기반 메소드 정의를 선호한다.
class Box<T> {
    protected T ob;    
    public void set(T o) { ob = o;}
    public T get() { return ob;}

    @Override
    public String toString() {
        return ob.toString();
    }
}

class Unboxer {
    // 상자 안의 내용물을 확인(출력)하는 기능의 메소드 정의
    /* 제네릭 메소드 */
    // public static <T> void peekBox(Box<T> box) {
    //   System.out.println(box);
    // }

    /* 와일드 카드 기반의 메소드 */
    public static void peekBox(Box<?> box) {
        System.out.println(box);
    }
}

class Wildcard {
    public static void main(String[] args) {
        Box<String> box = new Box<>();
        box.set("Simple String");
        Unboxer.peekBox(box);
    }
}

// 실행 결과
Simple String

 

 

와일드카드의 제한

상한 제한(upper bound) <? extends T> T와 T를 상속하는 자손들의 인스턴스만 전달되도록 제한
하한 제한(lower bound)  <? super T> T와 T가 상속하는 부모들의 인스턴스만 전달되도록 제한
  • 상한 제한
    • Box<? extends Number> box
    • Box<T>의 T로 전달되는 대상을 Number와 Number를 상속하는 자손 클래스로 제한
    • Box<Number>, Box<Integer>, Box<Long>, Box<Short> 등.. 
  • 하한 제한
    • Box<? super Integer> box
    • Box<T>의 T로 전달되는 대상을 Integer와 Integer가 상속하는 부모 클래스로 제한
    • Box<Integer>, Box<Number>, Box<Object>

 

💡 와일드 카드 제한의 목적

  • extends로 상한 제한을 두면 꺼내는 것(get)만 가능, 넣는 것(set) 불가능
  • super로 하한 제한을 두면 넣는 것(set)만 가능, 꺼내는 것(get) 불가능
  • 필요한만큼만 기능을 허용하여, 코드의 오류가 컴파일 과정에서 최대한 발견되도록 하기 위함이다.
class Box<T> {
	private T ob;	
	public void set(T o) { ob = o;}
	public T get() { return ob;}
}

class Toy {
	@Override
	public String toString() {
		return "I am a Toy";
	}
}

class BoxHandler {
	// 상자에서 물건을 꺼내는 메소드
	public static void outBox(Box<? extends Toy> box) {
		Toy t = box.get(); // 상자에서 꺼내기
		// box.set(new Toy()); // 넣으려 하면 컴파일 오류 발생
		System.out.println(t);
	}
	
	// 상자에 물건을 넣는 메소드
	public static void inBox(Box<? super Toy> box, Toy n) {
		box.set(n); // 상자에 넣기
		// Toy toy = box.get(); // 꺼내려 하면 컴파일 오류 발생
	}
}

public class Wildcard_BoundedUsage {
	public static void main(String[] args) {
		Box<Toy> box = new Box<>();
		BoxHandler.inBox(box, new Toy()); // 상자에 넣기
		BoxHandler.outBox(box); // 상자에서 꺼내기
	}
}

// 실행 결과
I am a Toy

 

상한제한이 get만 되는 이유 ?

class Car extends Toy {...} // 자동차 장난감
class Robot extends Toy {...} // 로봇 장난감
public static void outBox(Box<? extends Toy> box) {
    Toy t = box.get();
    box.set(new Toy()); // 컴파일 에러 발생
}

 

  • 위와 같이 Toy를 상속하는 하위 클래스가 있다면 Box<? extends Toy>로  Box<Toy>, Box<Car>, Box<Robot>가 전달될 수 있다.
  • 이때 만약 매개변수 타입으로 Box<Car>가 온다면
    • Box<Car>타입 인스턴스에 Toy 인스턴스를 저장하는 것은 불가능하다. (자식클래스에 부모 클래스를 담는 것은 불가능한 것과 같은 맥락)   

❓ 하한제한이 set만 되는 이유 ?

class Plastic {...}
class Toy extends Plastic {...}
public static void inBox(Box<? super Toy> box, Toy n) {
    box.set(n);
    Toy toy = box.get(); // 컴파일 에러발생
}
  • 위와 같이 Toy가 상속하는 상위 클래스가 있다면 Box<? super Toy>로 Box<Toy>, Box<Plastic>이 전달될 수 있다.
  • 이때 만약 매개변수 타입으로 Box<Plastic>이 온다면
    • Toy 타입 참조변수 toy가 Box<Plastic>타입 인스턴스를 참조하는 것은 불가능 하다. (자식클래스에 부모 클래스를 담는 것은 불가능한 것과 같은 맥락)

 

 

제한된 와일드 카드 선언을 갖는 제네릭 메소드

  • 자바는 제네릭 등장 이전에 정의된 클래스들과의 상호 호환성 유지를 위해 컴파일 시 제네릭과 와일드카드 관련 정보를 지우는 과정(Type Erasure)을 거친다.
  • 컴파일 과정에서 Type Erasure이 이뤄지기 때문에 다음과 같이 메소드 오버로딩이 인정되지 않는 경우가 생긴다.
class BoxHandler {
    // 다음 두 메소드는 오버로딩 인정 안됨.
    public static void outBox(Box<? extends Toy> box) {...}   // 컴파일러의 Type Erasure에 의해 (Box box)로 수정됨
    public static void outBox(Box<? extends Robot> box) {...} // 컴파일러의 Type Erasure에 의해 (Box box)로 수정됨
    
    // 다음 두 메소드는 두번째 매개변수로 인해 오버로딩 인정.
    public static void inBox(Box<? super Toy> box, Toy n) {...}     // 컴파일러의 Type Erasure에 의해 (Box box, Toy n)으로 수정됨
    public static void inBox(Box<? super Robot> box, Robot n) {...} // 컴파일러의 Type Erasure에 의해 (Box box, Robot n)으로 수정됨
}

 

이는 다음과 같이 제한된 와일드카드 선언을 갖는 제네릭 메소드 정의를 통해 해결할 수 있다.

  • public static <T> void outBox(Box<? extends T> box) {...}
class Box<T> {
	private T ob;	
	public void set(T o) { ob = o;}
	public T get() { return ob;}
}

class Toy {
	@Override
	public String toString() {
		return "I am a Toy";
	}
}

class Robot {
	@Override
	public String toString() {
		return "I am a Robot";
	}
}

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

public class Wildcard_Bounded_GenericMethod {
	public static void main(String[] args) {
		Box<Toy> tBox = new Box<>();
		BoxHandler.inBox(tBox, new Toy());
		BoxHandler.outBox(tBox);
        
        Box<Robot> rBox = new Box<>();
		BoxHandler.inBox(rBox, new Robot());
		BoxHandler.outBox(rBox);
	}
}

// 실행 결과
I am a Toy
I am a Robot