자바/지네릭스

와일드 카드

백_곰 2022. 5. 12. 11:56

1. 와일드 카드란?

- Juicer라는 클래스가 존재하고 static 메서드로 makeJuice()가 있다고 가정해보자.

class Juicer{
    static Juice makeJuice(FruitBox<Fruit> box) {  // <Fruit>으로 지정
         String tmp = "";
         for(Fruit f : box.getList()) tmp += f + " ";
         return new Juice(tmp);
    }
}

 

( Juice 클래스는 지네릭 클래스가 아닌데다, 지네릭 클래스라고 해도 static 메서드에는 타입 매개변수 T를 매개변수에

사용할 수 없으므로 아예 지네릭스를 적용하지 않던가, 위와 같이 같이 <Fruit>로 지정해줘야 한다. )

 

 

 

( 그래서 아래와 같이 fruitBox는 컴파일이 잘 되지만, appleBox에 대해서는 컴파일 에러가 발생한다. )

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();

Juicer.makeJuice(fruitBox);
// 컴파일 정상

Juicer.makeJuice(appleBox);
// 컴파일 에러

 

 

 

( 그렇게 되면, 우리는 또 아래의 코드처럼 static 메서드를 생성해야 한다. )

class Juicer{
    static Juice makeJuice(FruitBox<Fruit> box) {  // <Fruit>으로 지정
         String tmp = "";
         for(Fruit f : box.getList()) tmp += f + " ";
         return new Juice(tmp);
    }
    
    static Juice makeJuice(FruitBox<Apple> box) {  // <Fruit>으로 지정
         String tmp = "";
         for(Fruit f : box.getList()) tmp += f + " ";
         return new Juice(tmp);
    }
}

 

( 그러나, 위와같이 오버로딩하면, 컴파일 에러가 발생한다. )

( 지네릭 타입이 다른 것만으로 오버로딩이 성립하지 않기 때문이다. )

( 그 이유는 지네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거하므로, 메서드 중복 정의로 간주되기 때문이다. )

 

( 이럴 때 사용하기 위해 고안된 것이 바로 '와일드 카드' 이다. )

( 와일드 카드는 기호 "?"로 표현하는데, 어떠한 타입도 될 수 있다. )

( "?" 만으로는 Object 타입과 다를 게 없으므로, 'extends''super'로 상한과 하한을 제한할 수 있다. )

 

"?" 표현방식 설명
<? extends T>  와일드 카드의 상한 제한. T와 그 자손들만 가능.
<? super T>  와일드 카드의 하한 제한. T와 그 조상들만 가능.
<?>  제한 없음. 모든 타입이 가능. <? extends Object> 와 동일함.

 

 

 

( 그러므로, 와일드 카드를 사용하면 아래의 코드가 나오게 된다. )

class Juicer{
    static Juice makeJuice(FruitBox<? extends Fruit> box) {  // <Fruit>으로 지정
         String tmp = "";
         for(Fruit f : box.getList()) tmp += f + " ";
         return new Juice(tmp);
    }
}

 

 

 

 

( 또한 아래의 코드처럼 <? extends Object>를 수행할 수 있다. )

class Juicer{
    static Juice makeJuice(FruitBox<? extends Object> box) {  // <Fruit>으로 지정
         String tmp = "";
         for(Fruit f : box.getList()) tmp += f + " ";
         return new Juice(tmp);
    }
}

 

( 그러나 이렇게 되면 for문 안에 있는 Fruit가 에러일 수 있다. )

( 그 이유는 FruitBox의 모든 객체가 사용할 수 있기 때문이다. )

 

( 그러나, 실제로 컴파일을 해보면 에러가 발생하지 않는다. )

( 왜냐하면 FruitBox에서 이미 클래스에 <T extends Fruit>을 썻기 때문이다. )

( 그러므로, 이미 Fruit의 자손만 가능하므로 컴파일 에러가 발생하지 않는 것이다. )

 

 

 

 

1-1. 와일드 카드를 이해하기 위한 예제(1)

: 위에서 했던 코드들을 합친 예제이다.

 

import java.util.ArrayList;

public class Exercise003 {
	public static void main(String[] args){
		Fruit3Box3<Fruit3> Fruit3Box3 = new Fruit3Box3<Fruit3>();
		Fruit3Box3<Apple3> Apple3Box3 = new Fruit3Box3<Apple3>();

		Fruit3Box3.add(new Apple3());
		Fruit3Box3.add(new Grape3());
		Apple3Box3.add(new Apple3());
		Apple3Box3.add(new Apple3());

		System.out.println(Juicer.makeJuice(Fruit3Box3));
		System.out.println(Juicer.makeJuice(Apple3Box3));
	}
}

class Fruit3		          { public String toString() { return "Fruit3";}}
class Apple3 extends Fruit3 { public String toString() { return "Apple3";}}
class Grape3 extends Fruit3 { public String toString() { return "Grape3";}}

class Juice {
	String name;

	Juice(String name)	     { this.name = name + "Juice"; }
	public String toString() { return name;		 }
}

