Chapter 12 지네릭스, 열거형, 애너테이션 - 01. 지네릭스

1. 지네릭스(Generics)

1.1 지네릭란?

다양한 타입의 객체들을 메서드나 컬렉션 클래스에 컴파일 시의(compile-time type check)를 해주는 기능

객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.
타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고,
저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.

예를 들어 ArrayList와 같은 컬렉션 클래스는 다양한 종류의 객체를 담을 수 있긴
하지만 보통 한 종류의 객체를 담는 경우가 더 많다.

그런데도 꺼낼 때마다 타입 체크를 하고 형변환 하는 것은 불편할 수 밖에 없다.

게다가 원하지 않는 종류의 객체가 포함되는 것을 막을 방법이 없다는 것도 문제다.

이 문제들을 지네릭스를 해결해 준다.

지네릭스의 장점
1. 타입 안정성을 제공한다.
2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.

1.2 지네릭 클래스의 선언

지네릭 타입은 클래스와 메서드에 선언할 수 있는데,
먼저 클래스에 선언하는 지네릭 타입을 본다면 예를 들어서 클래스 Box가 아래 코드처럼 정의되어 있다고 가정한다.

class Box {
	Object item;
    
    void setItem(Object item) { this.item = item; }
    Object getItem() { return item; }
}

이 클래스를 지네릭 클래스로 변경하면 다음과 같이 클래스 옆에 < T >를 붙이면 된다.
그리고 'Object'를 모두 'T'로 바꿔준다.

class Box<T> {// 지네릭 타입 T를 선언
	T item;
    
    void setItem(T item) { this.item = item; }
    T getItem() { return item; }
}

Box< T >에서 '타입 변수'라고 하며, 'Type'의 첫 글자에서 따왔다.
타입 변수가 T가 아닌 다른 것을 사용해도 된다.
ArrayList< E >의 경우, 타입 변수 E는 'Element(요소)'의 첫 글자에서 따왔다.

타입 변수가 여러 개인 경우에는 Map<K,V>와 같이 콤마','를 구분자로 나열하면 된다.

K는 Key(키)를 의미하고, V는 Value(값)을 의미한다.

무조건 'T'를 사용하는 것보다 가능하면, 이처럼 상황에 맞게 의미있는 문자를 선택해서 사용하는 것이 좋다.

이들은 기호의 종류만 다를 뿐 '임의의 참조형 타입'을 의미한다는 것은 모두 같다.

기존에는 다양한 종류의 타입을 다루는 메서드의 매개변수나 리턴타입으로
Object타입의 참조변수를 많이 사용했고, 그로 인해서 형변환이 불가피했지만,
이젠 Object타입 대신 원하는 타입을 지정하기만 하면 된다.

이제 지네릭 클래스가 된 Box클래스의 객체를 생성할 때는 다음과 같이
참조변수와 생성자에 타입 T 대신에 사용될 실제 타입을 지정해줘야 한다.

Box<String> b = new Bow<String>(); // 타입 T 대신, 실제 타입을 지정
b.setItem(new Object()); // 에러. String이외의 타입은 지정불가
b.setItem("ABC"); // OK. String이므로 가능
String item = (String) b.getItem // 형변환이 필요없음

위 코드에서 타입 T대신에 String타입을 지정해줬으므로, 지네릭 클랙스 Box< T >는 다음과 같이 정의된 것과 같다.

class Box{ // 지네릭 타입을 String으로 지정
	String item;
    
    void setItem(String item) { this.item = item; }
    String getItem() { return item; }
}

지네릭이 도입되기 이전의 코드와 호환을 위해서
지네릭 클래스인데도 예전의 방식으로 객체를 생성하는 것이 허용된다.
다만 지네릭 타입을 지정하지 않아서 안전하지 않다는 경고가 발생한다.

Box b = new Box(); // Ok. T는 Object로 간주된다.
b.setItem("ABC"); // 경고. unchecked or unsafe operation
b.setItem(new Obejct()); // 경고. unchecked or unsafe operation

