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

배열과 리스트를 섞어쓰다 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하라!

📌 1. 발표 전 알아야 할 개념

✅ 컴파일과 런타임

1. 컴파일(Compile)

  • 프로그램 소스코드를 기계어(바이너리 코드) 혹은 중간 코드(바이트코드)로 변환하는 과정

  • 컴파일러가 수행

  • 실행 전에 오류를 확인하고 수정할 수 있음

  • Java, C, C++ 등 컴파일 언어에서 사용

2. 런타임(Runtime)

  • 프로그램이 실제로 실행되는 시점

  • 동적으로 변수 값을 변경하거나, 메모리를 할당하는 과정이 포함

  • 런타임 오류가 발생하는 곳

3. 전체 실행 과정을 알아보자!

  1. 소스 코드를 작성한다 (.java / .c 등)

  2. 컴파일(Compile) - 컴파일 과정에서 문법 오류가 있으면 실행 안됨

  3. 실행 파일 생성 - Java는 .class / C는 .exe 등

  4. 런타임(Runtime) - 실행, 변수 값 할당, 메모리 관리, 예외 처리가 이루어짐

✅ classCastException

  • 객체의 타입을 변환(캐스팅)하려고 할 때, 타입이 호환되지 않아 발생하는 런타임 에러러

📕 2. 배열과 제네릭릭의 차이

2-1. 공변 vs 불공변 : 타입 관계계의 차이

  • 공변 : (함께 변한다) 하위 타입을 상위 타입에 넣을 수 있다

  • 불공변 : 하위 타입을 상위 타입에 넣을 수 없다

🔗 배열공변

Car[] carArray = new SportsCar[3]; // 가능 (공변)
carArray[0] = new SportsCar();     // 가능
carArray[1] = new Car();           // 런타임 오류 발생 (ArrayStoreException)!!
  • 배열은 공변이기에, Car[]에 SportsCar[]를 할당할 수 있음

  • 그러나 타입 오류가 있을 경우 런타임 에러(ArrayStoreException) 발생

🔗 제네릭릭불공변

List<Car> carList = new ArrayList<SportsCar>(); // 컴파일 오류
  • 리스트는 불공변, 서브타입을 대입할 수 없음

  • 컴파일 단계에서 컴파일 에러 발생!

2-2. 실체화 : 타입 검사의 차이

  • 실체화 : 런타임에 타입 정보를 유지하는 것

🔗 배열실체화

Car[] carArray = new SportsCar[3];
carArray[0] = new SportsCar();
carArray[1] = new Car();           // 런타임 오류 발생 (ArrayStoreException)!!
  • 배열은 생성될 때 요소의 타입 정보를 함께 저장

  • 런타임에서 안전성 검사

🔗 제네릭타입 소거

List<String> carList = new ArrayList<>();
carList.add("tesla");
carList.add(123); // 컴파일 에러
  • 제네릭은 컴파일 시에 타입 정보를 검사하고, 런타임에는 타입 정보를 제거한다

  • 컴파일 시 : List, 런타임 : List

  • 런타임에 타입 정보를 확인할 수 없음

2-3. 배열 vs 리스트

  • 배열 : 런타임 실행 중 검사 → 실행 도중 오류 발생(ClassCastException) → 위험! → 타입 안정성 낮다

  • 리스트 : 컴파일, 코드 작성 시 검사 → 실행 전 오류 발생 → 안전 → 타입 안정성 높다다

➡️ 따라서 배열보다 리스트가 타입 안전성이 더 높다!

🙅🏼‍♂️ 3. 제네릭과 배열은 잘 어우러지지 않는다

3-1. 제네릭 배열 생성은 불가하다 💥

  • 제네릭 배열 List<String>[]은 생성할 수 없다

  • 컴파일 시 타입 정보가 소거되기 때문에, 런타임에서 배열의 타입을 검사할 수 없기 때문

제네릭 배열 생성이 가능하다고 가정하고, 아래 코드를 확인해 봅시다다

