스터디/이펙티브 자바

[Effective Java] Item26. 로(raw) 타입은 사용하지 말라

📝 작성 : 2022.06.11  ⏱ 수정 : 
728x90

로 타입(raw type)을 알기전에 제네릭을 알아야 합니다.

제네릭

클래스와 인터페이스 선언에 타입 매개변수(type parameter)가 쓰이면, 이를 제네릭 클래스, 제네릭 인터페이스라 합니다.
List인터페이스는 원소의 타입을 나타내는 타입 매개변수 E를 받습니다. 그래서 List인터페이스의 완전한 이름은 List<E>입니다.
이와 같은 제네릭 클래스, 제네릭 인터페이스를 통틀어 제네릭 타입(generic type)이라 합니다.

로 타입(raw type)

제네릭 타입에서 타입 매개변수를 사용하지 않은 타입을 말합니다. List<E>의 로 타입은 List입니다.
로 타입은 타입선언에서 제네릭 타입 정보가 전부 지워진 것 처럼 동작합니다.
jdk5.0 이전의 코드와 (제네릭이 나오기 이전) 호환되도록 하기 위한 궁여지책입니다.

로 타입의 위험성

public class Application {
    public static void main(String[] args) {
        List stamps = new ArrayList();
        stamps.add(new Stamp());
        stamps.add(new Coin());

        for (Object o : stamps) {
            Stamp stamp = (Stamp) o;
            System.out.println("stamp = " + stamp);
        }
    }

    private static class Stamp {}
    private static class Coin{}
}

코드를 작성하고 컴파일 할 때까지만 해도 문제가 없었는데 실행 시 ClassCastException이 발생합니다.

반면, stamps의 타입을 List<Stamp>로 하면 컴파일 오류가 발생합니다. 따라서 stamps의 원소는 Stamp타입(또는 그 하위타입)임이 보장됩니다.
따라서 로 타입의 사용을 막지는 않았지만 절대로 써서는 안됩니다. 로 타입을 쓰면 제네릭이 주는 안전성, 표현력을 모두 잃게 됩니다. (사용을 막지 않은 이유는 단순히 기존 코드와의 호환성 때문입니다.)

List<Object>는 써도 되는가?

List<Object>는 로 타입인 List와 별 차이 없는 것처럼 보입니다. 하지만 List<Object>는 사용해도 괜찮습니다.
간단하게 로 타입인 List는 제네릭 타입에서 완전히 발을 뺀 것이고, List<Object>는 모든 타입을 혀용한다는 의미입니다.
List를 매개변수로 받는 메서드에 List<String>을 넘길 수 는 있지만 List<Object>를 매개변수로 받는 메서드에는 넘길 수 없습니다.
이는 제네릭의 하위 타입 규칙때문입니다.

public static void main(String[] args) {
    List<String> strings = new ArrayList<>();
    unsafeAdd(strings, Integer.valueOf(24));
    String s = strings.get(0);
}

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

위의 코드는 컴파일은 되지만 실행시에 ClassCastException이 발생합니다. 이제 List를 List<Object>로 바꾸면 컴파일 오류가 발생합니다.

비한정적 와일드카드 타입(unbounded wildcard type)

로 타입은 쓰면 안되고, 타입 매개변수가 무엇인지 신경쓰고 싶지는 않을 때 사용할 수 있는 게 ?입니다.
로 타입인 List 대신 List<?>로 사용하는 것 입니다. 이렇게 작성하는 것 만으로 타입의 안전성을 깨지 않을 수 있습니다.
로 타입 List에는 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽습니다. 반면, List<?>에는 null이외에는 어떤 원소도 넣을 수 없습니다.

public static void main(String[] args) {
    List<?> list = new ArrayList<>();
    list.add("와일드카드");
}

컴파일오류 발생
java: incompatible types: java.lang.String cannot be converted to capture#1 of ? 

컬렉션의 타입 불변식을 훼손하지 못하게 막았습니다. (null외의) 어떤 원소도 넣지 못하게 했으며 꺼낼 수 있는 객체의 타입도 전혀 알 수 없게 되었습니다. 이러한 제약을 받아들일 수 없다면 제네릭 메서드나 한정적 와일드카드를 사용면 됩니다.

예외

로 타입을 쓰지 말라는 규칙에도 몇가지 예외가 있습니다.

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

자바 명세는 class 리터럴에 매개변수화 타입을 허용하지 않습니다.(배열과 기본타입은 허용)
예를 들어 List.class, String[].class, int.class는 가능하지만 List<String>.class는 허용하지 않습니다.

2. instanceof을 사용할 때는 로 타입을 사용합니다.

런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없습니다.
그리고 로 타입이든 비한정적 와일드 카드 타입이든 instanceof는 동일하게 동작합니다. 따라서 좀 더 깔끔한 로 타입을 사용합니다.

if (o instanceof List) {
    List<?> list = (List<?>) o;
}

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

반응형