제네릭
목표
- 자바의 제네릭에 대해 학습하세요.
학습할 것
- 제네릭 사용법
- 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
- 제네릭 메소드 만들기
- Erasure
제네릭 사용법
제네릭이란
JDK1.5에서 처음 도입된 제네릭은 간단히 이야기해서 컴파일 시의 타입체크를 해주는 기능입니다. 특히 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에서 유용하게 쓰입니다.
제네릭의 장점
- 타입 안정성을 제공한다.
- 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.
- 타입 안정성을 높힌다는 것은?
- 의도하지 않은 타입의 객체가 저장되는 것을 막는다.
- 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다.
제네릭 클래스 선언
제네릭 타입은 클래스와 메서드에 선언할 수 있는데, 먼저 클래스에 선언하는 타입을 보겠습니다.
class Box {
Object item;
void setItem(object item) { this.item = item; }
Object getItem() { return item; }
}
위처럼 클래스 Box가 정의되어 있을 때, Box 클래스를 제네릭으로 바꾸면 아래와 같습니다.
(클래스 옆에 ‘
class Box<T> { // 제네릭 타입 T를 선언
T item;
void setItem(T item) { this.item = item; }
T getItem() { return item;}
}
- Box
에서 **T**를 '타입 변수(type variable)'이라고 하며, 'Type'의 첫 글자에서 따온 것입니다. - 타입 변수는 T가 아닌 다른 것을 사용해도 됩니다.
- ArrayList
의 경우 'Element(요소)'의 첫 글자를 따와 사용했습니다. - Map<K, V> 처럼 타입 변수가 여러 개인 경우에는 콤마’,’를 구분자로 나열했습니다. (K=Key, V=Vale)
- ArrayList
타입 변수는 기호의 종류만 다를 뿐 ‘임의의 참조형 타입’을 의미한다는 것은 모두 같습니다.
제네릭 클래스의 객체를 생성할 때는 참조변수와 생성자에 타입 T 대신에 사용될 실제 타입을 지정해주어야 합니다.
Box<String> b = new Box<String>(); // 타입 T 대신, 실제 타입을 지정
b.setItem(new Object()); // 에러 - String 이외의 타임은 지정불가
b.setItem("ABC"); // OK
String item = (String) b.getItem(); // 형변환 필요없음
String item = b.getItem(); // OK
위의 코드에서 타입 T대신에 String 타입을 지정해 주었으므로, 제네릭 클래스 Box
class Box {
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 Object()); // 경고, unchecked or unsafe operation
타입 변수 T에 Object를 지정해주면 경고는 발생하지 않습니다.
Box<Object> b = new Box<Object>();
b.setItem("ABC");
b.setItem(new Object());
제네릭 용어
class Box<T> {}
- **Box
** : 제네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽습니다. - T : 타입 변수 또는 타입 매개변수 (T는 타입 문자)
- Box : 원시 타입(raw type)
타입 매개변수라는 것은 **Box
**과 **Box **는 제네릭 클래스 **Box **에 서로 다른 타입을 대입하여 호출한 것일 뿐, 이 둘이 별개의 클래스를 의미하는 것은 아닙니다.
제네릭 제한사항
제네릭 클래스 Box의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절합니다.
Box<Apple> applieBox = new Box<Apple>();
Box<Grape> applieBox = new Box<Grape>();
그러나 모든 객체에 대해 동일하게 작동해야하는 static 맴버에 타입 변수 T를 사용할 수는 없습니다. 왜냐하면 T는 인스턴스 변수로 간주되기 때문입니다.
static 맴버는 인스턴스 변수를 참조할 수 없습니다.
class Box<T> { static T item; // error static int compare(T t1, T t2) {} // error }
제네릭 주요 개념 (바운디드 타입, 와일드 카드)
바운디드 타입 (제한된 제네릭 클래스)
타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만, 여전히 모든 종류의 타입을 지정할 수 있습니다.
FruitBox<Toy> fruitBox = new FruitBox<Toy>();
fruitBox.add(new Toy()); //fruitBox에 toy를 넣을 수 있다.
타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법으로 extends를 사용할 수 있습니다. 제네릭 타입에 ‘extend’를 넣어, 특정 타입의 자손들만 대입할 수 있게 됩니다.
class FruitBox<T extends Fruit> { // Fruit의 자손타입만 지정가능
ArrayList<T> list = new ArrayList<T>();
}
Fruit의 자손 타입을 지정가능하므로 여러 과일을 담을 수 있는 상자가 가능하게 됩니다.
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
fruitBox.add(new Apple()); // Fruit의 자손 Apple
fruitBox.add(new Grape()); // Fruit의 자손 Grape
다형성에서 조상 타입의 참조변수에서 자손 타입의 객체를 가리킬 수 있는 것처럼, 매개변수화된 타입의 자손 타입도 가능합니다. 타입 매개변수 T에 Object를 대입하면, 모든 종류의 객체를 저장할 수 있게 됩니다.
만약 클래스가 아닌 인터페이스 제약이 필요하다면 이때도 extends를 사용합니다.
interface Eatable {}
class FruitBox<T extends Eatable> { ... }
클래스 Fruit의 자손이면서 Eatable의 인터페이스도 구현해야 한다면 & 기호로 연결합니다.
class FruitBox<T extends Fruit & Eatable> { ... }
와일드 카드
와일드 카드는 기호 ’?’로 표현되는 것으로 어떤 타입이 와도 상관없이 대응하기 위해 고안되었습니다. 그러나 ’?’만으로는 Object타입과 다를게 없으므로, 다음과 같이 extends와 super로 상한((upper boud)와 하한(lower bound)을 제한할 수 있습니다.
- <? extends T>
- 와일드 카드의 상한 제한. T와 그 자손들만 가능
- <? super T>
- 와일드 카드의 하한 제한. T와 그 조상들만 가능
- <?>
- 제한 없음. 모든 타입이 가능 <? extends Object>와 동일
참고 : 제네릭 클래스와 달리 와일드 카드에는 ‘&’을 사용할 수 없습니다. <? extends T & E> 와 같이 할 수 없습니다.
와일드 카드를 사용하지 않은 static 메서드
class Juicer {
static Juice makeJuice(FruitBox<Fruit> box) {
String tmp = "";
for(Fruit f : box.getList())
tmp += f + " ";
return new Juice(tmp);
}
}
와일드 카드를 사용한 메서드
class Juicer {
static Juice makeJuice(FruitBox<? extends Fruit> 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>();
Juicer.makeJuice(fruitBox);
Juicer.makeFjuice(appleBox);
제네릭 메소드 만들기
메서드의 선언부에 제네릭 타입이 선언된 메서드를 제네릭 메서드라고 합니다. 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) ... }
여기서 한가지 주목해야할 것은 sort()가 static 메서드라는 점입니다.
- static 맴버에는 타입 매개변수를 사용할 수 없지만,
- 메서드에 제네릭 타입을 선언하고 사용하는 것은 가능합니다.
Erasure
컴파일러는 제네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어줍니다. 그러고 나서 제네릭 타입을 제거합니다.
- 즉 컴파일된 파일(*.class)에는 제네릭 타입에 대한 정보가 없습니다.
제네릭 타입의 제거과정
- 제네릭 타입의 경계(bound)를 제거합니다.
제네릭 타입이
<T extends Fruit>
라면 T는 Fruit로 치환됩니다.<T>
인 경우는 T는 Object로 치환됩니다. 그리고 클래스 옆의 선언은 제거됩니다.
- As-Is
class Box<T extends Fruit> { void add(T t) { ... } }
- To-Be
class Box { void add(Fruit t) { ... } }
- 제네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가합니다.
- As-Is
T get(int i) { return list.get(i); }
- To-Be
Fruit get(int i) { return (Fruit)list.get(i); }
- As-Is
와일드 카드가 포함되어 있는 경우에는 다음과 같이 적절한 타입으로 형변환이 추가됩니다.
- As-Is
static Juice makeJuice(FruitBox<? extends Fruit> box) { String tmp = ""; for(Fruit f : box.getList()) tmp += f + " "; return new Juice(tmp); }
- To-Be
static Juice makeJuice(FruitBox box) { String tmp = ""; Iterator it = box.getList().iterator(); while(it.hasNext()) { tmp += (Fruit)it.next() + " "; } return new Juice(tmp); }
출처
자바의 정석