앞서 item28 에서는 제네릭과 배열은 함께 사용할 수 없다는 것(컴파일 오류)을 배웠습니다. 하지만 이번 장에서는 함수의 매개변수로서 제네릭과 가변인수(= 배열)은 같이 사용할 수 있고, 사용하는 경우 조심해야 하는 점을 말합니다.
이 글에서는
코드 28-3 제네릭 배열 생성을 허용하지 않는 이유 - 컴파일 되지 않는다.
List<String>[] stringLists = new List<String>[1]; // (1) List<Integer> intList = List.of(42); // (2) Object[] objects = stringLists; // (3) objects[0] = intList; // (4) String s = stringLists[0].get(0); // (5) ClassCastException
=> (1) 에서 컴파일이 된다고 가정해보면 결국 (5) 에서 런타임 오류인 ClassCastException 이 발생합니다.
따라서 ClassCastException 과 같은 (타입 불안정으로 인한) 런타임 오류가 발생하는 것을 방지하겠다는 제네릭 타입 시스템과 취지가 맞지 않으므로 자바에서는 배열과 제네릭을 같이 사용하는 것을 금지합니다.
Note 배열(공변) vs 제네릭(불공변)
코드 32-1 제네릭과 varargs를 혼용하면 타입 안정성이 깨진다!
static void dangerous(List<String>... stringLists) { List<Integer> intList = List.of(42); // (1) Object[] objects = stringLists; // (2) objects[0] = intList; // (3) String s = stringLists[0].get(0); // (4) ClassCastException }=> item28 과 마찬가지로 제네릭의 불공변(invariant) 특성이 가려지게 됩니다. 그래서 위의 코드처럼 제네릭을 사용함에도 불구하고, 타입이 다른 객체를 잘못 참조하게 되면 런타임 오류인
ClassCastException이 발생합니다. 그럼 item28처럼 (타입이 다른 이유로 인한) 런타임 오류가 발생함에도 왜 자바는 경고에서만 그치고 제네릭과 varargs를 함께 선언하는 것을 허용할까요?
Arrays.asList(T... a), Collections.addAll(Collection<? super T> c, T... elements), EnumSet.of(E first, E... rest)가 대표적입니다. 물론 이 메서드들은 타입 안전합니다.Object... 를 사용하는 것보다 컴파일 타임에 타입 검사 가능한 이점과 특정 타입의 varargs(ex: Integer...)를 사용하는 것보다 메서드의 타입 사용 범위가 넓어진다는 이점에서 사용 허용하는 것 같습니다.=> 메서드가 안전한게 확실하지 않다면 절대 @SafeVarargs 에너테이션을 달아서는 안됩니다. 그럼 어떻게 메서드가 타입 안전한지 알 수 있을까요?
@SafeVarargs를 써도 되는 간단한 규칙 두 가지가 있습니다.
따라서 다음과 같은 코드는 매개변수 배열에 intList를 저장하므로 위 규칙에 위반됩니다.
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42); // (1)
Object[] objects = stringLists; // (2)
objects[0] = intList; // (3)
}
신뢰할 수 없는 코드에 노출하는 경우
따라서 다음과 같은 코드는 신뢰할 수 없는 코드에 노출해서 위 규칙에 위반됩니다. 심지어 varargs 매개변수 배열에 아무것도 저장하지 않고도 타입 안정성을 깰 수 있으니 주의해야 합니다.
코드 32-2 자신의 제네릭 매개변수 배열의 참조를 노출한다 - 안전하지 않다!
static <T> T[] toArray(T... args) { return args; }=> 이 메서드가 반환하는 배열의 타입은 이 메서드에 인수를 넘기는 컴파일 타입에 결정되는데, 그 시점에는 컴파일러에게 충분한 정보가 주어지지 않아 타입을 잘못 판단할 수 있습니다. 따라서 자신의 varargs 매개변수 배열을 그대로 반환하면 힙 오염을 이 메서드를 호출한 쪽의 콜스택으로까지 전이하는 결과를 낳을 수 있습니다.
static <T> T[] pickTwo(T a, T b, T c) {
switch (ThreadLocalRandom.current().nextInt(3)) {
case 0: return toArray(a, b);
case 1: return toArray(a, c);
case 2: return toArray(b, c);
}
throw new AssertionError();
}
=> pickTwo 메서드는 내부에서 toArray 메서드를 호출합니다. 이 메서드가 toArray()에 넘길 배열의 타입은 Object[]가 되는데, pickTwo에 어떤 타입의 객체를 넘기더라도 담을 수 있는 구체적인 타입이기 때문입니다. 만약 toArray에 String 값이 넘어가면 반환값이 String[] 으로 확실해지지만, 위의 예제처럼 제네릭 T 타입를 넘긴 경우 T가 어떤 타입이어도 대응해야 하기 때문에 Object[]가 반환되야 한다는 것입니다.
public static void main(String[] args) {
String[] attributes = pickTwo("좋은", "빠른", "저렴한");
}
=> 따라서 위 메서드는 아무 문제가 없는 메서드이니 별다른 경고 없이 컴파일 되지만, 결국 pickTwo의 반환타입이 Object[]라 String[] 으로 자동 형변환되어 ClassCastException 이 발생합니다. 정리하자면 제네릭 varargs(T…) 매개변수를 그대로 반환하고 또 제네릭을 이용해 반환하는 메서드(pickTwo)로 인해 ClassCastException 이 발생한 것입니다.
static <T> List<T> flatten(List<? extends T>... lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists) {
result.addAll(list);
}
return result;
}
또 다른 방법으로는 제네릭 가변인자를 사용하지 않고 List로 대체하는 경우입니다.
static <T> List<T> flatten(List<List<? extends T>> lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists) {
result.addAll(list);
}
return result;
}
List<String> audience = flatten(List.of(friends, romans, countrymen));
static <T> List<T> pickTwo(T a, T b, T c) {
switch (ThreadLocalRandom.current().nextInt(3)) {
case 0: return List.of(a, b);
case 1: return List.of(a, c);
case 2: return List.of(b, c);
}
throw new AssertionError();
}
List<String> attributes = pickTwo("좋은", "빠른", "저렴한");
func dangerous(stringLists: [String]...) {
let intList: [Int] = [42]
var objects: [Any] = stringLists
objects[0] = intList
let s: String = stringLists[0][0];
}
=> 위 코드에서 stringLists를 objects 변수에 할당할 때부터 둘은 같은 객체(같은 주소값)를 바라보는게 아니라 복사가 되는 것이기 때문에 이후에 intList 값을 저장해서 일어나는 문제가 발생하지 않습니다.
=> 그리고 Swift 의 모든 컬렉션(Array,Set,Dictionary)은 Value 타입(값 타입)이므로 모든 컬렉션에서 위 문제는 발생하지 않습니다.
func toArray<T>(args: T...) -> [T] {
return args
}
func pickTwo<T>(_ a: T,_ b: T,_ c: T) -> [T]? {
switch Int.random(in: 0 ... 2) {
case 0: return toArray(args: a, b)
case 1: return toArray(args: b, c)
case 2: return toArray(args: c, a)
default:
return nil
}
}
let attributes = pickTwo("a", "b", "c")
// attributes의 타입은 [String]? 이라고 타입 추론됩니다.
=> 제네릭을 이중으로 쓴 위의 예시도 타입 추론이 된 걸 보면 스위프트는 자바와 달리 컴파일 타임에 모든 타입이 정확히 추론됨을 알 수 있습니다.
public func print(_ items: Any..., separator: String = " ", terminator: String = "\n")
func foo(_ items: Int...) { } // succeed
func foo(_ items: Int..., others: Int...) { } // compile error! Only a single variadic parameter '...' is permitted
func foo(_ items: Int..., _ number: Int) { } // compile error! A parameter following a variadic parameter requires a label
lightweight하게 사용할 수 있기 때문입니다.// When using a variadic parameter, any number of arguments can
// be passed, and the compiler will automatically organize them
// into an array.
func send(_ message: Message, attaching attachments: Attachment...) {
...
}
// Passing no variadic arguments:
send(message)
// Passing either a single, or multiple variadic arguments:
send(message, attaching: image)
send(message, attaching: image, document)