아래와 같이 타입 변수 T에 Object타입을 지정하면,
타입을 지정하지 않은 것이 아니라 알고 적은 것이므로 경고는 발생하지 않는다.

Box<Object> b = new

지네릭스가 도입되기 이전의 코드와 호환성을 유지하기 위해서
지네릭스를 사용하지 않은 코드를 허용하는 것일 뿐,
앞으로 지네릭 클래스를 사용할 때는 반드시 타입을 지정해서 지네릭스와 관련된 경고가 나오지 않도록 해야 한다.

지네릭스의 용어

class Box<T> {}

Box : 원시타입(raw type)
T : 타입 변수 또는 타입 매개변수.(T는 타입 문자)
Box< T > : 지네릭 클래스

타입문자 T는 Bot< T >의 타입 변수 또는 매개변수라고 하는데 메서드의 매개변수와 유사한 면이 있기 때문이다.

그래서 아래와 같이 타입 매개변수에 타입을 지정하는 것을
'지네릭 타입 호출'이라고 하고,
지정된 타입 'String'을 '매개변수화된 타입'이라고 한다.
앞으로 매개변수화된 타입을 간단하게 '대입된 타입'이라고 사용한다.

Box<String> b = new Box<String>();

Box< String>과 Box< Integer>는
지네릭 클래스 Box< T>에 서로 다른 타입을 대입해서 호출했을 뿐
이 둘이 별개의 클래스는 아니다.

이것은 마치 매개변수의 값이 다른 메서드 호출,
즉 add(3,5)와 add(2,4)가 서로 다른 메서드를 호출하는 것이 아닌 것과 같다.

컴파일 후 Box< String>과 Box< Integer>는 이들의 '원시 타입'인 Box로 바뀐다.
즉, 지네릭 타입이 제거된다.

지네릭스의 제한
지네릭스 클래스 Box의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절하다.
지네릭스는 이처럼 인스턴스별로 다르게 동장하도록 하려고 만든 기능이므로

Box<Apple> appleBox = new Box<Apple>(); // Ok. Apple 객체만 저장가능
Box<Apple> grape Box = new Box<Grape>(); // Ok. Grape 객체만 저장가능

그러나 모든 객체에 대해 동일하게 동작해야하는 static멤버에 타입 변수 T를 사용할 수 없다.
T는 인스턴스변수로 간주되기 때문이다.
static멤버는 인스턴스 변수를 참조할 수 없다는 것을 기억하자.

class Box<T> {
	static T item; // 에러
    static int compare(T t1, t2) {...} // 에러

static 멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다.
이는 'Box< Appler>.item'과 'Box< Grape>.item'이 다른 것이면 안 된다는 뜻이다.
그리고 지네릭 타입의 배열을 생성하는 것도 허용하지 않는다.
지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, 'new T[10]'과 같이 배열을 생성하는 것은 안 된다는 것이다.

class Box<T> {
	T[] itemArr; // Ok. T타입의 배열을 위한 참조변수
    	...
    T[] toArray() {
    	T[] tmpArr = new T[itemArr.length]; // 에러. 지네릭 배열 생성불가
        ...
        return tmpArr;
    }
    	...
}

지네릭 배열을 생성할 수 없는 것은 new연산자 때문인데,
이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다.

그런데 위 코드에 정의된 Box< T>클래스를 컴파일하는 시점에서는 T가 어떤 타입이 될지 전혀 알 수 없다.
instanceof연산자도 new연산자와 같은 이유로 T를 피연산자로 사용할 수 없다.

꼭 지네릭 배열을 생성해야할 필요가 있을 때는, new연산자대신
'Reflection API'의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나,
Object배열을 생성해서 복사한 다음에 'T[]'로 형변환하는 방법 등을 사용한다.

1.3 지네릭 클래스의 객체 생성과 사용

지네릭 클래스 Box< T>가 아래와 같이 정의되어 있다고 가정한다.
이 Box< T>의 객체에는 한 가지 종류, 즉 T타입의 객체만 저장할 수 있다.
이전과 달리 ArrayList를 이용해서 여러 객체를 저장할 수 있도록 했다.

class Box<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(); }
}

