본문 바로가기
멀티캠퍼스 풀스택 과정/Java의 정석

자바의 정석9-2 와일드 카드, 지네릭 메서드

by 이쟝 2022. 1. 12.

와일드 카드<?>

- 하나의 참조 변수로 대입된 타입이 다른 객체를 참조 가능

 

ArrayList<? extends Product> list = new ArrayList<Tv>();      // OK
ArrayList<? extends Product> llist = new ArrayList<Audio>();  // OK
ArrayList<Product> list = new ArrayList<Tv>();                // 에러 대입된 타입 불일치

 

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

 

-> 지네릭 클래스와 달리 와일드 카드에는 '&'를 사용할 수 없다. 즉, <? extends T&E> 사용불가

-> <? extends Product> : Product와 그 자손들(Product, Tv, Audio)

 

ex) 매개변수에 과일박스를 대입하면, 주스를 만들어서 반환하는 Juicer라는 클래스가 있고, 이 클래스에는 과일을 주스로 만들어서 반환하는 makeJuice( )라는 static메서드가 정의되어 있다.

 

class Juicer {
	static juice makeJuice(FruitBox<? extends Fruit> box) {
		String tmp = "";
		for(Fruit f : box.getList()) tmp += f + " ";
		return new Juice(tmp);
	}
}
System.out.println(Juicer.makeJuice(new FruitBox<Fruit>()));  // ok. FruitBox<Fruit>
System.out.println(Juicer.makeJuice(new FruitBox<Apple>()));  // ok. FruitBox<Apple>

 

더보기
더보기
더보기

FruitBox<Fruit>뿐만 아니라 FruitBox<Apple>과 FruitBox<Grape>도 가능!(와일드카드로 인해서 Fruit과 그 자손들 매개변수로! )

 


지네릭 메서드

- 메서드의 선언부에 지네릭 타입이 선언된 메서드 (타입 변수는 메서드 내에서만 유효)

 

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

-> 메서드에 타입변수 선언!!

-> Collections.sort( )의 구조. -> 지네릭 메서드

-> 지네릭 타입의 선언 위치는 반환타입 바로 앞

 

 

-> 지네릭 클래스에 정의된 타입 매개변수<T>와 메서드 타입 매개변수 <T>는 별개!(타입 문자(T)만 같음)

 class FruitBox<T> {

    

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

    

     }

}

-> 원래 static 멤버에는 타입 매개변수를 사용할 수 없지만, 이처럼 메서드에 지네릭타입을 선언하고 사용하는것은 가능!

-> 즉 메서드에 선언된 지네릭 타입은 지역 변수를 선언한 것과 동일하다고 생각하면 됨

-> 이 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이기 때문.

 

앞서 나왔던 makeJuice( )를 지네릭 메서드로 바꾼다면? 

 

static juice makeJuice(FruitBox<? extends Fruit> box) {
		String tmp = "";
		for(Fruit f : box.getList()) tmp += f + " ";
		return new Juice(tmp);
}

static <T extends Fruit> Juice makeJuice(FruitBox<T> box) { // FruitBox<Fruit>, Fruit<Apple> 가능
		String tmp = "";
		for(Fruit f : box.getList()) tmp += f + " ";
		return new Juice(tmp);
}

 

 

- 위의 메서드를 호출할 때 마다 타입을 대입해야 함(대부분 생략 가능)

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>( );      // 선언
FruitBox<Apple> appleBox = new FruitBox<Apple>( ):
   …
System.out.println(Juicer.<Fruit>makeJuice(fruitBox));     // makeJuice( ) 호출
System.out.println(Juicer.<Apple>makeJuice(appleBox));
// System.out.println(Juicer.makeJuice(appleBox)); 위 코드와 동일함

 

- 메서드를 호출할 때 타입을 생략하지 않을 때는 클래스 이름 생략 불가

 

 

<T extends Fruit>: 제너릭 메서드 <-> <? extends Fruit>: 와일드 카드

 

