28. 배열보다는 리스트를 사용하라
배열과 리스트를 섞어쓰다 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하라!
📌 1. 발표 전 알아야 할 개념
✅ 컴파일과 런타임
1. 컴파일(Compile)
프로그램 소스코드를 기계어(바이너리 코드) 혹은 중간 코드(바이트코드)로 변환하는 과정
컴파일러가 수행
실행 전에 오류를 확인하고 수정할 수 있음
Java, C, C++ 등 컴파일 언어에서 사용
2. 런타임(Runtime)
프로그램이 실제로 실행되는 시점
동적으로 변수 값을 변경하거나, 메모리를 할당하는 과정이 포함
런타임 오류가 발생하는 곳
3. 전체 실행 과정을 알아보자!
소스 코드를 작성한다 (.java / .c 등)
컴파일(Compile) - 컴파일 과정에서 문법 오류가 있으면 실행 안됨
실행 파일 생성 - Java는 .class / C는 .exe 등
런타임(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 발생
List 객체만을 담을 수 있는 starListArr 배열 생성
List 타입의 intList 생성 → 정수 123 리스트에 추가
strListArr 배열을 Object[] 타입의 objArr 변수에 할당함. 배열이 공변이므로, List은 Object의 하위 타입. 할당 가능
objArr는 Objec[] 타입이므로 List 타입 할당 가능. objArr[0]에 intList 할당.
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로 선언해서 아무 타입이나 저장 가능하다
제네릭을 시급히 적용해야 한다!!
제네릭을 적용한 첫 시도 - 제네릭 배열 사용
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