본문 바로가기
JAVA/Effective Java

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

by Garonguri 2022. 5. 13.
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

댓글