ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 26: Don’t use raw types
    독서/Effective Java 2021. 12. 28. 02:00

    Generic 장에서 사용할 용어들 정리

    한글 용어 영문 용어 아이템
    매개변수화 타입 parameterized type List<String> Item 26
    실제 타입 매개변수 actual type parameter String Item 26
    제네릭 타입 generic type List<E> Item 26, 29
    정규 타입 매개변수 formal type parameter E Item 26
    비한정적 와일드카드 타입 unbounded wildcard type List<?> Item 26
    로 타입 raw type List Item 26
    한정적 타입 매개변수 bounded type parameter <E extends Number> Item 29
    재귀적 타입 한정 recursive type bound <T extends Comparable<T>> Item 30
    한정적 와일드카드 타입 bounded wildcard type List<? extends Number> Item 31
    제네릭 메서드 generic method static <E> List<E> asList(E[] a) Item 30
    타입 토큰 type token String.class Item 33

     

    로 타입은 사용하지 말라

     

    제네릭 타입

    클래스와 인터페이스 선언에 type parameter가 쓰일 경우 우리는 이를 Generic Class/Generic Interface라 부르고, 통합적으로는 Generic Type이라 부른다.

    public interface List<E> extends Collection<E> {
      int size();
      // ...
    }
    // 실제 사용 예
    List<String> strings = new ArrayList<>();

    strings는 원소의 타입이 String인 리스트를 뜻하는 변수이다. 정규 타입 E를 "String" actual type parameter로 사용했다.

     

    로 타입

    Raw type이란 Generic Type에서 type parameter(위에서의 E, String)를 사용하지 않았을 경우 해당 타입을 의미한다.

    Ex) List의 Raw Type은 List다. Generic을 지원하기 이전에는 아래 코드와 같이 type parameter를 사용하지 않고 Raw Type으로 선언해 사용했다. Generic 지원 후에도 계속해서 Raw Type을 지원하는 이유는 기존 버전, 소스 코드들과의 호환성 때문이다.

    public class Coin {
      @Override
      public String toString(){
        return "coin";
      }
    }
    
    public class Stamp {
      @Override
      public String toString(){
        return "stamp";
      }
    
      public void cancel(){
        // do Something
      }
    }
    
    @Test
    void rawTypeTest(){
      final Collection stamps = new ArrayList();
      stamps.add(new Coin());
      stamps.add(new Stamp());
      log.info("stamps={}", stamps);
    
      // 문제가 되는 부분, 컴파일에는 성공하지만 런타임에 에러 발생
      assertThatThrownBy(() -> stamps.forEach(s -> ((Stamp) s).cancel())).isInstanceOf(ClassCastException.class);
    }
    
    // 결과
    stamps=[coin, stamp]

    해당 코드가 문제가 되는 부분은 실제 stamps를 사용하는 부분에서 Stamp임을 기대해 Stamp의 cancel()을 호출하도록 이루어졌지만, 실제로는 Coin과 Stamp 모두 들어있기 때문에 Coin을 읽어오는 부분에서 ClassCastException이 일어났다.

    해당 부분을 수정하는 방법은 다음과 같다.

    @Test
    void rawTypeTest(){
      final Collection<Stamp> stamps = new ArrayList();
      // Actual Type Parameter 로 Stamp 를 지정했기 때문에 Coin은 들어갈 수 없음.
      // stamps.add(new Coin());
      stamps.add(new Stamp());
      log.info("stamps={}", stamps);
    
      // 문제가 해결된 부분
      stamps.forEach(s -> s.cancel());
    }

    Raw Type을 아직 지원한다고 해도 Generic이 주는 안정성, 표현력을 모두 잃는 방편이니 사용하지 말자.

     

    List에서 Raw Type을 사용할 경우

    List를 Raw Type으로 사용하는 것은 위험하다고 앞에서도 언급했지만, List<Object>처럼 사용하는 것은 허용한다. 모든 타입을 허용한다는 의사를 컴파일러에게 명확하게 전달했기에 . List와 List의 차이는 List는 제네릭 타입과 무관하다.

    @Test
    void listRawTypeTest(){
      List<String> strings = new ArrayList<>();
      unsafeAdd(strings, Integer.valueOf(42));
      assertThatThrownBy(() -> {
        String s = strings.get(0);
      }).isInstanceOf(ClassCastException.class);
    }
    
    private static void unsafeAdd(List list, Object o){
      list.add(o);
    }

    그렇다면 String의 상위 타입인 Object를 Type parameter로 사용해 unsafeAdd 메소드를 작성해보면 어떨까?

    Type mismatch로 컴파일 단계에서 오류가 발생한다.

    String은 Object의 하위 타입인 것과 관계 없이 List<String>은 로 타입인 List의 하위 타입이지만 List<Object>의 하위 타입은 아니기 때문이다. 

    // 컴파일조차 불가능하다.
    @Test
    void listRawTypeTest(){
      List<String> strings = new ArrayList<>();
      unsafeAdd(strings, Integer.valueOf(42));
      assertThatThrownBy(() -> {
        String s = strings.get(0);
      }).isInstanceOf(ClassCastException.class);
    }
    
    private static void unsafeAdd(List<Object> list, Object o){
      list.add(o);
    }

     

     

    Type Parameter가 정해지지 않은 경우 방편

    직전의 예시처럼 Type Parameter가 정해져있지 않을 때 가장 쉽게 생각할 수 있는 방법은 Raw Type을 사용하는 것이다.

    static int numElementsInCommon(Set s1, Set s2){
      int result = 0;
      for(Object o1 : s1){
        if(s2.contains(o1))
        result++;
      }
      return result;
    }

    해당 메소드는 Raw Type을 사용했기 때문에 안전하지 않다. 따라서 Unbounded wildcard type을 대신 사용하는게 좋다. 제네릭 타입을 사용하고 싶지만 실제 매개변수가 무엇인지 신경 쓰고 싶지 않다면 물음표(?)를 사용하자. Unbounded wildcard type는 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 타입이다.
    또한 Collection<?>에는 null외에는 어떤 원소도 넣을 수 없다. 다른 원소를 넣으려하면 컴파일할 때 오류를 일으킨다.

    static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }

     

    추가로 알아보면 좋을 것

    https://docs.oracle.com/javase/tutorial/java/generics/types.html

    Class T 타입을 사용해서 위의 예시를 안전하게 변경할 수 있다.

    @Test
    void listRawTypeTest(){
      List<String> strings = new ArrayList<>();
      unsafeAdd(strings, Integer.valueOf(42));
      safeAdd(strings, "test");
      assertThatThrownBy(() -> {
        String s = strings.get(0);
      }).isInstanceOf(ClassCastException.class);
      assertThat(strings.get(1)).isEqualTo("test");
    }
    
    private static void unsafeAdd(List list, Object o){
      list.add(o);
    }
    
    private static <T> void safeAdd(List<T> list, T o){
      list.add(o);
    }

     

    Raw Type을 사용해야 하는 경우

    class 리터럴에는 로 타입을 사용해야한다. List.class, String[].class, int.class는 허용되지만, List<String>.class, List<?>.class는 허용되지 않는다.

    런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 unbounded wildcard type 이외의 parameterized type에는 적용할 수 없다. Raw type이든 unbounded wildcard type이든 instanceof는 똑같이 동작한다.

    (Unbounded wildcard type의 <?>는 아무런 역할을 하지 않는다.)

    if( o instanceof Set) {     // Raw Type
      Set<?> s = (Set<?>) o;    // Wildcard Type
    }

     

    정리

    Raw Type을 사용할 경우 Runtime에 예외 발생 가능성이 있으니 사용하지 말자.

     

    댓글

Designed by Tistory.