이펙티브 자바 - item 26. 로 타입은 사용하지 말라
on JAVA
이펙티브 자바 - item 26. 로 타입은 사용하지 말라
용어정리
한글 용어 | 영문 용어 | 예 |
---|---|---|
매개변수화 타입 | parameterized type | List |
실제 타입 매개변수 | actual type parameter | String |
제네릭 타입 | generic type | List |
정규 타입 매개변수 | formal type parameter | E |
비한정적 와일드카드 타입 | unbounded wildcard type | List<?> |
로 타입 | raw type | List |
한정적 타입 매개변수 | bounded type parameter | |
재귀적 타입 한정 | recursive type bound | <T extends Comparable |
한정적 와일드카드 타입 | bounded wildcard type | List<? extends Number> |
제네릭 메서드 | generic method | static |
타입 토큰 | type token | String.class |
로 타입과 단점
클래스와 인터페이스 선언에 타입 매개변수(type parameter)가 쓰이면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라고 한다. 그리고 이 둘을 통틀어 제네릭 타입이라고 한다.
로 타입 이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않은 경우를 의미한다. 예를 들어 List
로 타입 선언은 제네릭 타입정보가 전부 지워진 것 처럼 동작한다. 이는 제네릭이 생기기 전 코드와 호환되도록 하기 위한 어쩔 수없는 방법이기 때문이다.
로 타입 단점 예시
// Stamp 인스턴스만 취급한다.
private final Collection stamps = ...;
..
stamps.add(new Coin()); // 정상적으로 컴파일, unchecked call 경고
이 코드를 사용하면 실수로 stamp 대신 coin을 넣어도 넣는 경우 컴파일 단계에서 오류가 발생하지 않고 런타임에서 오류가 발생하는 큰 단점이 있다.
로 타입을 쓰면 제너릭이 안겨주는 안정성과 표현력을 모두 잃게 된다. 그럼에도 이러한 로 타입이 사용되는 이유는 단지 제너릭이 받아들이기 전에 짜여진 코드와의 호환성때문이다.
타입 매개변수
private final Collection<Stamp> stamps = new ArrayList<>() {};
제너릭 타입을 선언할 때 로 타입 대신 타입 매개변수를 함께 사용하면, 엉뚱한 타입의 인스턴스를 넣으려 할 때 컴파일 오류가 발생해 무엇이 잘못됐는지를 정확히 파악할 수 있다.
List
List
는 제네릭 타입에서 완전히 발을 뺀 것이지만 List<Object>
은 모든 타입을 허용한다는 것을 컴파일러에 명확히 전달한다는 차이점이 있다.
아래의 예시를 살펴보자.
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
unsafeAdd(strings, Integer.valueOf(42));
String s = strings.get(0); // ClassCastException
}
private static void unsafeAdd(List list, Object o) {
// unchecked call 경고
list.add(o);
}
위의 코드를 살펴보면 Integer를 String으로 변환하려고 하고있다. 하지만 위의 코드는 컴파일 단계에서 오류가 발생하지 않고 런타임에서 strings.get(0)의 결과를 변환하려 할 때 ClassCastException을 던진다.
위의 코드에서 로 타입 List만 매개변수 타입인 List
제네릭 타입을 쓰고 싶지만 타입 매개변수를 신경 쓰고 싶지 않을 때는 와일드 카드 타입을 사용하자
// 잘못된 예 - 모르는 타입의 원소도 받는 로 타입을 사용했다.
static int numElementsInCommon(Set set1, Set set2){
int result = 0;
for (Object o1 : set1) {
if (set2.contains(o1))
result++;
}
return result;
}
동작은 하나 로 타입을 사용해 안정성이 떨어진다. 따라서 비한정적 와일드 카드 타입을 대신 사용하는 게 좋다.
static int numElementsInCommonWildCard(Set<?> set1, Set<?> set2){...}
와일드카드 타입은 안전하고, 로 타입은 안전하지 않다. 로 타입 컬렉션에는 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽다. 반면, Collection<?>에는 (null 외에는) 어떤 원소도 넣을 수 없다. 다른 원소를 넣으려 하면 컴파일할 때 다음의 오류 메시지를 보게 될 것이다.
WildCard.java:13: error: incompatible types: String cannot be
converted to CAP#1
c.add("verboten");
^
로타입을 사용할 수 있는 몇 가지 예외
- class 리터럴에는 로 타입을 써야한다.
자바 명세에는 class 리터럴에 매개변수화 타입을 사용하지 못하게 했다. 예를들어 List.class
, String[].class
, int.class
는 허용하나 List<String>.class
나 List<?>.class
는 허용하지 않는다.
- instanceof 연산자
런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입을 적용할 수 없다. 또한 로 타입이든 비한정적 와일드카드 타입이든 instanceof는 완전히 똑같이 동작한다.
따라서 비한정적 와일드 카드 타입의 꺽쇠 괄화와 물음표는 역할 없이 코드만 지저분하게 만드므로 차라리 로 타입을 쓰는게 깔끔하다.
아래는 제네릭 타입에 instanceof를 사용하는 올바른 예이다.
if (o instanceof Set) { // 로 타입
Set<?> s = (Set<?>) o; // 와일드카드 타입
...
}
정리
- 로 타입을 사용하면 런타임에 예외가 발생할 수 있으니 사용하지 말자.
- 로 타입은 제네릭이 도입되기 이전 코드와의 호환성을 위해 제공될 뿐이다.
- Set
- 이들의 로 타입인 Set은 제네릭 타입 시스템에 속하지 않는다.
- Set