본문 바로가기
개발 공부/Java

이펙티브 자바 아이템 28 - 배열보다는 리스트를 사용하라 - 핵심 정리

by 개발인생 2022. 12. 12.
반응형

아이템 28 - 배열보다는 리스트를 사용하라 - 핵심 정리

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

배열과 리스트는 잘 어울리지 않는다.

  • 배열은 공변(covariant), 제네릭은 불공변

공변은 같이 변한다 라는 뜻이다.

public class IntegerToString {

    public static void main(String[] args) {
        // 공변
        Object[] anything = new String[10];
        anything[0] = 1; // 컴파일러가 못잡는 버그, 런타임 에러 발생

    }
}

배열은 상속 관계 에 따라 같이 변한다.

위의 코드에서 StringObject 를 상속하고 있다.

때문에 String 의 타입을 Object 타입으로 변환이 가능하다.

그래서 String 타입의 배열을 Object 타입의 배열에 담을 수 있다.

배열 안에 들어가는 타입을 최상위 타입으로 변환하는게 가능하다.

하지만 Object 타입으로 참조하고 있는 String 타입의 배열의 실체는 String 타입의 배열이다.

즉, 위의 코드에서 anything 배열 안의 실제 레퍼런스는 String 의 배열이다.

anything[0] = 1; 이 코드가 컴파일 에러가 나지는 않지만 문제가 있는 코드이다.

anything 배열이 참고하고 있는 타입은 String 타입이다.

여기에 숫자를 넣는 것은 잘못된 코드 이다.

하지만 배열은 공변 이기 때문에 컴파일러가 잡지 못한다.

public class IntegerToString {

    public static void main(String[] args) {
       // 불공변
       List<String> names = new ArrayList<>();
        List<Object> objects = names; // 컴파일 에러
    }
}

반면 제네릭은 불공변 이다.

여기서 List 와 제네릭에 대해 헷갈릴 수 있지만 둘은 별개이다.

List 는 제네릭을 쓸 수 있기 때문에 배열보다는 리스트를 사용하라는 것이다.

제네릭에서는 타입의 상하관계를 따지지 않는다.

List<String> 타입을 List<Object> 타입에 넣을 수 없다.

둘은 다른 타입이다.

애초에 컴파일 에러가 발생한다.

이게 바로 불공변 이다.

  • 배열은 실체화(reify) 되지만, 제네릭은 실체화 되지 않는다. (소거)

프로그래밍에서의 실체화는 내가 코딩을 할 때 작성한 타입이 런타암에도 유지가 되느냐이다.

public class IntegerToString {

    public static void main(String[] args) {
        // 공변
        Object[] anything = new String[10];
        anything[0] = 1; // 컴파일러가 못잡는 버그, 런타임 에러 발생

       // 불공변
       List<String> names = new ArrayList<>();
//        List<Object> objects = names;
    }
}

위의 코드에서 new String[10] 은 런타임에서도 String 타입의 배열이다.

배열은 실체화가 된다.

하지만 제네릭은 실체화되지 않는다.

List<String> 는 컴파일을 하고나면 소거 된다.

제네릭은 자바의 하위버전 호환을 위해 소거 방식으로 구현되었다.

public class MyGeneric {

    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("keesun");
        String name = names.get(0);
        System.out.println(name);
    }
}

위의 코드는 컴파일 시에

  List names = new ArrayList();
  names.add("keesun");
  Object o = names.get(0);
  String name = (String) o;

위와 같이 구현된다.

List<String> 에서의 타입 정보는 사라지고,

그 타입 정보가 List 안의 값을 캐스팅 할 때 사용된다.

값을 꺼낼 때는 Object 타입으로 꺼내고, 타입을 우리가 사용한 제네릭 타입으로 변환하는 과정을 추가한다.

위의 과정은 바이트 코드 에서 자세히 확인할 수 있다.

이러한 차이점 때문에 배열과 제네릭을 같이 사용하게 되면 어울리지 않고 굉장히 이상해진다.

뭔가 코드를 잘못 작성하면 제네릭은 컴파일 타임 에 깨지게 되고, 배열은 런타임 에 깨지게 된다.

때문에 우리는 제네릭을 선호하게 된다.

public class IntegerToString {

    public static void main(String[] args) {

       // 제네릭과 배열을 같이 사용할 수 있다면...
        List<String>[] stringLists = new ArrayList<String>[1];
        List<Integer> intList = List.of(42);
        Object[] objects = stringLists;
        objects[0] = intList;
        String s = stringLists[0].get(0);
        System.out.println(s);
    }
}

제네릭 타입의 배열을 선언할 수는 없다.

만약에 가능하다면 위의 코드처럼 작성할 것이다.

List 의 배열을 Object 배열에 넣게되고

objects[0] = intList; 의 코드로 List 배열에 List 를 넣게된다.

문제는 String s = stringLists[0].get(0); 의 코드에서 발생한다.

stringLists[0] 에는 List<Integer> 가 들어있고,

List<Integer>String 으로 캐스팅할 때 에러가 발생한다.

코드를 한줄 한줄 봤을 때는 아무 이상이 없지만 실행시에 에러가 발생하게 된다.

때문에 자바에서는 제네릭의 배열을 만들 수 없다.


public class Chooser_Array {
    private final Object[] choiceList;

    public Chooser_Array(Collection choices) {
        choiceList = choices.toArray();
    }

    public Object choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceList[rnd.nextInt(choiceList.length)];
    }
}

Chooser_Array 클래스는 어떠한 Collection 을 받아 해당 요소 안에 들어있는 값 중에 랜덤한 값을 리턴하는 기능을 한다.

public class Client {
   public static void main(String[] args) {
      List<Integer> intList = List.of(1, 2, 3, 4, 5, 6);

      Chooser_Array chooser = new Chooser_Array(intList);

      for (int i = 0; i < 10; i++) {
         Number choice = (Number) chooser.choose();
         System.out.println(choice);
      }
   }
}

클라이언트 코드에서는 다음과 같이 작성한다.

Chooser_Array 클래스에 List<Integer> 를 넘기고 랜덤한 값을 받았다.

위 코드에서의 문제는 Number choice = (Number) chooser.choose(); 코드이다.

이 부분에서 만약에 전달한 콜렉션과 호환하지 않는 타입으로 형변환 하려고 하면 에러가 발생한다.

이게 배열 기반으로 코딩했을 때 발생할 수 있는 문제 중 하나이다.

범용적으로 사용하기에는 조금 불편함이 있다.

이렇게 범용적으로 사용하는 클래스에서의 타입 형변환 문제 를 해결하기 위해 나온게 제네릭이다.

public class Chooser<T> {
    private final List<T> choiceList;

    public Chooser(Collection<T> choices) {
        choiceList = new ArrayList<>(choices); // 방어적인 복사
    }

    public T choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceList.get(rnd.nextInt(choiceList.size()));
    }

    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3, 4, 5, 6);

        Chooser<Integer> chooser = new Chooser<>(intList);

        for (int i = 0; i < 10; i++) {
            Number choice = chooser.choose();
            System.out.println(choice);
        }
    }
}

위처럼 제네릭 타입을 선언해 클래스를 작성한다.

더 이상 타입을 형변환을 하는 필요가 없어진다.

다른 타입을 사용하더라도 컴파일 타임에 에러를 확인할 수 있어 훨씬 안전한 코드가 된다.

성능에 아주 민감한 코드가 아니라면 배열을 리스트로 바꾸는 것을 추천한다.

반응형

댓글