Box< T>의 객체를 생성할 때는 다음과 같이 한다.
참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 한다.
일치하지 않으면 에러가 발생한다.

Box<Apple> appleBox = new Box<Apple>(); // OK
Box<Apple> appleBox = new Box<Grape>(); // 에러

두 타입이 상속관계에 있어도 마찬가지다. Apple이 Fruit의 자손이라고 가정하자.

Box<Fruit> appleBox = new Box<Apple>(); // 에러. 대입된 타입이 다르다.

단, 두 지네릭 클래스의 타입이 상속관계에 있고, 대입된 타입이 같은 것은 괜찮다.

FruitBox는 Box의 자손이라고 가정하자.

Box<Apple> appleBox = new FruitBox<Apple>(); // Ok. 다형성

JDK1.7부터는 추정이 가능한 경우 타입을 생략할 수 있게 됐다.
참조변수의 타입으로부터 Box가 Apple타입의 객체만 저장한다는 것을 알 수 있기 때문에,
생성자에 반복해서 타입을 지정해주지 않아도 되는 것이다.
따라서 코드의 두 문장은 동일한 것이다.

Box<apple> appleBox = new Box<Apple>();
Box<apple> appleBox = new Box<>();	// Ok. JDK1.7부터 생략 가능

생성된 Box< T>의 객체에 'void add(T item)'으로 객체를 추가할 때,
대입된 타입과 다른 타입의 객체는 추가할 수 없다.

Box<Apple> appleBox = new Box<Apple>();
appleBox.add(new Apple()); // Ok
appleBox.add(new Grape()); // 에러. Box<Apple>에는 Apple객체만 추가 가능

그러나 타입 T가 'Fruit'인 경우, 'void add(Fruit item)'가 되므로
Fruit의 자손들은 이 메서드의 매개변수가 될 수 있다.
Apple이 Fruit의 자손이라고 가정했다.

Box<Fruit> fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit()); // Ok
fruitBox.add(new Apple()); // Ok void add(Fruit item)

1.4 제한된 지네릭 클래스

타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만,
그래도 여전히 모든 종류의 타입을 저장할 수 있다는 것에는 변함이 없다.
그렇다면, 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법은 없는걸까

FruitBox<Toy> fruit Box = new FruitBox<Toy>();
fruitBox.add(new Toy()); // OK. 과일상자에 장난감을 담을 수 있다.

다음과 같이 지네릭 타입에 'extends'를 사용하면, 특정 타입의 자손만 대입할 수 있게 제한할 수 있다.

class FruitBox<T extends Fruit> {	// Fruit의 자손만 타입으로 지정가능
	ArrayList<T> list = new ArrayList<T>();
    ...
} 

여전히 한 종류의 타입만 담을 수 있지만, Fruit클래스의 자손들만 담을 수 있다는 제한이 더 추가된 것이다.

FruitBox<Apple> appleBox = nuew FruitBox<Apple>(); // Ok
FruitBox<Toy> toyBox = nuew FruitBox<Toy>(); // 에러. Toy는 Fruit의 자손이 아니다.

게다가 add()의 매개변수의 타입 T도 Fruit와 그 자손 타입이 될 수 있으므로,
아래와 같이 여러 과일을 담을 수 있는 상자가 가능하게 된다.

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
fruitBox.add(new Apple()); // Ok. Apple이 Fruit의 자손
fruitBox.add(new Grape()); // Ok. Grape가 Fruit의 자손

다형성에서 조상타입의 참조변수로 자손타입의 객체를 가리킬 수 있는 것처럼,
매개변수화된 타입의 자손 타입도 가능한 것이다.
타입 매개변수 T에 Object를 대입하면, 모든 종류의 객체를 저장할 수 있게 된다.
만일 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면,
이때도 'extend'를 사용한다.
'implements'를 사용하지 않는다는 점에 주의하자.

