본문 바로가기

독서찰기(讀書札記)/이펙티브 자바

[아이템 26] 로 타입(raw type)은 사용하지 말라

용어 정리

  • 제네릭 클래스/제네릭 인터페이스: 클래스/인터페이스 선언에 타입 매개변수(type parameter)가 쓰인 것
  • 제네릭 타입(generic type): 제네릭 클래스와 제네릭 인터페이스를 통틀어 칭함
  • 매개변수화 타입(parameterized type): e.g.) List<String>은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입
                                                                  여기서 String이 정규(formal) 타입 매개변수 E에 해당하는 실제(actual) 타입 매개변수
  • 로 타입(raw type): 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때 e.g.) List<E>의 로 타입은 List
                                  자바에 제네릭 기능이 생기기 전 코드와 호환되도록 하기 위한 궁여지책
한글 용어 영문 용어 아이템
매개변수화 타입 parameterized type List<String> 아이템26
실제 타입 매개변수 actual type parameter String 아이템26
제네릭 타입 generic type List<E> 아이템26, 아이템 29
정규 타입 매개변수 formal type parameter E 아이템26
비한정적 와일드카드 타입 unbounded wildcard type List<?> 아이템26
로 타입 raw type List 아이템26
한정적 타입 매개변수 bounded type parameter <E extends Number> 아이템29
재귀적 타입 한정 recursive type bound <T extends Comparable<T>> 아이템30
한정적 와일드카드 타입 bounded wildcard type List<? extends Number> 아이템31
제네릭 메서드 generic method static <E> List<E> asList(E[] a) 아이템30
타입 토큰 type token String.class 아이템33

 

[Why]

  • 로 타입을 쓰면 제네릭이 안겨주는 안전성과 표현력을 모두 잃게 된다.
// Stamp 인스턴스만 취급하려는 의도의 raw type
private final Collection stamps = ...;
// 실수로 동전을 넣는다.
stamps.add(new Coin(...));   // "unchecked call" 경고를 내뱉는다.

오류 없이 컴파일되고 실행된다.

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

컬렉션에서 이 동전을 다시 꺼내기 전에는 오류를 알아채지 못한다.

 

  • 로 타입은 제네릭 등장 이전의 자바 코드와의 호환성 때문에 있는 것이다.

 

[How]

// 매개변수화된 컬렉션 타입 - 타입 안전성 확보!
private final Collection<Stamp> stamps = ...;

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

 

List<String> strings = List.of("1", "2");

List list1 = strings;          // 가능
List<Object> list2 = strings;  // 불가능
  • List 같은 로 타입은 사용해서는 안 되나, List<Object>처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮다.
    • List는 제네릭 타입에서 완전히 발을 뺀 것이고, List<Object>는 모든 타입을 허용한다는 의사를 컴파일러에게 전달한 것이다.
    • List<String>: List를 받는 곳에는 넘길 수 있지만, List<Object>를 받는 곳에는 넘길 수 없다.
    • 제네릭의 하위 타입 규칙 때문인데, List<String>은 로 타입인 List의 하위 타입이지만 List<Object>의 하위 타입은 아니다. (아이템28)
  • 원소의 타입을 몰라도 되는 로 타입을 쓰고 싶을 때는 비한정적 와일드카드 타입(unbounded wildcard type)을 대신 사용하자.
    • 비한정적 와일드카드 타입인 Set<?>와 로 타입인 Set의 차이는 무엇일까?
    • 로 타입 컬렉션에는 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽다.
    • 반면, Collection<?>에는 null 외에는 어떤 원소도 넣을 수 없다. 어쨌든 컬렉션의 타입 불변식을 훼손하지 못하게 막는다.
    • 이러한 제약을 받아들일 수 없다면 제네릭 메서드(아이템30)나 한정적 와일드카드 타입(아이템31)을 사용하면 된다.

 

예외

  1. class 리터럴에는 로 타입을 써야 한다.
    • 자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못하게 했다(배열과 기본 타입은 허용한다)
    • 예를 들어 List.class, String[].class, int.class는 허용하고 List<String>.class, List<?>.class는 허용하지 않는다.
  2. 런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다. 그리고 로 타입이든 비한정적 와일드카드 타입이든 instanceof는 완전히 똑같이 동작한다.
if (o instanceof Set) {       // 로 타입
    Set<?> s = (Set<?>) o;    // 와일드카드 타입
    ...
}

o의 타입이 Set임을 확인한 다음 와일드카드 타입인 Set<?>로 형변환해야 한다(로 타입인 Set이 아니다). 이는 형변환(checked cast)이므로 컴파일러 경고가 뜨지 않는다.