item28. 배열보다는 리스트를 사용하라

이월(0216tw)·2024년 6월 1일
0

effective java

목록 보기
2/7

배열과 제네릭의 차이

(1) 배열은 공변, 제네릭은 불공변이다.

공변 : Sub가 Super의 하위 타입이라면 배열 Sub[] 는 배열 Super[]의 하위타입이다.
따라서 함께(공) 변한다(변) 는 뜻이다.

배열은 공변이다.

아래 코드를 통해 공변의 의미를 이해해보자.

class Animal {
    public void makeSound() {
        System.out.println("Some sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Bark");
    }
}

public class CovarianceExample {
    public static void main(String[] args) {
        Animal[] animals = new Dog[2]; // 배열은 공변성을 가짐
        animals[0] = new Dog();        // 가능
        animals[1] = new Animal();     // 에러 (실제 저장되는 타입은 하위여야 함)
      
        for (Animal animal : animals) {
            animal.makeSound(); // "Bark"
        }
    }
}

위 코드에서 Animal[] 인 부모타입 배열은 실제로는 Dog[] 라는 자식객체 배열을 참조한다. (공변성)
그렇기에 animals[0] 에 new Dog() 를 넣는 것이 가능하다.
하지만 Dog[] 배열에 Animal을 저장하려고 하면 ArrayStoreException이 발생한다.

👨‍💻정리
부모-자식 관계인 클래스의 경우 배열에서도 부모[] , 자식[] 관계가 성립할 수 있다.

제네릭은 불공변이다.

import java.util.ArrayList;
import java.util.List;

class Animal {
    public void makeSound() {
        System.out.println("Some sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Bark");
    }
}

public class InvarianceExample {
    public static void main(String[] args) {
        List<Animal> animals = new ArrayList<>();
        animals.add(new Dog());
        // List<Dog> dogs = animals; // 컴파일 오류, List<Animal>은 List<Dog>가 될 수 없음 
        
        for (Animal animal : animals) {
            animal.makeSound(); // "Bark"
        }
    }
}

👨‍💻정리
리스트 같은 제네릭 타입의 경우 List<Animal> 과 List<Dog> 는 함께 변하지 않으므로 상/하위의 의미가 없다.
배열은 런타임에러 , 제네릭은 컴파일 단계에서 에러를 확인할 수 있다



(2) 배열은 실체화(reify)가 가능하다.

실체화 : 런타임 시점에 컴포넌트 타입 정보를 유지하고 있는가?

예를 들어 배열 String[] 이 있다고 해보자.
배열은 실체화가 가능 하기 때문에 "나는 String 타입 요소를 받아야 하는구나!" 라고 인지를 한다.
그렇기에 런타임 시점에도 이를 유지하고 있다.

반대로 제네릭은 실체화가 불가능 하다.
왜냐면 컴파일 타임 이후에 런타임 시점에는 타입 소거(type erasure)가 발생하기 때문이다

코드로 확인해보자

public class ReificationExample {
    public static void main(String[] args) {
        // 배열의 실체화
        String[] stringArray = new String[10];
        Object[] objectArray = stringArray;
        objectArray[0] = "Hello";
        // objectArray[1] = 123; // 컴파일 오류, 런타임에 ArrayStoreException 발생

        System.out.println(stringArray[0]); // "Hello"
        
        // 제네릭의 비실체화
        List<String> stringList = new ArrayList<>();
        // List<Object> objectList = stringList; // 컴파일 오류, 제네릭은 불공변
    }
}

먼저 배열은 내가 String을 요소로 받아야 한다는 사실을 인지하고 있다.
이후 Object[] 배열에 String[] 을 대입해도 공변성으로 가능하다.
하지만 여전히 String[] 은 자신이 String 요소를 받아야 한다는 것을 알고 있다.

반면 제네릭은 컴파일 시점에 미리 타입을 확인해서 컴파일 에러를 발생시킨다.
하지만 런타임 시점에는 자신이 어떤 타입을 받는지를 알 수 없다(소거당하기 때문)


(3) 제네릭으로는 배열을 만들 수 없다.

예를 들어 List<String>[] stringArray = new ArrayList<String>[] 같은 코드는 불가능하다.
그 이유는 타입이 안전하지 않기 때문이다.

예를 들어보자

List<String>[] stringArray = new ArrayList<String>[3]; //이 코드가 가능하다고 가정해보자. 

List<Integer> intArray = List.of(42);   //[42] 가 리스트로 존재

Object[] objects = stringArray; // 배열은 공변이고 Object는 최상위 부모 클래스이므로 가능함 

objects[0] = intList; 
//현재 Object[] objects 는 new ArrayList<String>[3] 이 할당된 상태임
//ArrayList<String> = ArrayList<Integer> 는 원래는 불가능하지만 
//런타임 시점에는 타입이 소거됨 
//그래서 ArrayList = ArrayList 가 되므로 타입에 문제가 발생하지 않고 대입되는 것

String s = stringArray[0].get(0); // Integer값이 String으로 입력되려고 하니 ClassCastException 발생

👨‍💻정리
뭔가 복잡한 코드처럼 보이나 결론은 제네릭을 배열로 사용하면 이런 위험이 있다~ 는 말이며 올바른 타입 안전성을 유지하기 어렵기에 제네릭 타입의 배열 생성을 허용하지 않는다.


배열과 리스트 장단점 정리

배열

장점

  • 크기를 고정하므로 메모리 사용 예측 가능
  • 인덱스로 빠른 접근을 할 수 있음 (상수시간)
  • 연속된 메모리 공간을 사용해 효율적

단점

  • 크기가 초기화 시점에 결정되어 크기를 변경할 수 없음 (필요시 새로 생성해야함)
  • 삽입 삭제가 비효율적임 (만약 중간에 배열을 추가한다면 다른 요소들도 직접 처리해야함)
  • 타입안전성 : 배열은 공변성, 그래서 컴파일시점에는 타입이 안전한지 알 수 없고 런타임 시점에 확인할 수 있음

리스트 (ArrayList 을 기준으로)

장점

  • 동적으로 크기를 처리할 수 있음
  • 인덱스에 빠르게 접근 가능
  • 제네릭을 지원해 타입 안전성을 보장할 수 있음

단점

  • 삽입/삭제가 비효율적 , 인덱스의 값을 모두 바꿔줘야 하므로 비효율적
  • 메모리 오버헤드 : 배열보다 많은 메모리 사용 , 크기 조정 발생시 메모리 재할당 및 복사가 필요

최종결론
배열과 제네릭은 함께 쓰이기 쉽지 않다. 특징과 차이가 서로 존재하기 때문이다.
만약 섞어 쓰다가 오류가 발생하면 배열을 리스트로 대체 라는 방법을 적용해보자
타입 안전성을 제공해주니까.

profile
Backend Developer (Financial)

0개의 댓글