본문 바로가기

Reading Record/이펙티브자바

이펙트브자바 제네릭 (item 26,27,28)

제네릭 아이템26,27,28

Item 26 로타입은 사용하지 마라


로타입은 왜 사용하면 안될까?

로타입은 List list = new ArrayList(); 형태로 사용하는 방식이다.
이번장에서 왜 로타입을 사용하면 안되는지, 그렇다면 로타입을 사용하고 싶을땐 어떤 방법으로 제네릭을 사용해야하는지 알아보자.

 

로타입은 뭐든지 다 들어간다.

         @Test
             @DisplayName("로타입은 뭐든지 다 들어간다.")
             void rowType() {
                 List list = new ArrayList();
                 list.add("문자열");
                 list.add(1);

                 String text = (String) list.get(0);
                 Integer number = (Integer) list.get(1);

                 assertThat(text).isEqualTo("문자열");
                 assertThat(number).isEqualTo(1);
             }

로타입은 문자열,숫자 가리지 않고 다 넣을 수 있다. 위 코드에서도 문제없이 동작할 것이다.
"뭐든지 다 넣을 수 있으면 좋은거 아니야?" 라고 생각할 수 있지만 전혀 아니다.
리스트에서 나올 값을 항상 올바른 타입으로 캐스팅해줘야 한다는 점은 사용자 입장에서 굉장히 부담스러울 것이다.
로타입을 사용하는 것은 제네릭이 자랑하는 타입안전성표현력을 모두 잃게 된다.

      @Test
      @DisplayName("로타입은 뭐든지 다 들어간다.")
      void rowType2() {
          List<String> stringList = new ArrayList();
          unsafeAdd(stringList,new Integer(1));

          assertThatThrownBy(()->stringList.get(0))
                  .isInstanceOf(ClassCastException.class);

      }

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

이런 경우도 있을 수 있다. 매개형식타입으로 List stringList로 잘 선언했지만,
로타입 리스트와 오브젝트를 받아 add시켜주는 함수를 이용해 add한다면 컴파일은 되지만 get을 할때
ClassCastException이 떨어질 것이다.

로타입이 쓰고 싶을땐 어떻게 하지?

분명 로타입을 사용하고 싶은 요구사항도 있을 것이다.
로타입의 두 Set끼리 비교하여 같은것이 몇개 포함되었는지 비교하는 함수를 만든다면, 그 함수는 어떤 매개변수를 가지던지 상관하지 않고 싶을 것이다.

     private int count(Set destination, Set source) {
         int result = 0;
         for (Object o : source) {
             if (destination.contains(o)) {
                 result++;
             }
         }
         return result;
      }

만약 로타입을 사용하지 않는다면 필요한 타입들에 대한 함수들을 모두 만들어야 할 것이다.

      private int count(Set<Object> destination, Set<String> source) {
          ....
          return result;
      }

      private int count(Set<Object> destination, Set<Integer> source) {
          ....
          return result;
       }

 

이럴 경우에는 비한정적와일드카드타입을 활용해 해결한다. 비한정적 와일드카드 타입은 어떤 타입이든 상관없이 받을 수 있지만 타입의 안정성을 보장해준다.
또 로타입과 달리 비한정적 와일드카드 타입은 null외에 어떤 원소도 넣을 수 없어 불벽식을 보장한다.
이런 요구사항에서는 비한정적 와일드카드 타입을 쓰도록 하자.

     private int count(Set<?> destination, Set<?> source) {
         ....
         return result;
     }
 주의사항.   
 List<Object>와 List<String>은 공변관계가 아니다.   
 Object가 String의 상위 클래스라 해서 매개변수타입에서도 그런 규칙을 따르는 것은 아니다.  
 이 관계에서는 서로 다른 타입이라고 생각하면 된다.        

 

로타입을 써도 좋은 곳

  1. 클래스 리터럴에는 로타입을 사용해도 좋다.

클래스 리터럴에 매개변수화 (List) 타입을 사용하지 못하기 때문이다.
List.class,String[].class는 가능, List.class는 불가능!

  1. instanceof에도 사용해도 좋다.

런타임에는 제네릭 타입 정보가 지워진다.
그래서 instanceof에서 리스트타입인지 알고싶다면, 로타입이든 비한정적 와일드 타입이든 둘중 하나를 사용해라.

 

런타임에는 제네릭 타입정보가 사라진다?

제네릭 타입정보는 컴파일 타임에 검사하고 런타임에는 타입정보가 사라진다.

위 사진은 ArrayList에서 get하는 부분의 코드다.
위와같이 제네릭 타입으로 캐스팅하여 나오는 코드는 컴파일 타임에 자동으로 제네릭 타입으로 형변화하도록 코드를 심어넣은것이다.

Item 27 비검사 경고를 제거하라

제네릭을 사용하기 시작하면 수많은 컴파일러 경고들을 보게된다.

이번 장은 제네릭 사용시 제거할 수 있는 경고들을 알아보자.

제거할 수 있는 경고는 제거하자!


인텔리제이는 컴파일 과정을 대신해주기 때문에 **컴파일 로그 **를 보거나 할 일이 없다.

