본문 바로가기

Java

[Effective Java] 아이템 26. Raw타입은 사용하지 말라.

더보기

아이템26 요약

- raw타입은 런타임시에 예외가 발생할 수 있으니 사용하면 안된다.

- 차라리 제네릭<Object>로 어떤 타입의 객체도 저장할 수 있도록 컴파일러에 명시한다.

- 타입을 모르는 경우에는 비한정적 와일드 타입을 사용하여 모르는 타입을 받아들이게 한다. 

 

 

 

1. 제네릭 타입

- 클래스나 인터페이스를 선언할 때 List<E>와 같이 '타입 매개변수'가 쓰이면 제네릭 타입(제네릭 클래스 혹은 제너릭 인터페이스)라고 한다.

- 클래스(혹은 인터페이스) 이름 다음에 <,>안에 실제 타입 매개변수들을 나열하는 방식으로 정의한다!

- 제네릭 타입이 정의되면 Raw 타입도 함께 정의된다. -> List<E>의 raw타입은 List이다.)

 

 

2. 제네릭을 사용해보자

제네릭을 활용하면 선언 자체에 타입을 검사한다.

private final Collections<Stamp> stamps = ...; // 제너릭 활용
stamps.add(new Coin(...)); // 실수로 Coin을 넣는다.

이렇게 선언하면 컴파일러는 stamps에는 Stampe 인스턴스만 넣어야 함을 컴파일러가 인시하게 된다.

위 코드처럼 stamps에 Coin인스턴스를 넣으려 하면, 컴파일 오류가 발생하고 무엇이 잘못됐는지를 정확하게 알려준다.

Test.java:9: incompatiable types:Coin cannot be converted to Stamp

 

 

또한 원소를 꺼내는 모든 곳에서 보이지 않는 형변환을 추가하여 절대 실패하지 않음을 보장한다.

for (Iterator i = stamps.iterator(); i.hasNext(); ) {
	Stamp stamp = i.next();
    stamp.cancel();
}

 

 

3. raw타입을 사용해보자

  • Java 1.5 이전(제네릭 지원 전)에는 컬렉션을 아래와 같이 사용했다.
private final Collection stamps = ...; //Stamp인스턴스만 취급하려고 한다. 
stamps.add(new Coin(...)); // 실수로 Coin을 넣는다.
// unchecked call. 
// 아무 오류 없이 컴파일되고 실행된다.
Java

실수로 Stamp대신 Coin을 넣어도 아무런 오류 없이 실행되고 컴파일 된다.

하지만!! 컴파일 오류는 발생하지 않지만, 런타임 에러가 발생할 수 있다.

 

 

for (Iterator i = stamps.iterator(); i.hasNext(); ) {
	Stamp stamp = (Stamp) i.next(); // ClassCastException을 던진다.
    stamp.cancel();
}

add한 Coin 객체를 꺼내서 Stamp 변수에 할당할 때, ClassCastException이 발생한다.

(raw타입은 제네릭을 제공하기 전 코드와의 호환성을 깨뜨리지 않기 위해 제공되었다..!!)

 

 

런타임 에러가 발생하면 고칠 수 있지 않나??  왜 단점으로 언급되었을까?

오류는 가능한 한 발생 즉시, 이상적으로는 컴파일할 때 발견하는 것이 좋다.

하지만 위와 같은 예시에서는 런타임에야 오류를 알아차릴 수 있는데, 스택트레이스에 하단에 위치할 가능성이 커진다.

런타임에 문제를 겪는 코드와 실제 원인을 제공한 코드에 거리가 멀어진다.

즉, 코드를 타고타고 가면서 코드를 이해하고, 원인이 발생한 부분을 사람이 직접 찾아야 하는 경우가 많다. 

 

 

4. raw타입의 단점

제네릭에서 raw타입을 사용한다는 것은, 제너릭<E>에서 제네릭 타입을 지정하지 않는다는 것을 의미한다.

 

예시) List<String> 대신 List

 

raw타입을 사용하게 되면 제네릭의 타입 안정성과 표현력이라는 장점을 활용하지 못한다.

List보다는 List<Object>로 '모든 타입을 허용한다'는 의사를 컴파일러에게 전달하는 것이 좋다. 

 

List에는 List을 넘길 수 있지만, List<Object>

에는 List<String>을 넘길 수 없다.  (제네릭 하위 타입 규칙)

두 차이를 아래 예시로 살펴보자. 

예시) raw타입(List)를 사용

public staic void main (String[] args) {
	List<String> strings = new ArrayList<>();
    
    unsafeAdd(strings, Integer.valueOf(42));
    String s = strings.get(0); //컴파일러가 자동으로 형변환 코드를 넣어준다.
}

private static void unsafeAdd(List list, Object o) {
	list.add(o);
}

컴파일은 되지만, unsafeAdd가 실행될 때 ClassCastException이 발생한다. 

Integer를 String으로 변환하려 했기 때문이다. 

Test.java:9: warning:[unchecked] unchecked call to add(E) as a member of the raw type List

list.add(o);

 

 

예시) List<Object>로 명시

public static void main(String[] args) {
    List<String> strings = new ArrayList<>();

    unsafeAdd(strings, Integer.valueOf(42));
    String s = strings.get(0);
}

// List<Object>
private static void unsafeAdd(List<Object> list, Object o) {
    list.add(o);
}

Test.java:9: error: incompatiable types: List<String> cannot be converted to List<Object>

unsafeAdd(strings, Integer.valueOf(42));

-> 위와 같은 오류가 발생하며 컴파일이 되지 않는다.

 

원소의 타입을 모르는 경우에는?

비한정적 와일드 타입(unbounded wildcard type)을 사용한다.

static int numElementsInCommon(Set<?> s1, Set<?> s2){
	int result = 0;
    for(Object o1 : s1){
        if(s2.contains(o1))
            result++;
    }
    
    return result;
}

 

로타입을 사용하는 것에 비해 타입 안전(=허용할 수 없는 타입이라면 컴파일시에 에러메시지를 출력한다.)하다.

또한 raw타입 컬렉션에는 앞서 살펴본 예제처럼 아무 원소나 넣을 수 있어서 타입 불변식을 훼손하기 쉽다.

 

 

 

5. raw타입을 사용해야 하는 경우

1. 클래스 리터럴은 raw 타입을 사용해야 한다. 

List.class, String[].class, int.class는 허용되지만, List<String>.class, List<?>.class는 허용되지 않는다.

 

2. instanceof에는 raw타입을 사용해야 한다.

if( o instanceof Set) {
    Set<?> s = (Set<?>) o;
}