제네릭스 Generics
컴파일시 타입 체크(Compile-Time Type Check)해주는 기능.
다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스는
어떤 자료를 담을 지 알 수 없기 때문에 최상위 객체인 Object 타입으로 저장, 관리한다.
이 경우 의도치 않은 자료형이 담겨 실행 시에 오류가 발생할 수 있다.
해당 오류는 컴파일 시에 알 수 없는 것으로, 제네릭 타입을 지정하면 컴파일 시 오류를 확인할 수 있게 된다.
인스턴스 별로 다르게 동작할 수 있도록 만들어졌다.
다룰 객체의 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다.
제네릭스를 모르면 Java API 문서를 제대로 볼 수 없을 만큼 중요하다.
- 장점
타입 안전성 제공
타입 체크와 형변환을 생략할 수 있어 코드가 간결해 진다.
선언
제네릭 타입은 클래스와 메서드에 선언할 수 있다.
- 일반 클래스
public class Test {
Object item;
void setItem(Object item) {
this.item = item;
}
Object getItem() {
return item;
}
}
- 제네릭 클래스
public class Test<T> {
T item;
void setItem(T item) {
this.item = item;
}
T getItem() {
return item;
}
}
Test<T>의 T를 타입 변수라고 한다.
용어
class Test<T>
class 제네릭 클래스
class 원시 타입<T>
class Test<타입 변수>
Test<T> | 제네릭 클래스. ‘T의 Test’ 또는 ‘T Test’라고 읽는다. |
T | Test<T>의 타입 변수 또는 타입 매개변수. (T는 타입 문자) |
Test | 원시 타입 (raw type) |
타입 변수 Type Variable
상황에 맞게 의미 있는 문자를 선택해서 사용한다.
기호의 종류만 다를 뿐 '임의의 참조형 타입'을 의미한다는 것은 모두 같다.
여러 개인 경우 콤마(,)를 구분자로 나열한다.
- 자주 쓰이는 타입 문자
T : Object
E : Element
K : Key
V : Valeu
제한
- static 멤버
제네릭스는 객체 별로 다르게 동작하기 위해 만들어졌다.
때문에 모든 객체에 대해 동일하게 동작해야 하는 static 멤버에 타입 변수를 사용할 수 없다.
타입 변수는 인스턴스 변수로 간주 된다.
- 제네릭 타입의 배열
제네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, 배열을 생성하는 것은 안 된다.
new 연산자는 컴파일 시점에 타입 변수가 뭔지 정확히 알아야 하는데
제네릭 클래스는 컴파일하는 시점에 타입 변수가 어떤 타입이 될 지 전혀 알 수 없기 때문이다.
public class Test<T> {
T[] arr;
T[] arr1 = new T[10]; // error
instanceof 연산자도 같은 이유로 타입 변수를 피연산자로 사용할 수 없다.
- 제네릭 배열을 생성해야 할 경우
'Reflection API'의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나,
Object 배열을 생성해서 복사한 후 'T[]'로 형변환하는 방법 등을 사용한다.
와일드 카드 Wildcards
기호 ?
타입 변수는 보통 단 하나의 타입만 지정하지만 와일드 카드를 이용하면 하나 이상의 타입을 지정할 수 있다.
어떠한 타입도 될 수 있다. (타입 변수의 다형성)
<? extends T> | 와일드 카드의 상한 제한(Upper Bound). T와 그 자손들만 가능 |
<? super T> | 와일드 카드의 하한 제한(Lower Bound). T와 그 조상들만 가능 |
<?> | 제한 없음. 모든 타입 가능. <? extends Object>와 동일 |
- 고안된 이유
static 메서드에 제네릭스를 적용할 경우, 타입 매개변수는 사용하지 못하므로 특정 타입을 지정해야 한다.
그렇게 되면 해당 메서드는 특정 타입의 객체만을 사용할 수 있게 되어
다른 타입의 객체를 매개변수로 오게 하려면 타입 변수만 다른 똑같은 메서드를 만들어야 한다.
이 때 제네릭 타입이 다른 것만으로는 오버로딩이 성립되지 않기 때문에 메서드 중복 정의가 된다.
와일드 카드는 이런 상황에 사용하기 위해 고안되었다.
static void method(Box<TypeA> b){} // Compile error
static void method(Box<TypeB> b){} // Compile error
static void method(Box<TypeC> b){} // Compile error
제네릭 타입의 형변환
객체 안에 담을 수 있는 자료형을 지정하기 때문에 해당 자료형에 대한 형변환이 필요 없다.
ArrayList<String> list = new ArrayList<>();
list.add("dico");
String str = list.get(0);
ArrayList<String> : ArrayList에 담을 수 있는 자료형은 String 뿐이다.
- 제네릭 타입과 넌제네릭(Non-Generic) 타입간의 형변환
가능하지만 경고가 발생한다.
Test test = null;
Test<Object> objTest = null;
test = (Test) objTest; // Generic Type → Primitive Type. Warning
objTest = (Test<Object>) test; // Primitive Type → Generic Type. Warning
- 대입된 타입이 다른 제네릭 타입 간에는 형변환이 불가능하다.
Test<String> strTest = null;
Test<Object> objTest = null;
strTest = (Test<String>) objTest; // error. Test<Object> → Test<String>
objTest = (Test<Object>) strTest; // error. Test<String> → Test<Object>
제네릭 타입의 제거
컴파일러는 제네릭 타입을 이용해서 소스 파일을 체크하고,
필요한 곳에 형변환을 넣어준 후 제네릭 타입을 제거한다.
∵ 제네릭이 도입되기 이전의 소스 코드와의 호환성을 유지하기 위해
∴ 컴파일된 파일(*.class)에는 제네릭 타입에 대한 정보가 없다. (제네릭 타입은 대부분 Non-Reifiable Types)
- 과정 1. 제네릭 타입의 경계 제거
// before
class Box<T extends Fruit> {
void add(T t){}
}
// after
class Box {
void add(Fruit t){}
}
- 과정 2. 제네릭 타입을 제거한 후에 타입이 일치하지 않으면 형변환을 추가한다.
// before
T get(int i){
return list.get(i);
}
// after
Fruit get(int i){
return (Fruit) list.get(i);
}
- 과정 3. 와일드 카드가 포함되어 있으면 적절한 타입으로 형변환한다.
// before
static Juice makeJuice(FruitBox<? extends Fruit> box){
String tmp = "";
for(Fruit f : box.getList()) {
tmp += f + " ";
}
return new Juice(tmp);
}
// after
static Juice makeJuice(FruitBox box){
String tmp = "";
Iterator it = box.getList().iterator();
while(it.hasNext()) {
tmp += (Fruit) it.next() + " ";
}
retrun new Juice(tmp);
}
- 컴파일 후에
제거되지 않는 타입 == Reifiable Types
제거되는 타입 == Non-Reifiable Types
Example
package blog;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class Main {
public static void main(String[] args) {
FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>();
FruitBox<Grape> grapeBox = new FruitBox<>();
fruitBox.add(new Fruit("fruit", 200)); // Upper Bounded Wildcards
fruitBox.add(new Apple("apple", 150)); // Upper Bounded Wildcards
fruitBox.add(new Grape("grape", 300)); // Upper Bounded Wildcards
appleBox.add(new Apple("apple", 100));
appleBox.add(new Apple("apple", 170));
appleBox.add(new Apple("apple", 200));
grapeBox.add(new Grape("grape", 300));
grapeBox.add(new Grape("grape", 400));
grapeBox.add(new Grape("grape", 200));
System.out.println(Juicer.makeJuice(fruitBox)); // fruit 200 apple 150 grape 300 Juice
System.out.println(Juicer.makeJuice(appleBox)); // apple 100 apple 170 apple 200 Juice
System.out.println(Juicer.makeJuice(grapeBox)); // grape 300 grape 400 grape 200 Juice
Collections.sort(fruitBox.getList(), new FruitComp()); // Lower Bounded Wildcards
Collections.sort(appleBox.getList(), new FruitComp()); // Lower Bounded Wildcards
Collections.sort(grapeBox.getList(), new FruitComp()); // Lower Bounded Wildcards
System.out.println(fruitBox); // [apple 150, fruit 200, grape 300]
System.out.println(appleBox); // [apple 100, apple 170, apple 200]
System.out.println(grapeBox); // [grape 200, grape 300, grape 400]
}
}
class Box<T> {
ArrayList<T> list = new ArrayList<>();
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();
}
}
class FruitBox<T extends Fruit> extends Box<T> {
}
class Fruit {
String name;
int weight;
Fruit(String name, int weight) {
this.name = name;
this.weight = weight;
}
public String toString() {
return name + " " + weight;
}
}
class Apple extends Fruit {
Apple(String name, int weight) {
super(name, weight);
}
}
class Grape extends Fruit {
Grape(String name, int weight) {
super(name, weight);
}
}
class Juice {
String name;
Juice(String name) {
this.name = name + "Juice";
}
public String toString() {
return name;
}
}
class Juicer {
/*
* static Juice makeJuice(FruitBox<? extends Object> box)로 할 경우 box의 요소가 Fruit의
* 자손이라는 보장이 없다. 하지만 여기서는 제네릭 클래스 FruitBox를 <T extends Fruit>으로 제한했기 때문에 문제 없다.
*/
static Juice makeJuice(FruitBox<? extends Fruit> box) { // FruitBox<Fruit/Apple/Grape>
String tmp = "";
for (Fruit f : box.getList()) {
tmp += f + " ";
}
return new Juice(tmp);
}
}
/*
* public static <T> void sort(List<T> list, Comparator<? super T> c)
* sort(List<Fruit>, list, Comparator<? super Fruit> c) :
* Comparator<Object/Fruit> sort(List<Apple>, list, Comparator<? super Apple> c)
* : Comparator<Object/Fruit/Apple> sort(List<Grape>, list, Comparator<? super
* Grape> c) : Comparator<Object/Fruit/Grape> → List를 정렬하기 위해 Comparator를 구현할 경우
* 동일한 조상(Fruit)으로 구현하여 Fruit의 자손이 생길 때마다 구현해야 하는 번거로움을 해소한다.
*/
class FruitComp implements Comparator<Fruit> {
@Override
public int compare(Fruit o1, Fruit o2) {
return o1.weight - o2.weight;
}
}
참고 서적: 자바의 정석 3판 2