[item#28] 배열보다는 리스트를 사용하라
배열보다는 리스트를 사용하라.
배열과 제네릭 타입의 차이
- 배열은 공변이다.
- 제네릭은 불공변이다.
// Object[] objArr = new Long[1];
// objArr[0] = "타입이 다르다."; // ArrayStoreException 발생
List<Object> ol = new ArrayList<Long>();
ol.add("타입이 다르다.");
배열에서는 런타임할 때 알게되지만, 리스트를 사용하게 되면 컴파일할 때 바로 알 수 있다.
주요차이점은 배열은 실체화(reify)된다. -> 배열과 제네릭은 잘 어우러지지 못한다.
배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인하다.
그래서 위의 코드에서 보듯 Long배열에 String을 넣으려 하면 ArrayStoreException
이 발생한다.
반면, 제네릭은 타입 정보다 런타임에는 소거된다.
원소 타입을 컴파일타임에만 검사하며 런타임에는 알 수 조차 없다는 뜻.
List<String> names = new ArrayList<>();
names.add("dongjin");
String name = names.get(0);
System.out.println(name);
//////// 컴파일러가 만들어주는 코드 (예시)
List names = new ArrayList();
names.add("dongjin");
Object o = names.get(0);
String name = (String) o;
System.out.println(name);
소거는 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘.
// 공변
Object[] anything = new String[10];
anything[0] = 1;
// 불공변
List<String> names = new ArrayList<>();
// List<Object> objects = names;
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);
원래는 위 이미지처럼 제네릭 배열이 생성되지 않도록 첫 번째 줄에서 컴파일 오류를 내야 한다.
실체화 불가 타입
E
, List<E>
, List<String>
같은 타입을 실체화 불가 타입(non-reifiable type)이라 한다.
실체화가 되지 않아서 런타임에는 컴파일타임보다 타입 정보를 적게 가지는 타입이다.
소거 메커니즘 때문에 매개변수화 타입 가운데 실체화될 수 있는 타입은 List<?>
와 Map<?, ?>
같은 비한정적 와일드카드 타입뿐이다.
배열을 비한정적 와일드카드로 만들 수는 있지만, 유용하게 쓰일 일은 없다.
배열을 썼을 때 문제점과 리스트를 썼을 때 장점
Chooser 클래스를 예시로 만들어서 설명.
public class Chooser {
private final Object[] choiceList;
public Chooser(Collection choices){
choiceList = choices.toArray();
}
public Object choose(){
Random rnd = ThreadLocalRandom.current();
return choiceList[rnd.nextInt(choiceList.length)];
}
public static void main(String[] args) {
List<Integer> list = List.of(1, 2, 3, 4, 5, 6);
Chooser chooser = new Chooser(list);
for (int i = 0; i < 10; i++) {
Number choice = (Number) chooser.choose();
System.out.println(choice);
}
}
}
잘 작동함.
List<String> list = List.of("dong", "kim");
Chooser chooser = new Chooser(list);
for (int i = 0; i < 10; i++) {
Number choice = (Number) chooser.choose();
System.out.println(choice);
}
이렇게 바뀐다면 컴파일에선 오류가 나지 않지만 런타임 시에 ClassCastException
이 발생한다.
private final T[] choiceList;
@SuppressWarnings("uncheked")
public Chooser(Collection<T> choices) {
choiceList = (T[]) choices.toArray();
}
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList[rnd.nextInt(choiceList.length)];
}
이렇게 @SuppressWarning("unchecked")
를 붙이는 방법도 있지만
배열을 리스트로 바꾸는 방법이 더 좋다.
public class Chooser<T> {
private final List<T> choiceList;
@SuppressWarnings("uncheked")
public Chooser(Collection<T> choices) {
choiceList = new ArrayList<>(choices);
}
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size()));
}
public static void main(String[] args) {
List<Integer> list = List.of(1, 2, 3, 4, 5, 6);
Chooser<Integer> chooser = new Chooser<>(list);
for (int i = 0; i < 10; i++) {
Number choice = (Number) chooser.choose();
System.out.println(choice);
}
}
}
이렇게 하면 List에 다른 타입이 들어가면 컴파일단계에서 오류가 나기 때문에 안정성이 좋아진다.
만약 배열과 리스트를 섞어 쓰다가 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용해보자.
@SafeVarags
- 생성자와 메소드의 제네렉 가변인자에 사용할 수 있는 어노테이션
제네릭 타입과 가변인수 메소드를 함께 쓰면 경고 메시지를 받게 된다.
가변인수 메서드를 호출할 때마다 가변인수 매개변수를 담을 배열이 하나 만들어지는데, 이때 그 배열의 원소가 실체화 불가 타입이라면 경고가 발생하는 것이다.
이 문제는 @SafeVarags로 대처할 수 있다.
package org.example;
import java.util.ArrayList;
import java.util.List;
public class Dangerous {
@SafeVarargs // Not safe
static void dangerous(List<String>... stringLists) {
List<Integer> integerList = List.of(42); // List<String>... => List[] 리스트 배열이니까 오브젝트 배열에 할당할 수 있다. 공변이니까.
Object[] objects = stringLists;
objects[0] = integerList; // 힙 오염 발생
String s = stringLists[0].get(0); // ClassCastException
}
@SafeVarargs
static <T> void safe(T... values) {
for (T value : values) {
System.out.println(value); // 새로운 변수에 할당하지 않고, 리턴하지 않고 단순 출력할 때만 @SafeVarargs 쓰는 것을 권장.
}
}
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("a");
stringList.add("b");
stringList.add("c");
List<String> stringListB = new ArrayList<>();
stringListB.add("a");
stringListB.add("b");
stringListB.add("c");
List<Integer> integerList = new ArrayList<>();
integerList.add(1);
integerList.add(2);
integerList.add(3);
safe(stringListB, stringList, integerList);
dangerous(stringList, stringListB);
}
}