아이템 26. 로 타입은 사용하지 말라. - ksw6169/effective-java GitHub Wiki

용어

제네릭 타입

  • 클래스나 인터페이스 선언에 타입 매개변수가 쓰이면 이를 제네릭 클래스 혹은 제네릭 인터페이스라 한다.
  • 제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입이라 한다.
  • 예컨대 List 인터페이스는 원소의 타입을 나타내는 타입 매개변수 E를 받는다.
public interface List<E> extends Collection<E> {

    ...    

    boolean add(E e);
    
    ...
}

매개변수화 타입(parameterized type)

  • 각각의 제네릭 타입은 일련의 매개변수화 타입을 정의한다.
  • 먼저 클래스(혹은 인터페이스) 이름이 나오고, 꺽쇠 괄호 안에 실제 타입 매개변수들을 나열한다.
  • 예컨대 List<String> 은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입이다.
  • 여기서 String이 정규(format) 타입 매개변수 E에 해당하는 실제(actual) 타입 매개변수다.
/**
 * List<String> : 매개변수화 타입(원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입) 
 * String       : 실제 타입 매개변수
 */
List<String> list = new ArrayList<>();

로 타입(raw type)

  • 로 타입이란 타입 매개변수가 없는 제네릭 타입을 말한다.
  • 예를 들어 List<E> 타입의 로 타입은 List다.
  • 제네릭 타입을 하나 정의하면 그에 딸린 로 타입도 함께 정의된다.
  • 로 타입은 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작하는데, 제네릭 도입 이전의 코드와 호환되도록 하기 위한 궁여지책이라 할 수 있다.

정리

<String>     : 타입 인자 (=실제 타입 매개변수)
<T>          : 타입 매개변수
List<String> : 매개변수화 타입
List         : 로 타입

로 타입의 문제점

타입을 제어할 능력이 없다. (=타입 안전성을 확보하지 못한다.)

  • 제네릭을 지원하기 전에는 컬렉션을 다음과 같이 선언했다. 자바 9에서도 동작하지만 좋은 예가 아니다.
/**
 * Stamps의 인스턴스만 취급하려고 만든 Collection
 * 하지만 실제 타입 매개변수를 사용하지 않았으니 타입을 제어할 능력이 없다.
 */
private final Collection stamps = new ArrayList();
  • 위 코드를 사용하면 실수로 Stamp 대신 Coin을 넣어도 아무 오류 없이 컴파일되고 실행된다.
  • 대신 컴파일러가 모호한 경고 메시지를 보여주긴 한다.
// 실수로 동전을 넣는다.
stamps.add(new Coin("BTC", 50000000));  // "unchecked call" 경고를 내뱉는다.
  • 컬렉션에서 이 동전을 다시 꺼내기 전에는 오류를 알아채지 못한다.
for (Iterator i = stamps.iterator(); i.hasNext();) {
    Stamp stamp = (Stamp) i.next();  // ClassCastException을 던진다.
    stamp.cancel();
}
  • 이를 해결하기 위해서는 매개변수화된 컬렉션 타입을 사용하여 타입 안전성을 확보해야 한다.
  • 아래와 같이 선언하면 stamps에 Stamp의 인스턴스만 넣어야 함을 컴파일러가 인지하게 된다.
private final Collection<Stamp> stamps = ...;
  • stamps에 엉뚱한 타입의 인스턴스를 넣으려 하면 컴파일 오류가 발생하며 무엇이 잘못됐는지를 정확히 알려준다.
  • 컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환을 추가하여 절대 실패하지 않음을 보장한다.
Test.java:9: error: incompatible types: Coin cannot be converted to Stamp
stamps.add(new Coin());
               ^

로 타입은 절대 써서는 안된다.

  • 로 타입은 제네릭 이전 코드와의 호환성을 위해 남겨놓은 것일 뿐 절대 사용해서는 안된다.
  • 로 타입을 쓰면 제네릭이 안겨주는 안전성과 표현력을 모두 잃게 되기 때문이다.

