Generics
타입 파라미터, wildcard, type erasure, 장/단점, etc
Generics
메서드나 Collection 클래스에 컴파일 시의 타입체크를 해준다.
의도하지 않는 타입의 객체가 저장되는 것을 막고, 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여줌으로써 타입 안정성을 제공한다.
타입 파라미터
제네릭(Generics)에서는 <>
안에 타입을 지정하여 명시적으로 지정할 수 있다.
아래와 같이 Integer를 명시적으로 지정함으로써 해당 List은 정수형 숫자만 저장할 수 있는것이다.
List<Integer> list = new ArrayList();
타입 매개변수/타입 변수라고 부르며, 정해진 것이 없지만 아래처럼 파라미터 기호를 사용하고는 한다. | 타입 | 설명 | | ——- | ——————————— | | T | 타입(Type) | | E | 요소(Element) | | K | 키(Key) | | V | 값(Variable) | | N | 숫자(Number) | | S, U, V | 2번째, 3번째, 4번째에 선언된 타입 |
복수 타입 파라미터
타입 지정이 여러개가 필요시 다음과 같이 사용할 수 있다.
class FigureBox<T, U> {
List<T> squares = new ArrayList();
List<U> circles = new ArrayList();
public void add(T square, U circle) {
squares.add(square);
circles.add(circle);
}
}
wildcard
제네릭(Generics)은 보통 하나의 타입을 지정하지만 하나 이상의 타입을 지정해야하는 경우를 위해 와일드카드
가 존재한다.
와일드카드란 ‘?’를 이용하여 하나 이상의 타입을 가능하게 한다.
List<? extends Figure> list = new ArrayList();
List<?> list = new ArrayList();
List<? super Square> list = new ArrayList();
위처럼 3가지 형태로 와일드카드를 사용할 수 있다.
<?>
: 모든 클래스나 인터페이스를 타입으로 사용할 수 있다.
<? extends 상위타입>
: 상위 타입이나 이를 상속받은 하위 클래스들이 타입으로 사용할 수 있다.
<? super 하위타입>
: 하위 타입이나 상속하고 있는 상위 클래스를 타입으로 사용할 수 있다.
제네릭 장단점
장점
- 타입 안정성을 제공한다.
- 타입체크와 형변환을 생략할 수 있어 코드가 간결해진다. (불필요한 캐스팅 없애 성능 향상)
단점
- 가독성 저하? 문법이 복잡하고 어려울 수 있어 제네릭에 대해서 이해하지 않으면 예상치 못한 동작들이 발생할 수 있다.
- jdk1.5 같은 하위 버전에서 코드의 호환성이 불가
제네릭 타입 소거 (Erasure)
제네릭은 jdk1.5부터 도입된 문법으로 이전의 자바 버전과 호완성을 위해 제네릭 코드는 컴파일이 되면 사라지게 된다.
즉, 클래스파일(.class)에는 제네릭 타입에 대한 코드는 없어진다.
그래서 런타임시에는 제네릭을 제거하기 때문에 개발자가 잘못된 방향으로 설계를 하면 잠재적인 힙 오염(heep pollution)문제에 빠지게 된다.
실체화 타입
런타임에서 제거되지 않는것이 실체화 타입(Reifiable Type)이라 한다.
- 기본형 데이터 타입 (int, double, byte 등)
- 클래스 및 인터페이스 (Number, List, Map 등)
- 개발자가 작성한 클래스
비실체화 타입
컴파일 단계에서 타입소거에 의해 제거되는 것을 비실체화 타입(Non-Reifiable Type)이라 한다.
- List<T>, List<E>
- List<Number>, ArrayList<String>
- List<? extends Number>, List<? super String>
제네릭 클래스 Erasure
- unbound type(<?>, <T>)는 Object로 변환된다.
- bound type(<E extends Number>)는 Number로 변환된다.
- 제네릭 타입을 사용할 수 있는 일반 클래스, 인터페이스, 메서드에서만 소거 규칙을 적용한다.
- 타입 안정성 보존을 위해 필요하다면 type casting을 넣는다.
- 확장된 제네릭 타입에서 다형성을 보존하기 위해 bridge method를 생성한다.
아래는 unbound type이 변형되는 코드이다.
// 소거 전
public class Test<T> {
public void test(T test) {
System.out.println(test.toString());
}
}
// 소거 후
public class Test {
public void test(Object test) {
System.out.println(test.toString());
}
}
아래는 bound type이 변형되는 코드이다.
// 소거 전
public class Test<T extends Number> {
private T data;
public T getDate() {
return data;
}
public void setDate(T data) {
this.data = data;
}
}
// 소거 후
public class Test {
private Number data;
public Number getDate() {
return data;
}
public void setDate(Number data) {
this.data = data;
}
}
bound type에서는 한정시킨 타입으로 변환된다.
아래는 제네릭 타입의 안정성을 위한 bridge method에 대한 코드이다.
// 소거 전
public class Test implements Comparator<Integer> {
public int compare(Integer a, Integer b) {
//
}
}
// 소거 후
public class Test implements Comparator {
public int compare(Object a, Object b) {
return compare((Integer) a, (Integer) b);
}
}
Comparator의 compare 메서드의 매개변수 타입이 Object로 바뀌면서 이러한 불일치를 없애기 위해 컴파일러는 런타임에서 타임소거를 위한 bridge method를 만들어준다.