ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 아이템26. 로 타입은 사용하지 말라
    책/이펙티브자바 2021. 8. 24. 11:52

    제네릭

    클래스와 인터페이스 선언에 타입 매개변수가 쓰이면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라 한다.

    제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입(generic type) 이라 한다.

     

    각각의 제네릭 타입은 일련의 매개변수화 타입(parameterized type) 을 정의한다.

    예를들어, List은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입이다.

    List, 여기서 String 이 정규(formal) 타입 매개변수 E 에 해당하는 실제(actual) 타입 매개변수이다.


    로 타입의 단점

    제네릭 타입을 하나 정의하면 그에 딸린 로 타입(raw type) 도 함께 정의된다.

    로 타입이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다.

    예를들어, List 의 로 타입은 List 이다.

    로 타입은 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작하는데, 제네릭이 도래하기 전 코드와 호환되도록 하기 위한 궁여지책이라 할 수 있다.

    // Collection 의 로 타입 
    private final Collection stampes = ...;
    
    // 실수로 Coin을 넣는다.
    stamps.add(new Coin()); // "unchecked call" 

    이런식으로 로 타입으로 선언하게 되면 Stamp 대신 Coin 을 넣어도 아무 오류 없이 컴파일되고 실행된다.

    // 반복자의 로 타입
    for (Iterator i = stamps.iterator(); i.hasNext(); ) {
        Stamp stamp = (Stamp) i.next(); // ClassCastException 
        stamp.cancel();
    }

    EJ 책에서 계속 오류는 가능한 한 발생 즉시, 이상적으로는 컴파일할 때 발견하는 것이 좋다 라고 한다.

    위의 예시에서는 오류가 발생하고 한참 뒤인 런타임에야 알아챌 수 있는데, 이렇게 되면 런타임에 문제를 겪는 코드와 원인을 제공한 코드가 물리적으로 떨어져있을 가능성이 크다.

    ClassCastException이 발생하면 찾기 위해 stamps 에 동전을 넣는 부분을 전부 봐야할 수도 있다.


    제네릭의 장점

    제네릭을 활용하면 위와 같은 문제를 해결할 수 있다.

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

    이렇게 선언하면 컴파일러는 Stamp 인스턴스만 넣어야함을 인지하게 된다.

    이제 stamps에 엉뚱한 타입의 인스턴스를 넣으려 하면 컴파일 오류가 발생하며 무엇이 잘못인지 알려준다.

    error: incompatiable types: Coin cannot be converted
    stamps.add(new Coin());

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


    쓰면 안되는 로타입을 만들어 놓은 이유

    앞에서도 언급하듯, 로 타입을 쓰는 걸 언어 차원에서 막아 놓지는 않았지만 절대로 써서는 안 된다.

    로 타입을 쓰면 제네릭이 안겨주는 안전성과 표현력을 모두 잃게 된다.

     

    그러면 왜 써서는 안되는 로 타입을 애초에 만들어 놓은것일까 ?

    그것은 자바가 제네릭을 받아들이기 전에 제네릭 없이 짠 코드가 이미 많이 쓰옇기에 호환성 때문이다.


    List<Object> vs List

    List 같은 로 타입은 사용해서는 안 되나, List<Object> 처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮다.

    List 는 제네릭 타입에서 완전히 발을 뺀 것이고, List<Object> 는 모든 타입을 허용한다는 의사를 컴파일러에 명확히 전달한 것이다.

    매개변수로 List 를 받는 메서드에 List<String> 은 넘길 수 있지만, List<Object>

    를 받는 메서드에는 넘길 수 없다.

     

    이는 제네릭의 하위 타입 규칙 때문인데, List<String>은 로 타입인 List의 하위 타입이지만, List<Object>의 하위타입은 아닌 것이다.

    그 결과, List<Object>같은 매개변수화 타입을 사용할 때와 달리 List 같은 로 타입을 사용하면 타입 안전성을 잃게된다.

    // 런타임에 실패 - 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);
    }

    이 코드로 컴파일은 되지만 strings.get(0); 을 할때 ClassCastException 을 던진다.

    Integer → String 으로 변환 시도했기 때문이다.

    이러한 형변환은 보통 IDE에서 잡아줘서 실패하지 않지만 이 경우엔 컴파일러의 경고를 무시해서 그런것이다.

    // 컴파일에 실패 
    private static void unsafeAdd(List<Object> list, Object o) {
      list.add(o);
    }

    위의 unsafeAdd 메서드에서 로타입 List를 List<Object>

    로 바꾸면 오류 메시지가 출력되며 애초에 컴파일조차 되지 않는다.


    비한정적 와일드카드 타입

    // 모르는 타입의 원소도 받는 로타입을 사용한 잘못된 예
    static int numElementsInCommon(Set s1, Set s2) {
        int result = 0;
        for (Object o : s1)
            if (s2.contains(o1))
                result++;
        return result;
    }

    이 메서드는 동작은 하지만 로 타입을 사용해 안전하지 않다.

    따라서 비한정적 와일드 카드 타입 (unbounded wildcard type) 을 대신 사용하는게 좋다.

    제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않다면 물음표 (?) 를 사용하자.

    예를들어, 제네릭 타입인 Set의 비한정적 와일드 카드 타입은 Set<?> 이다.

    이것이 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 Set 타입이다.

    // 비한정적 와일드카드 타입 사용
    static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }

    그러면 Set<?> 와 로타입 Set 의 차이는 무엇일까 ?

    간단히 말하자면 와일드카드 타입은 안전하고, 로 타입은 안전하지 않다.

    로 타입 컬렉션에는 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽다.

    반면, Collection<?> 에는 (null 외에는) 어떤 원소도 넣을 수 없다.

     

    구체적으로는, null 외의 어떤 원소도 Collection<?>에 넣지 못하게 했으며 컬렉션에서 꺼낼 수있는 객체의 타입도 전혀 알 수 없게 했다.


    로타입을 쓰는 경우

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

     

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

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

    예를들어, List.class, String[].class, int.class 는 허용하고 List.class, List<?>.class는 허용하지 않는다.

     

    instanceof 연산자와 관련이 있다.

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

    그리고 로타입이든 비한정적와일드카드 타입이든 instanceof에는 똑같이 동작한다.

    // 로타입을 써도 좋은 예 - instanceof
    if (o instanceof Set) {
        Set<?> s = (Set<?>)o;
        ...
    }

    용어정리


    결론

    • 로 타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안 된다.
    • Set는 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입이고, Set<?>는 모종의 타입 객체만 저장할 수 있는 와일드카드 타입이다.
    • Set, Set<?> 는 안전하지만, 로타입인 Set은 안전하지 않다.
킹수빈닷컴