대신 인텔리제이는 컴파일 경고가 날 수 있는 부분을 코드상에서 미리 알려준다.

List<String> strings = new ArrayList(); 

위 코드는 타입추론이 힘들다.

<>다이아몬드를 추가해주면 타입추론이 가능해 컴파일단계에서 경고를 삭제할 수 있다.

제거할 수 없는 경고는 숨기자


타입이 안전하다고 확신이 들면 제거할 수 있는 경고들은 숨기자!

private E[] = new Object[n];

public void push(E element){
  element[i] = e;
}

public E pop(){
  return (E) elements[i];
}

위 약식 코드를 보면 퍼블릭 인터페이스는 모두 컴파일 타임에 정해진 타입 검사를 마치고 런타임에는 타입검사가 없어질 것이다.

위 코드는 컴파일 타임에서 pop함수는 Object를 E로 캐스팅해 넘기는 부분에서 ClassCastException이 발생할 수 있으므로 경고할 것이다.

하지만 Object안에 값을 넣는 인터페이스는 push 하나뿐이므로 Object는 언제나 E타입의 동일한 값만 들어가므로 타입 safe하다.

그러므로 pop에서 발생할 수 있는 경고를 제거해주는 것이좋다.

public E pop(){
  @SuppressWarnings("unchecked")
    E e=  (E) elements[i];
  return e;
}

SuppressWarnings 어노테이션은 경고를 감춰준다.

해당 어노테이션은 선언에만 달 수 있으므로 함수선언, 변수선언 등에 달 수 있다. (return에는 달 수 없다.)

그래서 위와같이 E를 꺼내는 부분을 선언으로 바꿔서 e를 리턴하므로 최대한 좁은 범위로 어노테이션을 달아놓은 것이다.

  • 실제 위 코드는 함수에 어노테이션을 붙이는 것이 좋지만, 최대한 좁은 범위를 어필하기 위해 저런 식으로 만들었다.

만약 pop코드에 ClassCastExpection이 터질 수 있는 코드들이 더 추가가 된다면, 함수부에 달린 어노테이션은 우리가 안전하다고 인지하지 못한 경고까지 감출 것이다.

그러므로 최대한 좁은 범위로 SuppressWarnings 어노테이션을 달자!

Item 28 배열보다는 리스트를 사용하라


배열보다 리스트를 사용할 경우 많은 이점을 누릴 수 있다.

써야하는 이유를 알아보자.

제네릭은 런타임에 안전하다.

    • 배열은 공변으로 존재하고, 제네릭은 불공변으로 존재한다.

공변이란?

Object[] objectArr = new Object[3];
Long[] longArr = new Long[3];

위 배열관계에서 Long배열Object배열의 하위 타입이 된다는 뜻이다.

    • 그래서 뭐가 문제?

      위 관계는 런타임동안 이런 문제를 일으킨다.

          Object[] objectArr = new Long[3];
        objectArr[0] = "타입때문에 넣을 수 없다." -- > 런타임 에러 
      
        List<Objec> objectList = new ArrayList<String>(); -->컴파일 에러
      

      하지만 리스트를 사용한다면 컴파일 타임에 문제를 발견할 수 있다.

      당연히 모든 프로그래머는 컴파일 타임에 문제를 발견하고 싶을것이다.

    • 배열은 실체화 가능 타입이고, 제네릭은 실체화 불가 타입이다.

1. 실체화 가능 타입은 런타임에도 타입정보를 가지고 있다.

2. 제네릭은 런타임에 타입정보를 가지고 있지 않다.

3. 즉 new List[], new E[]같은 제네릭을 배열로 만드는 행위가 불가능하다는 것이다.

만약 위같은 상황이 된다고 가정해보면

List<String>[] stringLists = new List<String>[1];  //1
List<Integer> intList = List.of(42);               //2
Object[] objects = stringLists;                    //3
objects[0] = intList;                              //4
String s = stringLists[0].get(0);                  //5

​ 위 상황에서 5번에서 에러가 날것이다. 배열은 공변이니 3번에서 따로 에러가 안날것이고,

4번에서 값을 집어넣을때도 컴파일 타임에 에러가 안날 것이다.

​ 4번에 값을 넣을때는 어떨까? 당연히 에러가 안난다. List는 런타임에 타입소거되어 List가 되고 값을 넣을 수 있을것이다.

​ 결국 5번에서 String으로 받을때 ClassCastExcetpion이 일어날 것이다.

위 같은 상황은 배열을 제네릭으로 만들수 없어 귀찮은 상황을 유발한다.

예를들어 제네릭 컬렉션은 자기 자신의 배열을 반환할 수 없다.(예외도 있다.)

또 가변인수 메서드sum (int ….target) 같은 메서드를 제네릭과 함께 쓸때는 알 수 없는 경고를 발생시킨다.

  • @SafeVarags어노테이션으로 해결가능
  • 리스트를 활용하여 이펙티브 자바에서 런타임에 ClassCastException을 만날일 없는 예제를 제공한다. 참고하자, 168P