JAVA - 지네릭스(2)

DevSeoRex·2022년 12월 2일
0
post-thumbnail

와일드 카드

// 매개변수로 과일박스를 받아 주스를 반환하는 Juicer 클래스
class Juicer {
	static Juice makeJuice(FruitBox<Fruit> box) { // <Fruit>로 지정
    	Strint tmp = "";
        for(Fruit f : box.getList()) tmp += f + " ";
        return new Juice(tmp);
    }
}
  • Juicer 클래스는 지네릭 클래스가 아니고, static 메서드에는 타입 매개변수 T를 매개변수에 사용할 수 없다.
  • 아예 지네릭스를 적용하지 않거나, 타입 매개변수 대신 특정 타입을 지정해야 한다.
// 특정 타입을 지정함으로서 생기는 문제 예제
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();

...

System.out.println(Juicer.makeJuice(fruitBox));	// OK. FruitBox<Fruit>
System.out.println(Juicer.makeJuice(appleBox));	// 에러. FruitBox<Apple>
  • 지네릭 타입을 'FruitBox<Fruit>'로 고정하면 Fruit의 자손들도 매개변수가 될 수 없다.
// 지네릭 타입만 변경해서 같은 이름 메서드 정의 예제(메서드 중복정의)\
static Juice makeJuice(FruitBox<Fruit> box) { 
    	Strint tmp = "";
        for(Fruit f : box.getList()) tmp += f + " ";
        return new Juice(tmp);
    }
    
static Juice makeJuice(FruitBox<Apple> box) { 
    	Strint tmp = "";
        for(Fruit f : box.getList()) tmp += f + " ";
        return new Juice(tmp);
    }
  • 지네릭 타입을 제한해서 자손을 매개변수로 대입할 수 없기 때문에 위와 같이 같은 이름의 메서드를 계속 자손의 개수만큼 만들어야 한다.
  • 위와 같이 오버로딩하면, 컴파일 에러가 발생하게 된다.
  • 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않는다.
  • 지네릭 타입은 컴파일러가 컴파일 후 제거해버리기 때문에 위의 메서드는 오버로딩이 아닌 메서드 중복정의이다

💡 이런 불편함을 개선하고자 와일드 카드가 고안 되었다.

와일드 카드는 보통 지네릭 타입을 지정하는 곳에 '?'로 표현한다. 예시를 들면

// 와일드 카드 예시 코드
FruitBox<? extends Fruit>

위와 같은 방식으로 사용할 수 있다. <?>만 사용하게 되면 모든 타입이 전부 대입이 가능하기 때문에 Object와 다를 것이 없다. 따라서 상속관계를 이용한 상한과 하한의 제한을 둘 수 있다.

💡 지네릭 클래스와는 다르게 와일드 카드에는 '&'를 사용할 수 없다.

와일드 카드를 이용해서 makeJuice( )를 수정한다면 아래와 같이 변경된다.

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

위와 같이 와일드 카드를 사용하면 Fruit 뿐만 아니라 자손 객체인 Apple과 Grape도 사용이 가능하게 된다.

지네릭 메서드

  • 메서드의 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라고 한다.
  • 대표적으로 Collections.sort( )가 지네릭 메서드이다.
  • 지네릭 타입의 선언 위치는 반환 타입 바로 앞이다.
  • 지네릭 메서드는 지네릭 클래스가 아닌 클래스에도 정의될 수 있다.

💡 같은 타입 문자 T를 사용해도 지네릭 클래스에 정의된 타입 매개변수와
     지네릭 메서드에 정의된 타입 매개변수는 전혀 별개의 것임에 주의해야
     한다.

// 지네릭 메서드 예제
class FruitBox<T> {
		...
	static <T> void sort(List<T> list, Comparator<? super T> c) {
	// 클래스에 선언된 타입변수 T와 sort() 메서드에 매개변수 타입변수 T는 
	// 문자만 같을 뿐 서로 다른 것이다.
		...
	}
}

지네릭 메서드를 정의하고 호출하는 방법은 아래의 코드와 같다.

// 지네릭 메서드 정의
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {
	String tmp = "";
	for(Fruit f : box.getList()) tmp += f + " ";
	return new Juice(tmp);
}

// 지네릭 메서드 호출

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

// 지네릭 메서드 makeJuice 호출
System.out.println(Juicer.<Fruit>makeJuice(fruitBox));
System.out.println(Juicer.<Apple>makeJuice(appleBox));
  • 지네릭 메서드를 호출할 때는 타입 변수에 타입을 대입해야 한다.
  • 대부분의 경우 컴파일러가 타입을 추정할 수 있기 때문에 생략이 가능하다.

복잡하게 선언된 지네릭 메서드

아래의 지네릭 메서드는 Collections 클래스의 sort( ) 메서드이다.
와일드카드와 지네릭 타입이 복잡하게 사용되있어서 이해하기가 어렵다.
아래의 메서드를 순차적으로 뜯어서 분석해보면 다음과 같다.

복잡하게 선언된 지네릭 메서드 분석

  • 1번 타입 T를 요소로 하는 List를 매개변수로 허용한다.
  • 'T'는 'T'또는 그 조상의 타입을 비교하는 Comparable를 구현한 클래스이어야 한다.
  • Comparable안에 <? super T>라는 조건이 있으므로, ?는 T이거나, T의 조상, Object가 들어갈 수 있다

지네릭 타입의 형변환

  • 지네릭 타입과 원시 타입간의 형변환도 가능하다.
  • 형변환은 항상 가능하지만 경고가 발생한다.
// 지네릭 타입의 형변환 예제
Box				box 	= null;
Box<Object>		objBox 	= null;

box		= (Box)objBox;			// OK. 지네릭 타입 -> 원시 타입.
objBox	= (Box<Object>)box;		// OK. 원시 타입   -> 지네릭 타입.
  • 대입된 타입이 다른 지네릭 타입간에는 형변환이 불가능하다.
// 대입된 타입이 다른 지네릭 타입간 형변환 예제
Box<Object> objBox = null;
Box<String> strBox = null;

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

지네릭 타입의 제거

  • 컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고 필요한 곳에 형변환을 넣어준다.
  • 컴파일된 파일(*.class)에는 지네릭 타입에 대한 정보가 없다.

지네릭 타입의 제거 과정

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

  • 지네릭 타입이 <T extends Fruit>라면 T는 Fruit로 치환된다. <T>인 경우는 Object로 치환된다.
// 지네릭 타입 제거 전
class Box<T extends Fruit> {
	void add(T t) {
		...
	}
}

// 지네릭 타입 제거 후
class Box {
	void add(Fruit t) {
		...
	}
}

2. 지네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다.

// 지네릭 타입 제거 전
T get(int i) {
	return list.get(i);
}

// 지네릭 타입 제거 후
Fruit get(int i) {
	return (Fruit)list.get(i);
}

2-1 와일드 카드가 포함되어 있는 경우의 형변환 추가

// 지네릭 타입 제거 전
static Juice makeJuice(FruitBox<? extends Fruit> box) {
	String tmp = "";
	for(Fruit f : box.getList()) tmp += f + " ";
	return new Juice(tmp);
}

// 지네릭 타입 제거 후
static Juice makeJuice(FruitBox<? extends Fruit> box) {
	String tmp = "";
	Iterator it = box.getList().iterator();
	while(it.hasNext()) {
		tmp += (Fruit)it.next() + " ";
	}
	return new Juice(tmp);
}

출처 : 자바의 정석 3rd Edition(남궁성 저, 도우출판)

0개의 댓글