item 28 : 배열보다는 리스트를 사용하라
Last updated
Last updated
배열을 꼭 반환해야 하는 상황이 아니라면 을 사용하자..!
그 이유가 뭘까?
item 26에서 한 번 정리했던 내용
배열은 공변(covariant)다. 공변성
이란 자신이 상속받은 부모 객체로 타입을 변화시킬 수 있다라는 것을 뜻한다.
Sub라는 클래스가 Super라는 클래스의 하위클래스라고 할때, 배열 Sub[]
는 Super[]
의 하위타입이 된다.
이것을 시도하면 런타임에 오류가 난다.
반면 제네릭은 불공변이다. List<Type1>
은 List<Type2>
의 하위 타입도 아니고 상위 타입도 아니다.
리스트
를 사용하면 애초에 컴파일 시에 오류가 난다. 어느쪽이든 Long용 저장소에 String은 못넣는다. 하지만 컴파일 에러는 가장 값이 싼 오류다.
정리하자면,
공변(convariant) : A가 B의 하위 타입일 때, T<A>가 T<B>의 하위 타입이면 T는 공변이라고 한다. 타입 A가 B의 하위 타입(subtype)이라면, A[]
는 B[]
의 하위 타입이 될 수 있는 성질이다.
불공변(invariant) : A가 B의 하위 타입일 때, T<A>가 T<B>의 하위 타입이 아니면, T는 불공변이라고 한다. 제네릭 타입 List<A>
와 List<B>
가 있을 때, A
가 B
의 하위 타입이더라도 List<A>
는 List<B>
의 하위 타입이 아니다. 즉, 제네릭 타입은 불공변성을 가진다.
배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
배열은 런타임에도 자신의 원소 타입을 인지하고, 타입 안전성을 검사할 수 있다. 즉, 배열
은 공변성을 가진다. 예를 들어, String[]
배열은 Object[]
로 사용할 수 있다.
즉, 배열
은 런타임 시 해당 오류를 알고 리스트
는 컴파일 시 바로 알 수 있다고 함
제네릭 타입은 컴파일 시점에만 타입을 검사하고, 런타임에는 타입 정보가 소거(Erasure)된다.
제네릭은 컴파일 시점에 타입 검사를 수행해 타입 안전성을 보장하지만, 런타임에는 제네릭 타입 정보가 사라진다. 이는 제네릭이 도입되기 전의 레거시 코드와 호환성을 유지하기 위한 메커니즘이다.
제네릭 배열 생성이 허용되지 않는 이유는 타입 안전하지 않기 때문이다.
제네릭 배열을 생성하면 런타임에 ClassCastException
이 발생할 수 있다. 이는 제네릭의 핵심 목표인 "런타임에 타입 안전성을 보장한다"는 취지에 어긋나게 된다.
배열은 공변이기 때문에, 예를 들어 String[]
배열을 Object[]
로 취급할 수 있지만, 제네릭 타입은 불공변이어서 List<String>
과 List<Object>
는 아무런 관계가 없다.
이상의 주요 차이로 인해 배열과 제네릭은 잘 어우러지지 못한다. 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
즉, 코드를 new List[], new List[], new E[] 식으로 작성하면 컴파일할 때 제네릭 배열 생성 오류를 일으킨다.
1번 라인: List<String>[]
배열을 생성한다고 가정하자. 이는 제네릭 배열 생성을 허용하지 않기 때문에, 실제로는 컴파일 오류가 발생한다.
2번 라인: List<Integer>
를 생성한다. 이는 정상적인 코드다.
3번 라인: List<String>[]
를 Object[]
로 할당한다. 배열은 공변이므로 문제가 없다.
4번 라인: List<Integer>
를 Object[]
의 첫 원소로 추가한다. 컴파일러는 이를 허용하며, 런타임에 List<Integer>
는 단순히 List
로 간주된다. 제네릭은 소거 방식으로 구현되어서 이 역시 성공한다.
5번 라인: stringLists[0]
에는 원래 List<String>
이 들어 있어야 하는데, 실제로는 List<Integer>
가 들어 있다. 컴파일러는 꺼낸 원소를 자동으로 String으로 형변환하는데, 이 원소는 Integer이므로 stringLists[0].get(0)
에서 String
타입으로 가져오려고 시도하면 ClassCastException
이 발생할 수 있다.
런타임에 타입 정보를 유지하지 않는 제네릭 타입을 의미하며, 제네릭 배열 생성과 관련된 문제의 원인이다.
실체화 불가 타입은 컴파일 시점보다 런타임에 타입 정보가 소거된 타입을 의미한다. 예를 들어, E
, List<E>
, List<String>
같은 제네릭 타입은 런타임에는 타입 정보를 유지하지 않는다.
제네릭 타입은 런타임에 실체화되지 않기 때문에, 제네릭 배열을 생성하면 런타임에 ClassCastException
이 발생할 가능성이 커진다.
실체화될 수 있는 타입은 List<?>
, Map<?,?>
와 같은 비한정적 와일드카드 타입뿐이다. 비한정적 와일드카드 타입은 런타임에 타입 정보가 소거되어도 안전하게 사용할 수 있다.
제네릭 배열 생성이 불가능하므로, 배열 대신 컬렉션(
List<E>
)을 사용하는 것이 더 안전하다.
배열을 사용하려고 할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 발생하면, 배열 대신 컬렉션을 사용하면 해결될 수 있다.
컬렉션은 제네릭 타입을 지원하며, 배열보다 유연하게 사용할 수 있다.
제네릭 가변인수 메서드는 제네릭 타입 매개변수를 사용하면서, 를 지원하는 메서드를 의미이다.
가변인수 메서드는 메서드를 호출할 때 임의의 개수의 인자를 전달할 수 있는 기능이다. 인자의 개수를 정하지 않고 여러 개의 인수를 받을 수 있다.
제네릭 가변인수 메서드를 사용하면 제네릭 타입의 여러 개의 인수를 전달할 수 있다.
<T>
: 제네릭 타입 매개변수 T
를 사용하여, 메서드의 인수 타입을 유연하게 지정하다.
T
는 호출 시점에 타입이 결정되는 제네릭 타입이다. 예를 들어, String
이나 Integer
등이 될 수 있다.
가변인수 T... elements
:
T
타입의 가변인수를 받는다. 이로 인해 임의의 개수의 인수를 전달할 수 있다. 전달된 인수는 배열로 처리된다.
@SafeVarargs
애너테이션:
이 애너테이션은 제네릭 가변인수 메서드에서 발생하는 경고를 억제하다. 제네릭 타입의 가변인수는 컴파일 시 경고가 발생할 수 있기 때문에, 메서드가 타입 안전함을 보장할 수 있을 때 사용해야 한다.
Arrays.asList(elements)
:
전달된 가변인수 배열을 리스트로 변환
main
메서드:
toList
메서드를 사용해 여러 개의 String
이나 Integer
타입의 인수를 전달하여 리스트를 생성
가변인수는 배열로 처리:
메서드 내부에서 가변인수 매개변수는 배열로 간주되며, 배열의 길이와 요소에 접근할 수 있다.
하나의 가변인수만 사용 가능:
메서드의 매개변수 목록에서 하나의 가변인수만 사용할 수 있으며, 항상 마지막에 위치해야 한다.
예를 들어, public void example(int fixed, String... varargs)
와 같이 가변인수는 마지막 매개변수여야 한다.
인수를 전달하지 않을 수 있음:
가변인수는 호출 시 인수를 하나도 전달하지 않아도 된다. 이 경우, 메서드 내부에서 해당 배열의 길이는 0
이 된다.
제네릭 타입과 가변인수 메서드(varargs method)를 함께 사용하면 경고 메시지가 발생할 수 있다. 가변인수 메서드를 호출할 때마다 매개변수를 담을 배열이 생성되는데, 그 배열의 원소가 실체화 불가 타입일 경우 경고가 발생하다.
이 문제는 @SafeVarargs
애너테이션을 사용하여 해결할 수 있다. @SafeVarargs
는 메서드의 가변인수 매개변수가 안전하다고 확신될 때 사용한다.
아래 예제에서는 가변인수를 사용해 여러 개의 리스트를 결합하는 메서드를 정의하고,
@SafeVarargs
애너테이션을 사용해 경고를 억제한다.
@SafeVarargs
애너테이션:
combineLists
메서드는 제네릭 가변인수 메서드이기 때문에, @SafeVarargs
애너테이션을 사용하여 경고를 억제한다.
이 애너테이션은 메서드가 안전하게 동작한다고 확신할 때만 사용해야 한다. 즉, 배열을 조작하거나 노출하지 않는 등, 타입 안전성을 깨뜨리지 않는 경우에만 사용해야 한다.
combineLists
메서드:
이 메서드는 가변인수로 전달된 여러 List<T>
를 하나의 List<T>
로 결합하여 반환한다.
가변인수 List<T>... lists
를 사용하면 호출할 때 여러 개의 리스트를 전달할 수 있다.
각 리스트를 순회하면서, result
리스트에 모든 요소를 추가
main
메서드:
여러 개의 리스트를 생성하고, combineLists
메서드를 사용하여 결합한다.
결합된 리스트는 [Apple, Banana, Carrot, Date, Eggplant, Fig]
로 출력된다.
@SafeVarargs
의 필요성가변인수 메서드를 호출할 때마다 매개변수를 담을 배열이 생성되는데, 그 배열의 타입이 실체화 불가 타입(제네릭 타입)일 경우 컴파일러가 경고를 발생시킨다.
@SafeVarargs
는 메서드가 타입 안전성을 보장할 수 있는 경우에만 사용해야 하며, 배열 조작이나 노출이 없는 경우에 사용하여 경고를 억제할 수 있다.
E[]
대신 컬렉션 List<E>
배열로 형변환할 때 오류나 경고가 뜨는 경우 E[]
대신 컬렉션 List<E>
를 사용하면 해결된다. 코드가 복잡해지고 성능이 살짝 나빠질 수 있지만, 타입 안정성과 상호운용성은 좋아진다.
이 클래스를 사용하려면 choose 메서드를 호출할 때마다 반환된 Object를 원한는 타입으로 형변환해야 한다. 만약 다른 타입의 원소가 들어있었다면 런타임에 형변환 오류가 날 것이다. 제네릭으로 만들어보자.
위의 코드를 컴파일하면 오류 메시지가 출력된다.
Object 배열을 T 배열로 변환해도 또 다른 경고가 뜬다. T가 무슨 타입인지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전을 보장하지 못한다는 것이다. 제네릭에서는 원소의 타입 정보가 소거되어 런타입에는 무슨 타입인지 알 수 없다. 동작은 하지만 원인을 제거한 것은 아니다.
이제 런타임에 ClassCastException을 만날 일이 없다.
제네릭과 배열의 차이: 배열은 런타임에 타입을 인지하지만, 제네릭은 런타임에 타입 정보가 소거된다.
제네릭 배열 생성이 불가능한 이유: 타입 안전성을 보장하기 위해 제네릭 배열 생성을 막아 놓았다.
실체화 불가 타입: 런타임에 타입 정보를 유지하지 않는 제네릭 타입을 의미하며, 제네릭 배열 생성과 관련된 문제의 원인이다.
대안: 배열 대신 컬렉션(List<
>
)을 사용하는 것이 타입 안전성과 상호운용성을 높이는 방법이이다.