[Effective Java] 5장 제네릭 (2부)
정리
아이템 30. 제네릭 타입과 마찬가지로, 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기도 쉽다. 타입과 마찬가지로 메서드도 형변환 없이 사용할 수 있는 편이 좋으며, 많은 경우 그렇게 하려면 제네릭 메서드가 되어야한다. 타입과 마찬가지로, 형변환을 해줘야 하는 기존 메서드는 제네릭하게 만들어주는 것이 좋다.
아이템 31. 복잡도 조금 올라가도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다. 널리 쓰일 라이브러리를 작성한다면 와일드카드 타입을 적절히 사용해줘야 한다. 생성자는 extends를 소비자는 super를 사용한다. Comparable과 Comparator는 모두 소비자이다.
아이템 32. 가변인수와 제네릭은 같이 사용하는 것이 좋지 않다. 이유는 가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열과 제네릭의 타입 규칙이 서로 다르기 때문이다. 제네릭 varargs 매개변수는 타입 안전하지는 않지만, 허용이 된다. 이를 사용한다면 그 메서드가 타입 안전한지 확인 후 @SafeVarargs 애너테이션을 달아 사용하자.
아이템 33. 컬레션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다. 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다. 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다. 또한, 직접 구현한 키 타입도 쓸 수 있다.
내용
아이템 30. 이왕이면 제네릭 메서드를 만들라
● 타입 매개변소들을 선언하는 타입 매개변수 목록은 메서드의 제한자와 반환 타입 사이에 온다.
● 불변 객체를 여러 타입으로 활용할 수 있게 만들어야 할 때 사용하면 좋다. 제네릭은 런타임에 타입 정보가 소거되므로 하나의 객체를 어떤 타입으로든 매개변수화할 수 있다.
● 자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있다. 이것을 재귀적 타입 한정(recursive type bound)라는 개념이다. 주로 자연적 순서를 정하는 Comparable인터페이스와 함께 쓰인다. 거의 모든 타입은 자신과 같은 타입의 원소와만 비교할 수 있다.
아이템 31. 한정적 와일드카드를 사용해 API유연성을 높이라
● List<Object> List<String>은 서로 하위 타입도 상위 타입도 아니다. 이런 불공변 방식보다 유연한 방법이 필요할 때가 있는데, 그럴 때 한정적 와일드카드를 사용하면 좋다.
● 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을 게 없다. 타입을 정확히 지정해야 하는 상황으로, 이때는 와일드카드 타입을 쓰지 말아야한다.
● 다음과 같은 상황에서는 와일드카드 타입을 쓰는 것이 기본원칙이다. PECS : Producer-extends, Consumer-super , Get and Put Principle(겟풋 원칙) 이라고 부른다.
매겨변수화 타입 T가 생산자라면 <? Extends T>를 사용하고, 소비자라면 <? Super T>를 사용한다.
예를 들어 Stack에서 push의 매개변수는 Stack이 사용할 E 인스턴스를 생산하므로 매개변수의 적절한 타입은 Iterable<? extends E>이다. pop의 매개변수는 Stack으로부터 E 인스턴스를 소비하므로 매개변수의 적절한 타입은 Collection<? super E>이다.
|
● 반환 타입에는 한정적 와일드카드 타입을 사용하면 안된다. 클라이언트 코드에서도 와일드카드 아입을 써야하기 때문이다. 클래스 사용자가 와일드카드 타입을 신경 써야한다면 그 API에는 문제가 있을 가능성이 높다.
● 컴파일러가 올바른 타입을 추론하지 못할 때면 명시적 타입 인수(explicit type argument)를 사용해서 타입을 알려줘야한다.
● 인터페이스를 직접 구현하지 않고, 직접 구현한 다른 타입을 확장한 타입을 지원하기 위해 필요하다.
●메서드 선언에 타입 매개변수가 한번만 나오면 와일드카드로 대체하라. 이때, 비한정적 타입 매개변수라면 비한정적 와일드 카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸면 된다.
● 다음과 같이 직관적으로 구현한 코드가 컴파일 되지 않는다.
public void swap (List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
|
원인은 리스트의 타입이 List<?>인데, List<?>에는 null이외에는 어떤 값도 넣을 수 없다는 데 있다. 와일드카드 타입의 실제 타입을 알려주는 메서드를 private 도우미 메서드를 아래와 같이 작성하여 활용하는 방법이다.
public void swap (List<?> list, int i, int j) {
swapHelper(list, i, j);
}
public <E> swapHelper (List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
|
이 리스트에서 꺼낸 값의 타입은 항상 E이고, E타입의 값이라면 이 리스트에 넣어도 안전하기 때문에 문제를 해결할 수 있다.
아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라
● 가변인수 메서드를 호출하면 가변인수(varargs)를 담을 배열이 생성되는 이 배열이 클라이언트에 노출하는 문제가 있다. 그래서 여기에 제네릭이나 매개변수화 타입이 포함되면 컴파일 경고가 발생한다. 주로 힙 오염이 발생하여 안전성이 깨지기 때문이다.
● 보이지 않는 곳에서 형변환이 이뤄져서 ClassCastException이 발생한다.
● 제네릭 가변 매개변수를 받는 메서드를 선언할 수 있게 하는 이유는, 실무에서 매우 유용하기 때문이다.
● 자바 7에서는 @SafeVarargs 애너테이션이 추가되어 제네릭 가변인수 메서드 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있게 해준다. 즉 @SafeVarargs 애너테이션은 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치이다.
● @SafeVarargs 애너테이션을 사용하는 규칙은 다음과 같다.
1. 제네릭이나 매개변수화 타입의 varargs매개변수를 받는 모든 메서드에 @SafeVarargs 애너테이션을 사용한다.
2. 재정의할 수 없는 메서드에만 사용한다. (자바 8에선 오적 정적 메서드와 final 인스턴스 메서드에서 사용가능, 자바 9에선 private 인스턴스에서도 사용가능)
|
● 가변인수 메서드가 호출할 때 varargs 매개변수를 담는 제네릭 배열이 만들어진다. 메서드가 이 배열에 아무것도 저장하지 않고, 그 배열의 참조가 밖으로 노출되지 않는다면 즉, 그 매개변수들을 덮어쓰기 않고, 신뢰할 수 없는 코드가 배열에 접근할 수 없다면 타입은 안전하다.
● 가변 매개변수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면 그 메서드는 안전하다.
● 제네릭 가변 매개변수 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않지만 예외는 있다.
1. @SafeVarargs로 제대로 애노테이트된 또 다른 vararge 메서드에 넘기는 것은 안전하다.
2. 그저 이 배열 내용의 일부 함수를 호출만 하는 일반 메서드에 넘기는 것도 안전하다.
|
아이템 33. 타입 안전 이종 컨터이너를 고려하라
● 제네릭은 컬렉션과 단일원소 컨테이너에도 자주 사용이된다. 매개변수화되는 대상은 컨테이너 자신이다. 따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.
● 더 유연한 수단으로는 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면 된다.
● 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴을 타입 토큰(type token)이라 한다.
● 타입 안전 이종 컨테이너는 타입의 반환을 보장해주며, 모든 키의 타입이 제각각이라 일반 맵과 달리 여러가지 타입의 원소를 담을 수 있는 것을 말한다.
● 타입 안전 이종 컨테이너에서 변수 값을 꺼내올 때 Class의 cast메서드를 사용해 이 객체 참조를 Class객체가 가리키는 타입으로 동적 형변환 한다.
이 메서드는 단순히 주어진 인수가 Class 객체가 알려주는 타입의 인스턴스인지 검사 후 그대로 return 하거나 아니면 ClassCastException이 발생한다.
● cast 메서드를 사용하는 이유는 cast메서드의 시그니처가 Class클래스가 제네릭이라는 이점을 완벽히 활용하기 때문이다.
● Favorites 클래스는 2가지의 제약이 있다.
1. 악의적인 클라이언트가 Class객체를 (제네릭이 아닌) raw 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨진다. 하지만 비검사 경고가 뜰 것이다. 이를 해결하기 위해서 동적 형변환을 사용하면 타입의 안전성을 보장할 수 있다.
2. 실체화 불가 타입에는 사용할 수 없다. 슈퍼 타입 토큰(super type token)으로 해결할 수 있다. (하지만 책에선 완벽한 우회로는 없다고 이야기를 했다.)
|
● Favorites가 사용하는 타입 토근은 비한정적이다. 즉, getFavorite와 putFavorite은 어떤 객체든 받아드린다. 허용하는 타입을 제한하고자 할땐, 한정적 타입 토큰을 활용하면 된다.
● Class<?> 타입의 객체가 있고, 이를 한정적 타입 토큰을 받는 메서드에 넘기려면 asSubClass를 사용하면 된다. 호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형변환한다. 형변환했다는 것은 이 클래스가인수로 명시한 클래스의 하위 클래스라는 뜻이다.
형변환에 성공하면 반환되고 실패하면 ClassCastException이 발생한다.
용어정리
● 항등함수 : 입력 값을 수정 없이 그대로 반환하는 특별한 함수
● 타입 안전 이종 컨테이너 패턴 예
public class Favorites {
private Map <Class<?>, Objcet> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> getFavorite(Class<T> type) {
return type.cast(favorites.get(type);
}
}
|
와일드카드 타입이 중첩(nested)되어 있다. 맵이 아니라 키가 와일드 카드인 것이다. 이렇게 모든 키가 서로 다른 매개변수화 타입일 수 있다는 뜻으로 다양한 타입을 지원할 수 있다.
● 한정적 타입 토큰 : 단순히 한정적 타입 매개변수나 한정적 와일드카드를 사용하여 표현 가능한 타입을 제한하는 타입토큰이다.
아이템 30 ~ 33까지 제네릭을 사용할 때 타입의 안전성을 중시하고 보장하는 것을 강조하고 있다. 그래서 한정적 와일드카드를 사용하여 타입의 유연성과 안전성을 가져가라는 것도 이런한 이유이다. @SafeVarargs을 통해 해당 API가 타입이 안전하다는 것을 보장하고 제공하는 것이 좋다.
댓글
댓글 쓰기