interface Eatable {}
class FruitBox<T extends Eatable> {...}

클래스 Fruit의 자손이면서 Eatable인터페이스도 구현해야한다면 아래와 같이 '&'기호로 연결한다.

class FruitBox<T extends Fruit & Eatable> {...}

이제 FruitBox에는 Fruit의 자손이면서 Eatable을 구현한 클래스만 타입 매개변수 T에 대입될 수 있다.

1.5 와일드 카드

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

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를 매개변수에 사용할 수 없으므로 아예 지네릭스를 적용하지 않던가,
위와 같이 타입 매개변수 대신, 특정 타입을 지정해줘야 한다.

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)); // dpfj. FruitBox<Apple>

이렇게 지네릭 타입을 'FruitBox< Fruit>'로 고정해 놓으면,
위 코드에서 알 수 있듯이 'Fruit< Apple>' 타입의 객체는 makeJuice()의 매개변수가 될 수 없으므로,
다음과 같이 여러 가지 타입의 매개변수를 갖는 makeJuice()를 만들 수 밖에 없다.

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

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

하지만 위처럼 오버로딩을 하면, 컴파일 에러가 발생한다.
지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문이다.
지네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해버린다.
그래서 위 두 메서드는 오버로딩이 아니라 '메서드 중복 정의'이다.

이럴 때 사용하기 위해 고안된 것이 바로 '와일드 카드'이다.
와일드 카드는 '?'로 표현하는데, 와일드 카드는 어떠한 타입도 될 수 있다.

'?'만으로는 Object타입과 다를 게 없으므로,
다음과 같이 'extends'와 'super'로 상한(upper bound)과 하한(lower bound)을 제한할 수 있다.

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

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

와일드 카드를 사용해서 makeJuice()의 매개변수 타입을 FruitBox에서
FruitBox <? extends Fruit>으로 바꾸면 다음과 같이 된다.

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

이제 이 메서드의 매개변수로 FruitBox뿐만 아니라,
FruitBox< Apple>와 FruitBox< Grape>도 가능하게 된다.
**_! 매개변수의 타입을 'FruitBox<? extends Object>'로 하면, 모든 종류의 FruitBox가 매개변수로 가능하다.

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> fruitBox = new FruitBox<Aruit>();
	...
System.out.println(juice.makeJuice(fruitBox)); // Ok. FruitBox<Fruit>
System.out.println(juice.makeJuice(appleBox)); // Ok. FruitBox<Apple>

매개변수의 타입을 FruitBox<? extends Object>로 하면,
모든 종류의 FruitBox가 이 메서드의 매개변수로 가능해진다.
대신, 전과 달리 box가 요소가 Fruit의 자손이라는 보장이 없으므로
아래의 for문에서 box에 저장된 요소를 Fruit타입의 참조변수로 못받는다.

static Juice makeJuice(FruitBox<? extends Object>box) {
	String tmp ="";

	for(Fruit f : box.getList()) tmp += f + ""; // 에러. Fruit이 아닐 수 있음
	return new Juice(tmp);
}

그러나 실제로 테스트 해보면 문제없이 컴파일되는데
그 이유는 바로 지네릭 클래스 FruitBox를 제한했기 때문이다.

class FruitBox<T extends Fruit> extends Box<T> {}

컴파일러는 위 문장으로부터 모든 FruitBox의 요소들이 Fruit의 자손이라는 것을
알고 있으므로 문제 삼지 않는 것이다.

1.6 지네릭 메서드

메서드의 선언부에 지네릭 타입이 선언된 선언된 메서드를 지네릭 메서드라고 한다.
앞에서 본 것처럼 Collections.sort()가 바로 지네릭 메서드이며,
지네릭 타입의 선언 위치는 반환 타입 바로 앞이다.

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

지네릭 클래스에 정의된 타입 매개변수와 지네릭 메서드에 정의된 타입 매개변수는 전혀 별개의 것이다.
같은 타입 문자 T를 사용해도 같은 것이 아니라는 것에 주의해야 한다.
!지네릭 메서드는 지네릭 클래스가 아닌 클래스에도 정의될 수 있다.

