본문 바로가기

Dev Book Review/Effective Java

[Effective Java] item26. 로타입은 사용하지 말라

1. 용어정리

public class<T> Example{
     private T member; 
}
  • 제네릭 클래스[인터페이스] : 클래스[인터페이스] 선언에 타입 매개변수(type parameter)가 쓰인다. Example.class
  • 제네릭 타입(Generic Type) : 제네릭 클래스와 제네릭 인터페이스를 통틀어 이르는 말. Example<T>
  • 매개변수화 타입(parameterized Type) : 각각의 제네릭 타입은 parameterizedType을 선언함. Example<String>
  • 타입 매개 변수(Type parameter) : 제네릭 선언에 사용된 매개변수 <T>
  • formalType : Example<E>
  • actualType : Example<String>
  • 로타입(raw type) : 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않았을 때. Example
    로타입은 제네릭 전후 코드의 호환을 위한 것이며, 동작하지만 좋은 예는 아니다.

 

2. 제네릭의 타입 안정성

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

a. 로타입의 단점 : 컴파일 타임에 타입 정보를 알지 못한다.

ClassCastException과 같은 런타임에야 알아챌 수 있는 에러를 만들기 쉽다.

실제로 아래 예제는, 로타입의 사용으로 인해 컴파일 타임에는 문제가 없으나, 실제 런타임에는 ClassCastException이 터진다.
Integer값을 String값으로 캐스팅하려 했기 때문이다.

public class Raw {
    public static 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);
    }
}
 

b. 제네릭의 장점 : 컴파일 타임에 타입 선언이 녹아든다.

컴파일러가 타입선언에 대해 인지하고 있기 때문에, 아무런 경고 없이 컴파일되면 의도대로 동작할 것임을 보장한다.

public class Raw {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        unsafeAdd(strings, "42");   // 컴파일 타임에 에러를 체크해주어서 바꿀 수 있다.
        String s = strings.get(0);
    }

    private static <T> void unsafeAdd(List<T> list, T o) { // 제네릭 선언
        list.add(o);
    }
}

컴파일러가, 컬렉션에서 원소를 넣는 모든 곳에 보이지 않는 형변환을 추가하여 절대 실패하지 않음을 보장하였다.

++ 추가 내용

 

3. 로타입은 절대로 사용하지 말자.

제네릭이 안겨주는 안정성과 표현력을 모두 잃게된다.

a. 로타입이 만들어진 이유 - 호환성

기존 코드를 모두 수용하면서 제네릭을 사용하는 새로운 코드와 맞물려돌아가게 하기 위해서이다.
호환성 : 로타입 지원 + 제네릭 구현시 소거(erasure) 방식을 이용

List vs List<Object>

  • List : 로타입 - 제네릭타입에서 완전 발은 떼었다.
  • List<Object> : 임의 객체를 허용하는 매개변수화 타입 - 타입에 대한 정보를 컴파일러에 알려주었다.
  • 제네릭의 하위타입 규칙 : List<String>은 로타입인 List의 하위타입이지만, List<Object>의 하위타입은 아니다.

로타입을 사용하면 타입 안정성을 잃게된다.

 

4. 타입 안전 + 유연 : 비한정 와일드카드 타입

비한정 와일드카드 타입(unbounded wildcard type) : 제네릭 타입을 쓰고 싶지만, 실제 타입 매개변수가 무엇인지 신경쓰고 싶지 않을 때 사용한다. Set<E>의 비한정 와일드카드 타입은 Set<?>

static int numElementsInCommon(Set<?> s1, Set<?> s2){...}

와일드카드 타입은 안전하고 로타입은 안전하지 않다.

  • 로타입 : 아무 원소나 넣을 수 있어 타입 불변식을 훼손하지 쉽다.
  • 와일드카드 타입 : Collection<?>에는 어떤 원소도 넣을 수 없다. (null 외에는)

    컴파일 타임에 오류메세지를 볼 수 있다.
    컬렉션의 타입 불변식을 훼손하지 못하게 막고, 컬렉션에서 꺼낼 수 있는 객체의 타입도 알수 없게 한다.

 

5. 로타입의 규칙예외

a. class 리터럴에는 로타입을 사용한다

자바 명세 자체가 class 리터럴에 매개변수화 타입을 사용하지 못하게 하였다.

  • 허용 : List.class, String[].class, int.class
  • 불가 : List<String>.class, List<?>.class

b. Instanceof 연산자는 로타입을 사용한다.

instanceof 연산자는 비한정 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다.

로타입이든 비한정적 와일드카드 타입이든 instanceof는 똑같이 동작한다. : <?> 코드만 지저분해지므로 로타입

런타임에는 제네릭 타입정보가 지워진다.

if(o instanceof Set){            // 로타입
  Set<?> s = (Set<?>) o;    // 와일드카드 타입
}

 

6. 용어정리

한글 영문
매개변수화 타입 parameterized type List<String>
실제 타입 매개변수 actual type parameter String
제네릭 타입 generic type List<E>
정규 타입 매개변수 formal type parameter E
비한정적 와일드카드 타입 unbounded wildcard type List<?>
로 타입 raw type List
한정적 타입 매개변수 bounded type parameter <E extends Number>
재귀적 타입 한정 recursive type bound <T extends Comparable<T>>
한정적 와일드카드 타입 Bounded wildcard type <? extends Number>
제네릭 메서드 generic method static <E> List<E> asList(E[] a)
타입 토큰 type token String.class