JAVA/Effective Java

item 28. 배열보다는 리스트를 사용해라

Garonguri 2022. 5. 13. 16:01
728x90

위 아이템에서는 배열과 제네릭 타입의 차이점을 다루고 있으며 결과적으로 배열보다 제네릭 사용을 권유하고 있다.


1. 배열은 공변, 제네릭은 불공변

 

공변과 불공변?

공변 : 함께 변한다. 즉, 관계(ex 상속..)를 맺는 클래스/타입들이 관계가 유지된 채로 함께 변한다.

Object[] objectArray = new Long[1];
objectArray[0] = "스트링";
System.out.println(objectArray[0]);
//in Runtime : Exception in thread "main" java.lang.ArrayStoreException

"Long은 Object를 상속하고 있다.
배열로 사용되고 있기 때문에, Long에 String type을 넣어도 컴파일이 가능하다."

불공변 : 함께 변하지 않는다. 즉, 관계가 유지되지 않는다.

List<Object> ol = new ArrayList<Long>();

"Long은 Object를 상속하고 있다.
제네릭 타입을 사용하는 경우 같은 타입이 아니므로 컴파일 에러가 발생한다."


2. 배열은 실체화, 제네릭은 비실체화

 

실체화와 비실체화?

실체화 : 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다. 즉, 런타임 시점에 타입을 인지하고 실행한다.

 

비실체화 : 런타임 시점에는 타입 정보를 소거한다. 제네릭 타입 시스템의 취지는 컴파일 시점에 에러를 잡고 런타임에 ClassCastException이 발생하는 것을 막아주는 것이다. 

class Test {
    public static void main(String[] args) {
        List<String> strs = new ArrayList<>();
        strs.add(1);
        strs.add(2);

        String str1 = strs.get(0); // 형변환이 필요없다
        String str2 = strs.get(1); // 형변환이 필요없다
    }
}

위 Test.java 파일을 컴파일하고, 컴파일한 Test.class 파일을 디컴파일하면 아래와 같은 결과를 볼 수 있다.

디컴파일 한 결과 (Test.java -> Test.class -> decompile)

class Test {
    Test() {
    }

    public static void main(String[] var0) {
        List var1 = new ArrayList(); // 타입 정보가 없다. 제네릭이 사라졌다..?
        var1.add(1);
        var1.add(2);
        String var2 = (String)var1.get(0); // 형변환이 추가
        String var3 = (String)var1.get(1); // 형변환이 추가
    }
}

위에서 알 수 있듯이, 제네릭은 런타임 시점에 타입 정보가 사라진다.

 

배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다. 

- 따라서 new List<E>[], new List<String>[], new E[]과 같이 제네릭 배열이 생성되면 컴파일 오류를 내야 한다.

- 만약 제네릭 배열 생성을 허용한다면  어떻게 될까? 컴파일러가 자동으로 형변환코드를 생성해 형변환을 진행하는 과정에서 런타임에 ClassCastException이 발생할 수 있다. -> 이는 런타임에서 CCE가 발생하는 일을 막아주겠다는 제네릭 타입 시스템 취지에 어긋난다. 


실체화 불가 타입?

실체화되지 않아서 런타임에는 컴파일 타입보다 타입 정보를 적게 가지는 타입을 의미한다.

- E, List<E>, List<String>등과 같은 타입을 실체화 불가 타입이라 한다.

 

앞에서 배웠던 매개변수화 타입 중, 비한정적 와일드카드 타입 "?"만 List<?>, Map<?,?>등의 형태로 실체화를 할 수 있다고는 하지만, 유용하게 쓰일 일은 없다고 한다.


  • 배열을 제네릭으로 만들수 없어서 안좋은 점도 있다.
    • 제네릭 컬렉션에서 자신의 원소 타입을 담은 배열을 반환하고 싶은 경우
      • 보통 불가능하다.
      • item33에 가면 해결될~~수도~~
    • 제네릭 타입과 가변인수 메서드를 함께 쓰면 해석이 어려운 경고 메시지를 받을 수 있다.
      • 가변인수 메서드를 호출할 때마다 이를 담을 배열이 만들어지는데, 이 때 그 배열 원소가 실체화 불가 타입일 수가 있다.
      • 이 때 경고가 발생한다.
      • @SafeVarargs annotaion을 사용해 대처 가능

  • 배열로 형변환을 하던 중 제네릭 배열 생성 오류나 비검사 형변환 경고가 떴다면?
    • 대부분 배열 E[] 대신 컬렉션 List<E>를 사용하면 해결된다. 해봐라~!
      • 코드는 조금 복잡해지고 성능이 나빠질 수는 있다.
      • 그러나, 타입 안정성과 상호 운용성이 좋아진다.
핵심 정리

  배열 제네릭
공/불공변 공변 불공변
실체/비실체화 실체화 비실체화(타입정보 소거)
타입안정성 컴파일타임에는 타입안전X
런타임에는 타입안전O
컴파일타임에는 타입안전O
런타임에는 타입안전X
결론 따라서 배열과 제네릭을 섞어 쓰기란 쉽지 않다.
둘을 쓰다가 컴파일 오류나 경고를 만나면, 제네릭으로 바꿔라!

 

728x90