이펙티브 자바 - item 26. 로 타입은 사용하지 말라

item 26. 로 타입은 사용하지 말라

이펙티브 자바 - item 26. 로 타입은 사용하지 말라

용어정리

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

로 타입과 단점

클래스와 인터페이스 선언에 타입 매개변수(type parameter)가 쓰이면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라고 한다. 그리고 이 둘을 통틀어 제네릭 타입이라고 한다.

로 타입 이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않은 경우를 의미한다. 예를 들어 List의 로 타입은 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");
        ^

로타입을 사용할 수 있는 몇 가지 예외

  1. class 리터럴에는 로 타입을 써야한다.

자바 명세에는 class 리터럴에 매개변수화 타입을 사용하지 못하게 했다. 예를들어 List.class, String[].class, int.class 는 허용하나 List<String>.classList<?>.class 는 허용하지 않는다.

  1. instanceof 연산자

런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입을 적용할 수 없다. 또한 로 타입이든 비한정적 와일드카드 타입이든 instanceof는 완전히 똑같이 동작한다.

따라서 비한정적 와일드 카드 타입의 꺽쇠 괄화와 물음표는 역할 없이 코드만 지저분하게 만드므로 차라리 로 타입을 쓰는게 깔끔하다.

아래는 제네릭 타입에 instanceof를 사용하는 올바른 예이다.

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

정리

  • 로 타입을 사용하면 런타임에 예외가 발생할 수 있으니 사용하지 말자.
  • 로 타입은 제네릭이 도입되기 이전 코드와의 호환성을 위해 제공될 뿐이다.
  • Set는 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입이고, Set<?>는 모종의 타입 객체만 저장할 수 있는 와일드카드 타입이다.
  • 이들의 로 타입인 Set은 제네릭 타입 시스템에 속하지 않는다.
  • Set와 Set<?>은 안전하지만, 로 타입인 Set은 안전하지 않다.