class Juicer {
	static Juice makeJuice(Fruit3Box3<? extends Fruit3> Box3) {
		String tmp = "";
		
		for(Fruit3 f : Box3.getList()) 
			tmp += f + " ";
		return new Juice(tmp);
	}
}

class Fruit3Box3<T extends Fruit3> extends Box3<T> {}

class Box3<T> {
//class Fruit3Box3<T extends Fruit3> {
	ArrayList<T> list = new ArrayList<T>();
	void add(T item) { list.add(item);      }
	T get(int i)     { return list.get(i); }
	ArrayList<T> getList() { return list;  }
	int size()       { return list.size(); }
	public String toString() { return list.toString();}
}

 

 

 

 

1-2. 와일드 카드를 이해하기 위한 예제(2)

: 'super'로 와일드 카드의 하한을 사용하여 제한하는 예제이다.

 

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

public class Exercise004 {
	public static void main(String[] args){
		Fruit4Box4<Apple4> Apple4Box4 = new Fruit4Box4<Apple4>();
		Fruit4Box4<Grape4> Grape4Box4 = new Fruit4Box4<Grape4>();

		Apple4Box4.add(new Apple4("GreenApple4", 300));
		Apple4Box4.add(new Apple4("GreenApple4", 100));
		Apple4Box4.add(new Apple4("GreenApple4", 200));

		Grape4Box4.add(new Grape4("GreenGrape4", 400));
		Grape4Box4.add(new Grape4("GreenGrape4", 300));
		Grape4Box4.add(new Grape4("GreenGrape4", 200));

		Collections.sort(Apple4Box4.getList(), new Apple4Comp());
		Collections.sort(Grape4Box4.getList(), new Grape4Comp());
		System.out.println(Apple4Box4);
		System.out.println(Grape4Box4);
		System.out.println();
		Collections.sort(Apple4Box4.getList(), new Fruit4Comp());
		Collections.sort(Grape4Box4.getList(), new Fruit4Comp());
		System.out.println(Apple4Box4);
		System.out.println(Grape4Box4);
	}
}
class Fruit4	{
	String name;
	int weight;
	
	Fruit4(String name, int weight) {
		this.name   = name;
		this.weight = weight;
	}

	public String toString() { return name+"("+weight+")";}
	
}

class Apple4 extends Fruit4 {
	Apple4(String name, int weight) {
		super(name, weight);
	}
}

class Grape4 extends Fruit4 {
	Grape4(String name, int weight) {
		super(name, weight);
	}
}

class Apple4Comp implements Comparator<Apple4> {
	public int compare(Apple4 t1, Apple4 t2) {
		return t2.weight - t1.weight;
	}
}

class Grape4Comp implements Comparator<Grape4> {
	public int compare(Grape4 t1, Grape4 t2) {
		return t2.weight - t1.weight;
	}
}

class Fruit4Comp implements Comparator<Fruit4> {
	public int compare(Fruit4 t1, Fruit4 t2) {
		return t1.weight - t2.weight;
	}
}

class Fruit4Box4<T extends Fruit4> extends Box4<T> {}

class Box4<T> {
	ArrayList<T> list = new ArrayList<T>();

	void add(T item) {
		list.add(item);
	}

	T get(int i) {
		return list.get(i);
	}

	ArrayList<T> getList() { return list; }

	int size() {
		return list.size();
	}

	public String toString() {
		return list.toString();
	}
}

 

( Collections.sort()를 사용하여 정렬하였는데, 이 메서드의 선언부는 아래와 같다. )

static <T> void sort(List<T> list, Comparator<? super T> c)

 

( 위와 같이 정의된 <T> 메서드를 '지네릭 메서드' 라고 부른다. )

( 위의 코드에서 'super'로 정의한 이유는 코드 중복에 대한 문제를 방지하기 위해서이다. )

 

( 만약 우리가 sort()에 super라는 것이 없었더라면, 위 예제에서처럼 Fruit에 대한 자손들인 Apple과 Grape를

정렬하기 위해서는 각각의 AppleComp GrapeComp 만들어 정렬하였을 것이다. )

 

( 그러나, 이런식으로 Fruit의 자손이 생길때마다 Comp에 대한 코드를 계속 추가해야하는 문제가 발생하게 된다. )

 

( 그러나, Comparator<? super T>를 정의하였기 때문에, 손쉽게 FruitComp만 정의해주면 굳이 추가적으로

각각의 Comp를 만들어줄 필요가 없는 것이다. )

 

 

( 아래의 예시를 보고 이해하자. )

 

< Collections.sort()의 호출 >

sort(appleBox.getList(), new FruitComp());

 

< 실제 sort() 메서드 >

static <Apple> void sort(List<Apple> list, Comparator<? super Apple> c);
Comparator<? super T> 올 수 있는 Comparator<>
Comparator<? super Apple> Comparator<Apple>, Comparator<Fruit>,
Comparator<Object>
Comparaotr<? super Grape> Comparator<Grape>, Comparator<Fruit>,
Comparator<Object>