List 같은 로 타입은 안되지만, List<Object>와 같은 매개변수화 타입은 괜찮다.

  • 매개변수로 List를 받는 메소드에 List<String>을 넘길 수 있지만, List<Object> 를 받는 메소드에는 넘길 수 없다.
  • 이는 제네릭의 하위 타입 규칙 때문으로 List<String>List의 하위 타입이지만, List<Object> 의 하위 타입은 아니기 때문이다. (=불공변)
  • 그 결과 List<Object> 같은 매개변수화 타입을 사용할 때와 달리 List 같은 로 타입을 사용하면 타입 안전성을 잃게 된다. (예를 들어, 로 타입 매개변수인 List 를 이용해 List<String> 을 받을 경우, List 에는 String 뿐만 아니라 다른 타입의 데이터도 넣을 수 있게 된다. 따라서 제네릭에서 보장하는 타입 안전성을 잃게 된다.)

예제 1 - 로 타입 매개변수를 이용해 제네릭에서 보장하는 타입 안전성을 잃음

// 런타임에 실패한다. (unsafeAdd 메소드가 로 타입을 사용)
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);
}
  • 이 코드는 컴파일은 되지만 로 타입인 List를 사용하여 다음과 같은 경고가 발생한다.
Test.java:10: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
    list.add(o);
            ^
  • 이 프로그램을 그대로 실행하면 strings.get(0)의 결과를 형변환하려 할 때 ClassCastException을 던진다. (Integer를 String으로 변환하려 시도하였기 때문이다.)
  • 다음은 로 타입인 List를 매개변수화 타입인 List<Object>로 바꾼 다음에 다시 컴파일 해보자. 이번에는 다음 오류 메시지가 출력되며 컴파일조차 되지 않는다.
Test.java:5: error: incompatible types: List<String> cannot be converted to List<Object>
    unsafeAdd(strings, Integer.valueOf(42));
        ^
  • 결론은 로 타입을 사용하지말고 List<Object> 와 같은 매개변수화 타입이라도 이용해서 컴파일 타임에 타입 안전성을 지킬 수 있도록 하는 것이 좋다.

예제 2 - 로 타입 매개변수를 이용해 제네릭에서 보장하는 타입 안전성을 잃음

  • 예제 1과 마찬가지로 동작은 하지만 로 타입을 사용해 안전하지 않다.
static int numElementsInCommon(Set s1, Set s2) {
    int result = 0;
    for (Object o1 : s1)
        if (s2.contains(o1))
            result++;
    return result;
}
  • 만약 원소의 타입을 몰라도 되는 로 타입이 쓰고 싶은 것이라면 비한정적 와일드 타입(unbounded wildcard type) 을 대신 사용하는 게 좋다.
static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }

비한정적 와일드카드 타입과 로 타입의 차이

  • 와일드카드 타입은 안전하고, 로 타입은 안전하지 않다.
  • 로 타입 컬렉션에는 아무 원소나 넣을 수 있으니 타입 불변식을 해치기 쉽다.
  • 반면에 Collection<?> 에는 (null 외에는) 어떤 원소도 넣을 수 없다.
  • 즉, 컬렉션의 타입 불변식을 훼손하지 못하게 막은 것이다.
  • 구체적으로는 null 외의 어떤 원소도 Collection<?> 에 넣지 못하게 했으며 컬렉션에서 꺼낼 수 있는 객체의 타입도 전혀 알 수 없게 했다.
  • 만약 이러한 제약을 받아들일 수 없다면 제네릭 메소드나 한정적 와일드카드 타입을 사용하면 된다.

로 타입을 써야하는 상황도 있다.

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

  • 자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못하게 했다. (배열과 기본 타입은 허용한다.)
// 허용
List.class, String[].class, int.class

// 허용하지 않음
List<String>.class, List<?>.class

2. instanceof 연산자를 사용할 때는 로 타입을 써야 한다.

  • 런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외에 매개변수화 타입에는 적용할 수 없다.
  • 그리고 로 타입이든 비한정적 와일드카드 타입이든 instanceof는 완전히 똑같이 동작한다.
  • 비한정적 와일드카드 타입의 괄호와 물음표는 아무런 역할 없이 코드만 지저분하게 만드므로 차라리 로 타입을 쓰는 편이 깔끔하다.
  • 다음은 제네릭 타입에 instanceof를 사용하는 올바른 예다.
if (o instanceof Set) {      // 로 타입
    Set<?> s = (Set<?>) o;   // 와일드카드 타입
    ...
}

참고 자료

  • Effective Java 3/E
⚠️ **GitHub.com Fallback** ⚠️