class FruitBox<T> {
		...
	static <T> void sort(List<T> list, comparator<? super T> c) {
		...
	}
}

위 코드에서 지네릭 클래스 FruitBox에 선언된 타입 매개변수 T와
지네릭 메서드 sort()에 선언된 타입 매개변수 T는 타입 문자만 같을 뿐 서로 다른 것이다.
그리고 sort()가 static메서드라는 것에 주목하자.
앞서 설명한 것처럼, static멤버에는 타입 매개변수를 사용할 수 없지만,
이처럼 메서드에 지네릭 타입을 선언하고 사용하는 것은 가능하다.

메서드에 선언된 지네릭 타입은 지역 변수를 선언한 것과 같다고 생각하면 이해하기 쉬운데,
이 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이므로 메서드가 static이건 아니건 상관이 없다.
! 같은 이유로 내부 클래스에 선언된 타입 문자가 외부 클래스의 타입 문자와 같아도 구별될 수 있다.

앞에서 나왔던 makeJuice()를 지네릭 메서드로 바꾸면 다음과 같다.

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

==>

static <T extends Fruit> Juice makeJuice(Fruit<T> box) {
	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(Juice.<Fruit>makeJuice(fruitBox));
System.out.println(Juice.<Apple>makeJuice(appleBox));

그러나 대부분의 경우 컴파일러가 타입을 추정할 수 있기 때문에 생략해도 된다.
위 코드에서도 fruitBox와 appleBox의 선언부를 통해 대입된 타입을 컴파일러가 추정할 수 있다.

System.out.println(Juice.<Fruit>makeJuice(fruitBox)); // 대입된 타입을 생략할 수 있다.
System.out.println(Juice.<Apple>makeJuice(appleBox));

주의할 점은 지네릭 메서드를 호출할 때,
대입된 타입을 생략할 수 없는 경우에는 참조변수나 클래스 이름을 생략할 수 있다는 점이다.

System.out.println(<Fruit>makeJuice(fruitBox));	// 에러. 클래스 이름 생략불가
System.out.println(this.<Fruit>makeJuice(fruitBox));	// Ok
System.out.println(Juicer.<Fruit>makeJuice(fruitBox));	// Ok

같은 클래스 내에 있는 멤버들끼리 참조변수나 클래스이름,
즉 'this'나 '클래스 이름'을 생략하고 메서드 이름만으로 호출이 가능하지만,
대입된 타입이 있을 때는 반드시 써줘야 한다.
이것은 단지 기술적인 이유에 의한 규칙이므로 그냥 지키기만 하면 된다.

지네릭 메서드는 매개변수의 타입이 복잡할 때도 유용하다.
만일 아래와 같은 코드가 있다면 타입을 별도로 선언함으로써 코드를 간략히 할 수 있다.

public static void printAll(ArrayList<? extends Product> list,
			ArrayList<? extends Product> list2) {
	for(Unit u : list) {
		System.outprintln(u);
	}
}

==>

public static <T extends Product> void printAll(ArrayList<T> list,
						ArrayList<T> list2) {
	for(Unit u : list) {
		System.outprintln(u);
	}
}

아래의 메서드는 Collections클래스의 sort()인데,
조금 전에 소개한 sort()와 달리 매개변수가 하나짜리다.

public static<T extends Comparable<? super T>> void sort(List<T> list)

매개변수로 지정한 List를 정렬한다는 것은 알겠지만,
메서드에 선언된 지네릭 타입이 조금 복잡한데 이럴 때는 와일드 카드를 걷어보면 된다.

public static<T extends Comparable<T>> void sort(List<T> list)

위 코드를 보면 알 수 있다시피 List의 요소가 Comparable인터페이스를 구현한 것이어야 한다.
앞에서 살펴본 것처럼 인터페이스라고 해서 'implements'라고 쓰지 않는다.

이제 다시 와일드 카드를 넣어보자

public static <T extends Comparable<? super T>> void sort(List<T> list)

1 : List
2 : <T extends Comparable<? super T>>

  1. 타입 T를 요소로 하는 List를 매개변수로 허용한다.
  2. 'T'는 Comparable을 구현한 클래스이어야 하며 ().
    'T'또는 그 조상의 타입을 비교하는 Comparable이어야 한다는 것 (Comparable<? super T>)을 의미한다.
    만일 T가 Student이고, Person의 자손이라면, <? super T>는 Student, Person, Object가 모두 가능하다.

1.7 지네릭 타입의 형변환

지네릭 타입과 원시 타입(raw type)간의 형변환이 가능할까

Box		box = null;
Box<Object> object = null;

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

위 코드를 보면 알 수 있다시피, 지네릭 타입과 넌지네릭(non-generic) 타입간의 형변환은 항상 가능하다.
다만 두 경우 모두 경고가 발생한다.
그러면, 대입된 타입이 다른 지네릭 타입 간에는 형변환이 가능할까

Box<Object> objBox = null;
Box<String> strBox = null;

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

형변환 할 수 없다. 대입된 타입이 Object일지라도 할 수 없다.
이 사실은 이미 배웠는데 아래의 문장이 안 된다는 얘기는
Box이 Box로 형변활될 수 없다는 사실을 간접적으로 알려주는 것이기 때문이다.

// Box<O:bject objBox = (Box<Object>)new Box<String>();
Box<Object> objBox = new Box<String>(); // 에러 발생 형변환 불가능

그렇다면 Box이 Box<? extends Object>로 형변환은 될까

Box<? extends Object> wBox = new Box<String>)();

