제네릭을 지원하기 전에는 컬렉션에서 객체를 꺼낼 때마다 형변환을 해야 했다.
제네릭을 사용하면 컬렉션이 담을 수 있는 타입을 컴파일러에게 알려준다.
컴파일러는 알아서 형변환 코드를 추가할 수 있고 엉뚱한 타입의 객체를 넣으려는 시도를
컴파일 과정에서 판단하여 더 안전하고 명확한 프로그램을 만들어준다.
26. 로 타입은 사용하지 말라.
클래스와 인터페이스 선언에 타입 매개변수가 쓰이면 이를 제네릭 클래스 혹은 제네릭 인터페이스라 한다.
ex) List<E>
: List 인터페이스는 원소의 타입을 나타내는 타입 매개변수 E를 받는다.
제네릭 타입을 하나 정의하면 그에 딸린 로 타입(raw type)도 함께 정의된다.
로 타입이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다.
public class Trouble<T> {
public List<String> getStrs() {
return Arrays.asList("str");
}
public static void main(String[] args) {
Trouble t = new Trouble();
for (String str : t.getStrs()) {
System.out.println(str);
}
}
}
위 코드를 컴파일 하려고 하면 다음과 같은 에러가 발생한다.
Trouble.java:17: error: incompatible types: Object cannot be converted to String
for (String str : t.getStrs()) {
^
위 예제 코드에서 t가 로 타입 변수이다.
Raw Type은 타입 파라미터 T만 지워버리는 것이 아니라 슈퍼 클래스의 타입 파라미터도 지우고,
해당 클래스에 정의된 모든 타입 파라미터를 지워버린다.
그래서 t.getStrs()
의 반환 타입이 List<String>
이 아닌 Raw Type List가 된 것이다.
Raw Type은 자바에 제네릭이 도입되기 전 코드와 호환성을 보장하기 위한 것이므로
정적 타입 언어인 자바의 강점을 이용하기 위해 Raw Type을 사용하면 안 된다.
27. 비검사 경고를 제거하라.
제네릭을 사용하기 시작하면 수많은 컴파일러 경고를 보게 될 것이다.
예) Set<Lark> exaltation = new HashSet();
위 코드는 unchecked conversion 경고를 출력한다.
컴파일러가 알려준 타입 매개변수를 명시하면 경고가 사라지는데
자바 7부터 지원하는 다이아몬드 연산자(<>)로 해결할 수 있다.
new HashSet<>();
위 예제는 해결하기 쉬운 경고다.
해결하기 어렵더라도 할 수 있는 한 모든 비검사 경고를 제거하면
그 코드는 타입 안전성이 보장된다.
만약 경고를 제거할 수 없지만 타입 안전하다고 확신할 수 있다면
최대한 좁은 범위에 @SuppressWarnings
어노테이션을 적용하자.
경고를 숨기기로 한 근거가 있어야 한다.
28. 배열보다는 리스트를 사용하라
배열과 제네릭 타입에 중요한 차이점이 있다.
1) 공변 / 불공변
- 배열은 공변이다. (함께 변한다는 의미)
Sub가 Super의 하위 타입이면, Sub[]는 Super[]의 하위 타입이 된다. -
제네릭은 불공변이다. List
과 List 는 서로 다르다. 상위 타입도 하위 타입도 아니다. - 런타임 실패
Object[] objectArray = new Long[1];
objectArray[0] - "타입이 달라 넣을 수 없다."; // ArrayStoreException을 던진다.
- 컴파일되지 않음
List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입이다.
ol.add("타입이 달라 넣을 수 없다.");
배열은 실수를 런타임에야 알게 되지만, 리스트를 사용하면 컴파일할 때 바로 알 수 있다.
2) 실체화 (reify)
배열은 실체화된다. 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
반면, 제네릭은 타입 정보가 런타임에는 소거된다.
원소 타입을 컴파일타임에만 검사하며 런타임에는 알 수조차 없다는 뜻이다.
위 주요 차이로 인해 배열과 제네릭은 잘 어우러지지 못한다.
배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
다음 코드들은 컴파일할 때 제네릭 배열 생성 오류를 일으킨다.
new List<E>[]
new List<String>[]
new E[]
제네릭 배열을 허용하지 않는 이유
List<String>[] stringLists = new List<String>[]; // (1)
List<Integer> intList = List.of(42); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
Stirng s = stringLists[0].get(0); // (5)
실제로는 컴파일되지 않지만, (1)을 허용한다고 가정해보자.
제네릭은 소거 방식으로 구현되어서 (4)까지 성공한다.
즉 런타임에는 List<Integer>
인스턴스 타입은 단순한 List가 되고,
List<Integer>[]
인스턴스의 타입은 List[]가 된다.
따라서 (4)에서 ArrayStoreException을 일으키지 않는다.
그러나 List<String>
인스턴스만 담겠다고 선언한 stringLists 배열에는 지금 List<Integer>
인스턴스가 저장되어 있다.
그리고 (5)에서 원소를 꺼내는데 컴파일러는 꺼낸 원소를 자동으로 String으로 형변환하는데,
이 원소는 Integer이므로 런타임에 ClassCastException이 발생한다.
이를 방지하려면 (제네릭 배열이 생성되지 않도록) (1)에서 컴파일 오류를 내야 한다.