앞서 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)