제너릭 메서드 와일드 카드
Static < T extends Fruit > Juice makeJuice(FruitBox<T> box) { static Juice makeJuice(FruitBox<? extends Fruit> box) {
지네릭 메서드의 목적: 메서드를 호출할 때마다 다른 지네릭 타입을 대입할 수 있게 한 것 와일드카드의 목적: 와일드 카드는 하나의 참조변수로 서로 다른 타입이 대입된 여러 지네릭 객체를 다루기 위한 것
주로 와일드 카드가 안될 때 제네릭 메서드 사용!

 


지네릭 타입의 형변환

- 지네릭 타입과 원시 타입 간의 형변환은 바람직 하지 않다.(경고 발생)

 

Box            Box = null;  -> 원시 타입의 참조변수
Box<Object> objBox = null;  -> 지네릭 타입의 참조변수

Box box = (Box)objBox;   // Ok. 지네릭 타입 -> 원시 타입. 경고발생!!! 
// Box<String> -> Box 

objBox = (Box<Object>)box;  // Ok. 원시 타입 -> 지네릭 타입. 경고발생!!!
// Box -> Box<String>

-> 타입을 섞어 쓰는 것은 좋지 않음

 

Box<String> strBox = null;   // String 지네릭 타입 
Box<Object> objBox = null;   // Object 지네릭 타입

objBox = (Box<Object>)strBox;   // 에러. Box<String> -> Box<Object>
strBox = (Box<string>)obj.Box;    // 에러. Box<Object> -> Box<String>

-> 지네릭 타입이 다르면 형변환 불가능

 

- 와일드 카드가 사용된 지네릭 타입으로는 형변환가능

 

Box<Object>         objBox = (Box<object>)new Box<String>();  // 에러. 형변환 불가능

Box<? extends Object> wBox = (Box<? extends Object>)new Box<String>();  // OK 
Box<? extends Object> wBox = new Box<String>();  // 위 문장과 동일
- (Box<? extends Object>) 생략가능
- Box<String> -> Box<? extends Object>로 가능

같은 방법으로 
FruitBox<? extends Fruit> box = new FruitBox<Fruit>( ); // OK
FruitBox<? extends Fruit> box = new FruitBox<Apple>( ); // OK 
FruitBox<? extends Fruit> box = (FruitBox<? extends Fruit>) new Fruitbox<Apple>( ); // 위 문장과 
-> FruitBox<Apple> -> FruitBox<? extends Fruit> 가능!(자손타입 -> 조상타입)

 

 

 


지네릭 타입의 제거

- 컴파일러는 지네릭 타입을 제거하고, 필요한 곳에 형변환을 넣는다.

- 지네릭이 도입되기 이전의 소스 코드와의 호환성을 유지하기 위해서(안정성 때문)

- 컴파일 후에 <T>는 적절한 타입으로 변환됨

 

(1) 지네릭 타입의 경계(bound)를 제거

 

 

-> TFruit의 자손이어야 한다(제한된 타입) -> 컴파일러가 알아서 제한된 타입으로 변경 만약 제한된 타입이 아니면 컴파일러는 <T> -> Object로 변환함!

 

(2) 지네릭 타입 제거 후에 타입이 불일치하면, 형변환을 추가

 

 

-> 원래코드는 return (Fruit)list.get(i),  지네릭타입이 추가되면서 (Fruit)이 생략되었다. 하지만 지네릭 타입이 제거된다면 get(int i)의 타입이 Fruit인 원시타입이 되어서 타입을 맞춰주기 위해 다시 (Fruit)을 추가해야 한다.

 

 

(3) 와일드 카드가 포함된 경우, 적절한 타입으로 형변환 추가

 

 

-> box 안에 FruitFruit자손이 있음. 그래서 box.getList( ))의 타입에는 Fruit아니면 Fruit 자손이 들어올 수 있어서 타입이 생략됨!

 

 

-> 와일드 카드가 사라지게 되면서 코드가 조금 더 복잡하게 됨