[Effective Java] 아이템26 - Raw 타입은 사용하지 말라
5장에서는 제너릭의 이점을 최대로 살리고 단점을 최소화하는 방법을 이야기 한다.
Raw Type 이란
class
와 interface
선언에 type parameter가 쓰이면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라 한다.
public interface List<E> extends Collection<E> { ... }
List 인터페이스는 원소의 타입을 나타내는 type parameter E
를 받는다.
List인터페이스를 쓸 때, 타입을 E의 위치에 넣어서 사용한다.
List<String>
은 원소의 타입이 String
인 리스트를 뜻하는 parameterized type
이다. -> 매개변수로 변한 타입이라는 뜻
이렇게 제네렉 타입을 하나 정의하면 그에 딸린 raw type도 함께 정의된다.
- Raw Type : 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다.
List<E>
의 Raw Type은 매개변수가 없는 List
그 자체이다.
제네릭은 자바5부터 사용할 수 있다. Raw Type은 제네릭을 지원하기 전의 코드에 대한 호환의 목적인 궁여지책이라 할 수 있다.
List list = new ArrayList(); // Raw type
List<Integer> listIntegers = new ArrayList<>(); // parameterized type
Raw Type은 자바5 이전 generic이 없던 레거시 코드에 사용될 수 있다. 하지만, 우리는 이제부터 사용하면 안된다! 왜냐하면 :
- Not expressive
- They lack type safety, and
- Problems are observed at run time and not at compile time
2번과 3번에 대해서 자세히 알아보자
컴파일 타임이 아닌 런타임에 에러를 인지한다.
로 타입 선언에서의 예
위 코드를 설명하면 아래와 같다.
Stamp
,Coin
클래스Collection
로 타입으로 stamps 선언 (개발자는 Stamp라는 클래스만 담는다는 의미로 해당 변수 선언)- 실수로
Stamp
대신Coin
추가
이 때, 실수로 Stamp 대신 Coin을 해당 컬렉션에 넣어도 아래와 같은 경고창만 보여주고 오류 없이 컴파일되고 실행된다.
Type safety: The method add(Object) belongs to the raw type Collection. References to generic type Collection
자 그럼, Coin
이 담겨져 있는 stamps에서 원소를 꺼내오고, (당연히 Stamp가 들어가 있을 거라고 생각한 개발자가) Stamp
로 형변환 해보자.
java.lang.ClassCastException: ch5.sunmin.item26.RawType$Coin cannot be cast to ch5.sunmin.item26.RawType$Stamp
즉, 컬랙션에서 이 동전을 다시 꺼내기 전에는 오류를 알아채지 못한다. 컴파일 타임이 아닌 런타임에 에러를 인지한다.
제너릭을 활용해서 타입 선언
private final static Collection<Stamp> stampsGeneric = new ArrayList<>();
이렇게 제너릭을 사용해서 타입을 매개변수로 지정해주면, 컴파일러에서 Stamp의 인스턴스만 넣어야 함을 인지하게 된다. 따라서 의도대로 동작할 것임을 보장한다.
이제 똑같이 잘못된 클래스를 넣을 경우, 컴파일 조차도 안되게끔 에러를 보여주는 것을 볼 수 있다.
로 타입을 파라미터로 쓰는 메서드에서의 예
로타입인 List
를 받는 메서드에 List<String>
을 넘길 수는 있다. 왜냐하면 List<String>
은 로 타입인 List
의 하위 타입이기 때문이다.
하지만 실행을 하면 컴파일은 정상이지만 Integer 를 String으로 변환하려 시도하기 때문에 ClassCastException
을 던지게 된다.
이제 unsafeAdd 메서드의 파라미터를 List
에서 List<Object>
로 바꿔보자.
아예 컴파일이 되지 않는다. 왜냐하면 List<String>
은 List<Object>
의 하위 타입은 아니기 때문이다. [아이템28]
정리하자면, 로 타입인 List는 모든 타입의 원소를 넘겨받을 수 있지만, 매개변수화 타입인 List<Object>
는 List<Object>
로만 파라미터를 받을 수 있다.
이렇게 하면 Type Safety 하게 코드를 작성할 수 있지만, 아예 원소의 타입을 몰라도 되는 로 타입을 쓰고 싶을 수 있다.
비한정적 와일드카드 타입
private static int numElementsInCommon(Set s1, Set s2) {
int result = 0;
for (Object o1 : s1) {
if(s2.contains(o1)) {
result ++;
}
}
return result;
}
로타입인 Set
을 파라미터로 받아서, 모르는 타입의 원소도 받을 수 있도록 작성했다. 메서드는 정상 작동하지만 Type Safety 하지 못하다. (위의 예시처럼 다른 Type이 들어왔을 때 ClassCastException
) 이럴 때는 비한정적 와일드카드 타입 (unbounded wildcard type) 을 대신 사용하는게 좋다.
제네릭 타입을 쓰고는 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않다면 unbounded wildcard type
을 사용하면 된다.
private static int numElementsInCommon(Set<?> s1, Set<?> s2) {...}
Set<?>
이 바로 unbounded wildcard type 이다. 처음에 넣은 타입 외에는 다른 원소를 넣을 수 없다.
처음에 넣은 타입 이외의 원소를 넣어보자!
The method add(capture#1-of ?) in the type Set<capture#1-of ?> is not applicable for the arguments (String)
위 소스는 Set에 Integer를 넣고, 메서드에 파라미터로 전송했다. 그 Set에 String을 다시 넣으려면 아래와 같은 오류로 컴파일되지 않는다. 따라서 그냥 로타입 보다는 Type Safe 하다고 볼 수 있다.
로 타입을 허용하는 예외
class 리터럴
자바 명세는 parameterized type
을 배열과 기본타입에는 허용하지만 List<String>.class
와 List<?>.class
와 같은 class 리터럴에 매개변수화 타입을 사용하지 못하게 했다.
이게 무슨 말인가해서 class 리터럴에 대해 알아봤다.
클래스 리터럴, 타입 토큰, 수퍼 타입 토큰 포스팅을 읽으니 감이 왔다.
- 클래스 리터럴 (Class Literal)은
String.class
,Integer.class
등을 말하며 하나의 객체로 생각하면 된다. 그러면, 이들의 타입으로는 ..String.class
의 타입은Class<String>
,Integer.class
의 타입은Class<Integer>
로 볼 수 있다. - 그러면
Class<T>
가 로타입인Class
의 매개변수화 타입이라는 것을 유추해볼 수 있다.
Class
에 제너릭을 추가하여 Class<T>
처럼 매개변수화 타입을 파라미터로 받는 예이다.
class SimpleTypeSafeMap {
private Map<Class<?>, Object> map = new HashMap<>();
public <T> void put(Class<T> k, T v) {
map.put(k, v);
}
public <T> T get(Class<T> k) {
return k.cast(map.get(k));
}
}
Class<T>
제너릭 타입으로, 다양한 Type을 Map에 put 하고 get 할때는 그 클래스 리터럴 자체로 casting을 할 수 있는 Map 클래스이다.
여기서 List.class, String[].class, int.class 와 같은 클래스 리터럴은 Class<T>
에 허용하고, List<String>.class
와 List<?>.class
는 허용하지 않는다.
SimpleTypeSafeMap simpleTypeSafeMap = new SimpleTypeSafeMap();
simpleTypeSafeMap.put(String.class, "abcde");
simpleTypeSafeMap.put(Integer.class, 123);
String v1 = simpleTypeSafeMap.get(String.class);
Integer v2 = simpleTypeSafeMap.get(Integer.class);
// 아래와 같은 List<String>.class라는 클래스 리터럴은 언어에서 지원해주지 않으므로 사용 불가!!
simpleTypeSafeMap.put(List<String>.class, Arrays.asList("a", "b", "c"));
instanceof
연산자
런타임에는 제너릭 타입 정보가 지워지므로 instanceof
연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다.
if(o instanceof Set<String>) { // 제너릭 -> 오류 발생!
Set<?> s = (Set<?>) o; // 와일드타입으로 형변환
}
Object o = new Object();
if(o instanceof Set) { // 로타입
Set<?> s = (Set<?>) o; // 와일드타입으로 형변환
}
댓글남기기