타입 안정성을 높인다는 것 : 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다.
class Box {
private Object item;
public void setItem(Object item) { this.item = item; }
public Object getItem() { return item; }
}
Object
타입으로 설정했다. 그런데 이렇게 작성하면 물건을 담을 때엔 문제가 없지만 꺼낼 때에 좀 번거롭다. Box b = new Box();
b.setItem("new item");
String s = (String) b.getItem(); // 형변환 필요
String
타입의 아이템을 담고 바로 꺼내 쓰려 했는데, 방금 담은 아이템이 String
타입이라는 것은 코드를 쓴 나만 아는 것이지 컴파일러는 모른다. 그렇기 때문에 컴파일러 입장에서는 마지막 줄처럼 String
타입으로 객체에 저장된 아이템을 받을 수 있다고 보장할 수 없다. 그래서 형변환을 해 주어야만 꺼내쓸 수 있다. class Box<T> {
private T item;
public void setItem(T item) { this.item = item; }
public T getItem() { return item; }
}
public static void main(String[] args) {
Box<String> b = new Box();
b.setItem("new item");
String s = b.getItem();
}
Box
클래스를 제네릭 클래스로 바꿔보았다. 객체를 생성하는 단계에서부터 클래스에 담을 자료형을 지정해줄 수 있기 때문에 담겨 있는 아이템을 꺼낼 때 굳이 형변환을 하지 않아도 된다. 그리고 코드상으로도 어떤 자료형이 담겨 있는 것인지 명확하게 알 수 있다. 때문에 Object
클래스를 이용했던 코드보다 훨씬 가독성이 좋아졌다. Box<T>
: 제네릭 클래스. T의 Box
또는 T Box
라고 읽는다.T
: 타입 변수 또는 타입 매개변수(T는 타입문자)Box
: 원시 타입(row type)static
멤버에 타입 변수 T를 사용할 수 없다. T는 인스턴스 변수로 간주되기 때문이다. class Box<T> {
static T item; // Error!
static int compare(T t1, T t2) {} // Error!
}
static
키워드가 붙은 멤버는 메모리에 즉시 할당이 되어야 하는데 자료형이 뭔지 알아야 그에 맞게 할당을 할 것이 아닌가. class Box<T> {
T[] items; // OK
T[] items = new T[3]; // Error!
}
new
연산자로 배열을 생성하는 것은 할 수 없다. new
연산자는 컴파일 시점에 타입 T가 뭔지 정확하게 알아야 하기 때문이다. 하지만 지금 클래스에 작성된 코드만 봤을 때엔 타입 T가 어떤 타입이 될 지 전혀 알 수 없다. instanceof
연산자도 마찬가지 이유로 T를 피연산자로 사용할 수 없다. Reflection API
의 newInstance()
와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object
배열을 생성해서 복사한 다음에 T[]
로 형변환하는 방법을 사용할 수 있다고 하는데, ArrayList
와 같은 컬렉션 클래스를 사용하면 간편하고 가독성도 좋으니까 이 쪽을 사용하자. class FruitBox<T extends Fruit> {
ArrayList<T> list = new ArrayList<>();
}
Fruit
타입의 자손만 타입으로 지정할 수 있다. extends
키워드를 사용한다. interface Eatable {}
class FruitBox<T extends Eatable> {}
Fruit
의 자손이면서 Eatable
인터페이스도 구현해야 한다면 &
기호로 연결한다.class FruitBox<T extends Fruit & Eatable> {}
Fruit
의 자손이면서 Eatable
인터페이스를 구현한 클래스만 타입 매개변수 T에 대입될 수 있다. <?>
을 볼 수 있다. 이것의 명칭을 와일드카드라 한다.static Juice makeJuice(FruitBox<?> f) {}
와일드카드도 제네릭과 비슷하게 상한과 하한을 적용할 수 있다.
<? extends T>
: 와일드카드의 상한 제한. T와 그 자손들만 가능
<? super T>
: 와일드카드의 하한 제한. T와 그 조상들만 가능
<?>
: 제한 없음. 모든 타입 가능. <? extendx Object>
와 동일
하지만 &
기호는 사용할 수 없다. <? extends T & E>
는 불가능하다.
static Juice makeJuice(FruitBox<? extends Fruit> f) {}
Fruit
클래스와 그 자손 클래스들만 가능해진다. class FruitBox<T> {
static <T> void sort(List<T> list, Comparator<? super T> c) {}
}
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {}
FruitBox<T>
의 타입 매개변수 T에 대한 자료형을 <T extends Fruit>
을 통해 지정해 주었다. 그래서 이 메서드가 스태틱 메서드여도 타입 매개변수 T를 사용할 수 있는 것이다. Collections
의 sort()
메서드를 이해할 수 있다. public static <T extends Comparable<? super T>> void sort(List<T> list)
Comparable
을 구현한 클래스여야 하며 T 또는 그 조상의 타입을 비교하는 Comparable
이어야 한다. Student
이고 Person
의 자손이라면 <? super T>
은 Student
, Person
, Object
가 가능하다.