가능하다.
그래서 전에 배운 makeJuice메서드의 매개변수에 다형서이 적용될 수 있었던 것이다.
매개변수로 FruitBox, FruitBox, FruitBox 등 가능

static Juice makeJuice(FruitBox<? extends Fruit> box) {...}

FruitBox<? extends Fruit> box = new FruitBox<Fruit>(); // OK
FruitBox<? extends Fruit> box = new FruitBox<Apple>(); // OK
FruitBox<? extends Fruit> box = new FruitBox<Grape>(); // OK

반대로의 형변환도 성립하지만, 확인되지 않는 형변환이라고 경고가 발생한다.
'FruitBox <? extends Fruit>'에 대입될 수 있는 타입이 여러개인데다,
FruitBox를 제외한 다른 타입은 FruitBox로 형변환될 수 없기 때문이다.

FruitBox<? extends Fruit>box = null; // Ok. 미확인 타입으로 형변환 경고
FruitBox<Apple> appleBox = (FruitBox<Apple>)box;

1.8 지네릭 타입의 제거

컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다.
그리고 지네릭 타입을 제거한다.
즉, 컴파일된 파일(*.class)에는 지네릭 타입에 대한 정보가 없는 것이다.
이렇게 하는 주된 이유는 지네릭이 도입되기 이전의 소스코드와의 호환서을 유지하기 위해서이다.

JDK1.5부터 지네릭스가 도입됐지만, 아직도 원시 타입을 사용해서 코드를 작성하는 것을 허용한다.
그러나 앞으로 가능하면 원시 타입을 사용하지 않도록 하면 된다.

언젠가는 분명히 새로운 기능을 위해 하위 호환성을 포기하게 될 때가 올 것이기 때문이다.

1. 지네릭 타읩의 경계(bound)를 제거한다.
지네릭 타입이 라면 T는 Fruit로 치환된다.
< T>인 경우는 T는 Object로 치환된다.
그리고 클래스 옆의 선언은 제거된다.

class Box<T extends Fruit) {
	void add(T t) {
		...
	}
}

==>

class Box {
	void add(Fruit t) {
		....
	}
}

2. 지네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다.
List get()은 Object타입을 반환하므로 형변환이 필요하다.

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

==>

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

좋은 웹페이지 즐겨찾기