List<String>[] strListArr = new List<String>[1]; // 허용된다고 가정
List<Integer> intList = List.of(123);
Object[] objArr = strListArr;
objArr[0] = intList; // 문제
String s = strListArr[0].get(0); // ClassCastException 발생
  1. List 객체만을 담을 수 있는 starListArr 배열 생성

  2. List 타입의 intList 생성 → 정수 123 리스트에 추가

  3. strListArr 배열을 Object[] 타입의 objArr 변수에 할당함. 배열이 공변이므로, List은 Object의 하위 타입. 할당 가능

  4. objArr는 Objec[] 타입이므로 List 타입 할당 가능. objArr[0]에 intList 할당.

  5. strListArr[0]에서 String을 가져오려고 했으나, List 타입의 intList를 참조하고 있음. ➡️ ClassCastException

따라서 제네릭 배열은 생성할 수 없다!

✨ 4. 배열 대신 리스트를 사용하자

  • 책에서는는 Chooser<T> 클래스를 사용하여 제네릭 배열을 사용하면 안 되는 이유 및 리스트를 활용해야 하는 이유에 대해 소개했다

  • 이를 좀 더 쉽게 변형해서 알아보자.

1. 제네릭을 사용하지 않은 구현 - Object

/**
 * 여러 개의 항목 중 하나를 무작위로 선택하는 클래스
 * 1. 제네릭 적용하지 않은 초기 버전 - Object[]
 */
class Chooser {
    private final Objcet[] choices;
    /**
     * Chooser 객체를 생성
     *
     * @param choices 선택할 항목 배열 (Object 타입)
     */
    public Chooser(Object[] choices) {
        this.choices = choices;
    }
    /**
     * 리스트에서 무작위 요소를 선택하여 반환
     *
     * @return 무작위로 선택된 요소 (Object 타입)
     */
    public Object pickRandom() {
        Random rand = new Random();
        return choices[rand.nextInt(choices.length)];
    }
}
  • Object를 반환하므로 사용자가 매번 형변환을 해야한다

  • Object로 선언해서 아무 타입이나 저장 가능하다

  • 제네릭을 시급히 적용해야 한다!!

  1. 제네릭을 적용한 첫 시도 - 제네릭 배열 사용

class Chooser1<T> {
    private final T[] choices; // 제네릭 배열 생성 불가

    public Chooser1(T[] choices) {
        this.choices = (T[]) choices; // 컴파일 오류 (타입 소거)
    }

    // 생략
}
- Object[] 대신 제네릭을 사용하여 특정 타입만 저장하도록 개선
- 제네릭의 타입 소거로 인해, 컴파일 이후에 `T`가 사라지고 사실상 `Object[]`처럼 동작하게 됨
- 배열은 실체화 되지만, 제네릭은 실체화 되지 않아 T의 타입을 보장할 수 없음
- 비검사 형변환 경고 발생! 원인을 제거하자

3. 리스트를 사용하자. 💥

class Chooser2<T> {
    private final List<T> choices;

    public Chooser2(List<T> choices) {
        if (choices == null || choices.isEmpty()) {
            throw new IllegalArgumentException("선택 목록은 비어 있을 수 없습니다.");
        }
        this.choices = choices;
    }

    //생략
}
- `Chooser<T>`를 사용하여 안정성 유지
- ClassCastException을 만나지 않는다

➡️ 리스트를 사용하자!


💨 향후 확장 포인트

  • List<?>, Map<?>과 같은 비한정적 와일드카드 타입을 통해 일부 실체화할 수 있다


🤖 최종 결론

배열은 실체화되어 런타임에서도 타입 정보를 유지하지만, 제네릭은 타입 소거를 한다 배열과 제네릭을 함께 사용할 경우 타입 안전성이 위험하다 제네릭 배열을 만들지 말고 리스트를 사용하자!


❗어려웠던 점

  • 제네릭에 대한 개념이 확실하지 않아서 헷갈렸는데, 이 아이템을 통해 이해를 높일 수 있었다!


😶‍🌫️ 느낀점

  • 제네릭과 배열의 충돌을 이해하는 것이 중요하다!

  • 사용하는 타입, 메서드가 많아지고 프로젝트 볼륨이 커질수록 고려해야되는 부분이 많다.

Last updated