아이템 26 - 로 타입은 사용하지 말라
•
클래스와 인터페이스 선언에 타입 매개변수(type parameter)가 쓰이면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라 한다.
•
제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입이라 한다.
•
제네릭 타입은 일련의 매개변수화 타입을 정의한다.
로타입(raw type)
•
제네릭 타입을 하나 정의하면 그에 딸린 로 타입도 함께 정의된다.
•
로 타입이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다.
•
List<E>의 로 타입은 List다.
위험한 이유
•
컴파일 타임에 타입체크가 이뤄지지 않아 의도하지 않은 타입이 들어갈 수 있다.
•
이는 컬렉션에서 아이템을 가져와 형변환해 사용하면 런타임 에러인 ClassCastException를 Throw하게 된다.
•
즉 로 타입을 사용하게 된다면 제네릭이 안겨주는 안전성과 표현력을 모두 읽게 된다.
•
로 타입을 지원하는 이유는 호환성 때문이다.
비한정적 와일드 카드
•
로 타입을 사용하는 대신 사용할 수 있다.
•
매개변수에 물음표(?) 비한정적 와일드 카드를 넣어서 사용한다면 불변성을 유지할 수 있다.
◦
Collection 프레임워크에 값을 추가하려고 한다면 null을 제외한 모든 값들은 컴파일 타임에서 에러를 잡아낼 수 있다.
◦
즉! 비한정적 와일드 카드를 사용하면 타입 세이프티하게 제네릭을 이용할 수 있다.
로 타입사용 예외
•
class 리터럴에는 로 타입을 사용해야 한다.
◦
자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못하게 했다.
if (o instanceof Set) {
Set<?> s = (Set<?>) o;
...
}
Java
복사
로 타입을 써도 좋은 예시
아이템 27 - 비검사 경고를 제거하라
•
제네릭을 사용하기 시작하면 수많은 컴파일러 경고를 보게 될 것이다.
•
비검사 형변환 경고, 비검사 메서드 호출 경고, 비검사 매개변수화 가변인수 타입경고, 비검사 변환 경고 등
•
대부분의 비검사 경고는 쉽게 제거할 수 있다.
할 수 있는 한 모든 비검사 경고를 제거하라.
•
비검사 경고를 제거하면 ClassCastException이 발생할 일이 없고, 의도한 대로 코드가 작동할 것이다.
•
경고를 제거할 수 없지만 Type Safety를 확신할 수 있다면 @SuppressWarnings("unchecked") 애너테이션을 달고 경고를 숨기자.
•
@SuppressWarnings 애너테이션은 항상 가능한 한 좁은 범위에 적용하자.
◦
변수 선언, 아주 짧은 메서드, 혹은 생성자
•
@SuppressWarnings 애너테이션을 사용해 경고를 숨긴 이유에 대해서 주석으로 적어두자.
아이템 28 - 배열보다는 리스트를 사용하라
•
배열과 제네릭 타입의 중요한 차이는 두 가지 있다.
배열은 공변(covariant), 제네릭은 불공변(invariant)이다.
•
제네릭은 서로 다른 타입일시 하위 타입도 상위 타입도 아니다.
◦
List<A> ≠ List<B>
Object[] objectArr = new Long[1];
objectArr[0] = "다른 타입"; // ArrayStoreException을 던진다
Java
복사
런타임에서 실패하는 코드
List<Object> ol = new ArrayList<Long>();
ol.add("다른 타입");
Java
복사
컴파일 타임에서 실패하는 코드
배열은 실체화(reify)되고, 제네릭은 타입 정보가 런타임에는 소거(erasure)된다.
•
배열은 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
•
제네릭은 원소 타입을 컴파일타임에만 검사하며 런타임에는 알수조차 없다는 뜻이다.
제네릭 배열이 허용된다면?
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)
Java
복사
•
(2)는 원소가 하나인 List<Integer>를 생성한다.
•
(3)은 (1)에서 생성한 List<String>[]을 Object[]에 할당한다.
•
(4)는 (2)에서 생성한 List<Integer>의 인스턴스를 Object[]의 첫 원소로 저장한다.
•
(5)는 이 배열의 처음 리스트에서 첫 원소를 꺼내려 한다.
◦
꺼낸 원소를 자동으로 String으로 형변환 하는데, 이 원소는 Integer이므로 런타임에 ClassCastException이 발생한다.
⇒ 때문에 (1)은 컴파일 타임에서 에러가 발생한다.
실체화 불가타입
•
E, List<E>, List<String> 같은 타입을 실체화 불가 타입이라 한다.
•
실체화되지 않아서 런타임에는 컴파일타임보다 타입정보를 적게 가지는 타입이다.
•
소거 메커니즘 때문에 매개변후화 타입 가운데 실체화될 수 있는 타입은 List<?>와 Map<?, ?> 같은 비한정적 와일드카드 타입뿐이다.
•
배열보다는 리스트를 우선시하자
아이템 29 - 이왕이면 제네릭 타입으로 만들라
public class Stack<E> {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
@SuppressWarnings("unchecked")
E result = (E) elements[--size];
elements[size] = null;
return result;
}
...
}
Java
복사
아이템 7의 단순한 스택을 제네릭 타입으로 변경한 코드
•
제네릭은 실체화 불가 타입으로 배열 생성 안된다.
◦
Object 배열 생성하고 제네릭 배열로 형변환하는 것으로 위회할 수 있다.
◦
형변환을 원소를 읽을때마다 해줘야 한다.
•
제네릭 타입 안에서 리스트를 사용하는게 항상 가능하지도, 꼭 더 좋은 것도 아니다.
•
자바가 리스트를 기본 타입으로 제공하지 않으므로 ArrayList 같은 제네릭 타입도 결국은 기본 타입인 배열을 사용해 구현해야 한다.
•
HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용하기도 한다.
•
해당 예제는 기본 제공되는 제네릭 타입이 아닌 완전히 새로운 제네릭 타입을 만드는 과정을 다뤘다.
아이템 30 - 이왕이면 제네릭 메서드로 만들라
•
클래스와 마찬가지로, 메서드도 제네릭으로 만들 수 있다.
•
매개변수화 타입을 받는 정적 유틸리티 메서드는 보통 제네릭이다.
public static <E> Set<E> union (Set<E> s1, Set<E> s2){
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
Java
복사
•
타입 매개변수 목록은 메서드의 제한자와 반환 타입 사이에 온다.
•
모든 타입이 같다면 한정적 와일드카드 타입을 사용해 더 유연하게 개선할 수 있다.
•
제네릭은 런타임에 타입 정보가 소거되므로 하나의 객체를 어떤 타입으로든 매개변수화할 수 있다.
제네릭 싱글턴 팩터리
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
@SuppressWarinings("unchecked")
public static <T> UnaryOperator<T> identityFunction(){
return (UnaryOperator<T>) IDENTITY_FN;
}
Java
복사
제네릭 싱글턴 팩터리 패턴
•
항등함수는 입력 값을 수정 없이 그대로 반환하는 특별한 함수이므로 T가 어떤 타입이든 UnaryOperator<T>를 사용해도 타입 안전하다.
재귀적 타입 한정
public interface Comparable<T>{
int compareTo(T o);
}
Java
복사
public static <E extends Comparable<E>> E max(Collection<E> c);
Java
복사
재귀적 타입 한정을 이용해 상호 비교할 수 있음을 표현했다.
•
상대적으로 드물지만, 자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있다.
•
Comparable을 구현한 원소의 컬렉션을 입력받는 메서드들은 정렬, 검색등 기능을 수행하는데 이런 기능을 수행하기 위해 컬렉션에 담긴 모든 원소가 상호 비교되어야 한다.
아이템 31 - 한정적 와일드카드를 사용해 API 유연성을 높이라
•
매개변수화 타입은 불공변이다.
•
즉 서로 다른 타입 A와 B가 있을때 List<A>은 List<B>의 하위 타입도 상위 타입도 아니다.
하지만 때론 불공변 방식보다 유연한 무언가가 필요하다.
생산자 코드
public void pushAll(Iterable<E> src) {
for (E e : src) {
push(E);
}
}
public static void main(String[] args) {
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> iterable = ...;
numberStack.pushAll(iterable);
}
Java
복사
•
Integer는 Number의 하위타입으로 위 코드는 논리적으로 잘 동작해야 할것 같지만 실제로는 타입 에러가 나게 된다.
•
이러한 상황에 한정적 와일드카드 타입이 유용하게 사용될 수 있다.
public void pushAll(Iterable<? extends E> src) {
for (E e : src) {
push(E);
}
}
Java
복사
•
Iterable<? extends E>매개변수는 Iterable이 E가 아닌 E의 하위 타입이 Iterable이여아 한다는 의미를 가지게 된다.
•
이렇게 한정적 와일드카드를 사용하면 타입이 안전해지므로 클라이언트 코드가 정상적으로 컴파일된다.
소비자 코드
public void popAll(Collection<E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
...
public static void main(String[] args) {
Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = ...;
numberStack.popAll(objects);
}
Java
복사
•
이번 상황 역시 Number와 Integer간 타입 오류가 발생한다.
public void popAll(Collection<? super E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
Java
복사
•
Collection<? super E> 매개변수는 E의 상위타입의 Collection을 받을 수 있게 되었다.
PECS
•
producer - extends
•
consumer - super
•
제대로만 사용한다면 와일드카드 타입이 쓰였다는 사실조차 인식하지 못할것이다.
◦
클래스 사용자가 와일드카드 타입을 신경써야 한다면 그 API에 무슨 문제가 있을 가능성이 크다.
정리
•
메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체하라.
•
유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하자.
•
Comparable과 Comparator는 소비자라는 사실도 기억하자.
아이템 32 - 제네릭과 가변인수를 함께 쓸 때는 신중하라
•
가변인수 메서드와 제네릭은 자바 5때 함께 추가되었으니 서로 잘 어우러지리라 기대하겠지만, 슬프게도 그렇지 않다.
•
가변인수는 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 해주는데, 구현 방식에 허점이 있다.
•
가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어진다.
•
매개변수에 제네릭이나 매개변수화 타입이 포함되면서 컴파일 경고가 발생하게 된다.
◦
경고의 원인은 매개변수화 타입의 변수가 타입이 다른 객체를 참조하면서 힙 오염이 발생하기 때문이다.
제네릭 가변인자의 모순
•
제네릭 배열을 프로그래머가 직접 생성하는건 허용하지 않으면서 제네릭 가변인자 매개변수를 받는 메서드를 선언할 수 있는건 분명 모순적이다.
•
이런 모순을 수용한 이유는 제네릭이나 매개변수화 타입의 가변인자 매개변수를 받는 메서드가 실무에서 유용하기 때문이다.
•
대표적인 예시로 Arrays.asList(T... a), Collections.addAll(Collection<? super T> c, T... elements), EnumSet.of(E first, E...rest)가 있다.
안전 가변인자를 나타내는 @SafeVarargs
•
자바 7에서 @SafeVarargs에너테이션이 추가되어 제니릭 가변인수 메서드 작성자가 클라이언트 픅에서 발생하는 경고를 숨길 수 있게 되었다.
•
@SafeVarargs애너테이션은 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치다.
타입 안전한 상황
static <T> T[] toArray(T... args){
return args;
}
Java
복사
매개변수를 밖으로 꺼내는 행위는 타입 안전하지 않다.
•
매개변수 배열이 호출자로부터 순수하게 인수를 전달하는 일만 한다면 타입 안전하다.
•
매개변수 배열에 다른 메서드가 접근하도록 허용한다면 안전하지 않다.
◦
@SafeVarargs로 제대로 애노테이드 된 다른 가변배열 메서드에 넘기는 것은 안전하다.
◦
그저 이 배열 내용의 일부 함수를 호출만 하는 일반 메서드로 넘기는 것도 안전하다.
@SafeVarargs 사용규칙
•
제네릭이나 매개변수화 타입의 가변인자 매개변수를 받는 모든 메서드에 @SafeVarargs를 달자.
◦
타입 안전하지 않은 가변인자 메서드에는 절대 사용해서는 안된다.
@SafeVarargs 사용 판단 기준
•
가변인자 매개변수 배열에 메서드에서 아무것도 저장하지 않는다.
•
그 배열(혹은 복제본)을 신뢰할 수 없는 코드(클라이언트)에 노출하지 않는다.
아이템 33 - 타입 안전 이종 컨테이너를 고려하라
•
제네릭은 Set<E>, Map<K, V> 등의 컬렉션과 ThreadLocal<T>, AtomicReference<T> 등의 단일원소 컨테이너에도 흔히 쓰인다.
•
이런 모든 쓰임에서 매개변수화되는 대상은 (원소가 아닌) 컨테이너 자신이다.
•
따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.
더 유연하게 사용하고 싶다면 이종 컨테이너 패턴을 사용하자
•
타입 별로 즐겨 찾는 인스턴스를 저장하고 검색할 수 있는 Favorites 클래스를 생각해보자
•
각 타입의 Class 객체를 매개변수화한 키 역할로 사용하면 되는데, 이 방식이 동작하는 이유는 class의 클래스가 제네릭이기 때문이다.
•
class의 리터럴 타입은 Class가 아닌 Class<T>다.
◦
String.class의 타입은 Class<String>이고 Integer.class의 타입은 Class<Integer>이다.
•
컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴을 타입 토큰(type token)이라 한다.
public class Favorites {
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}
public static void main(String[] args) {
Favorites favorites = new Favorites();
favorites.putFavorite(String.class, "Java");
favorites.putFavorite(Integer.class, 0xcafebabe);
favorites.putFavorite(Class.class, Favorites.class);
String favoriteString = favorites.getFavorite(String.class);
int favoriteInteger = favorites.getFavorite(Integer.class);
Class<?> favoriteClass = favorites.getFavorite(Class.class);
System.out.printf("%s %x %s", favoriteString, favoriteInteger, favoriteClass.getName());
}
Java
복사
타입 안전 이종 컨테이너 패턴
•
Favorites 인스턴스는 타입 안전하다.
◦
String 타입을 요청했는데 Integer를 반환하는일은 절대 없기 때문이다.
•
모든 키의 타입이 제각각이라, 일반적인 맵과 달리 여러 가지 타입의 원소를 담을 수 있다.
•
따라서 Favorites는 타입 안전 이종 컨테이너라 할 만하다.
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
Java
복사
Favorites 구현 코드
•
favorites의 타입은 `Map<Class<?>, Object>이다.
◦
비한정적 와일드카드 타입이라 이 맵 안에 아무것도 넣을 수 없다고 생각할 수 있지만, 사실은 그 반대다.
◦
와일드카드 타입이 중첩되어 맵이 아닌 키가 와일드카드 타입인 것
•
favorites의 값은 Object기 때문에 키와 값 사이의 타입 관계를 보증하지 않는다.
◦
하지만 값을 가져올때 타입캐스팅을 넣을 수 있다.
타입 안정 이종컨테이너의 제약사항
•
악의적인 클라이언트카 Class객체를 제네릭이 아닌 로타입으로 넘기면 Favorites인스턴스의 타입 안전성이 쉽게 깨진다.
◦
이는 HashSet과 HashMap등의 일반 컬렉션 구현체도 똑같은 문제가 있다.
◦
Favorites객체가 타입 불변식을 어기는 일이 없도록 보장하려면 putFavorite() 메서드에서 인수루 주어진 instance의 타입이 type으로 명시한 타입과 같은지 확인하면 된다.
•
실체화 불가 타입에는 사용할 수 없다.
◦
String이나 String[]은 사용할 수 있지만 List<String>은 실체화 불가 타입이므로 사용할 수 없다.
◦
왜냐하면 List<String>는 List.class와 같은 객체를 공유하기 때문이다.
한정적 타입 토큰
•
Favorites가 사용하는 타입 토큰은 비한정적이다.
◦
getFavorite와 putFavorite는 어떤 Class객체든 받아들인다.
•
한정적 타입 토큰이란 단순히 한정적 타입 매개변수나 한정적 와일드카드를 사용하여 표현 가능한 타입을 제한하는 타입 토큰이다.
•
애너테이션 API는 한정적 타입 토큰을 적극적으로 사용한다.
•
예를 들어 AnnotatedElement 인터페이스에 선언된 메서드로, 대상 요소에 달려 있는 애너테이션을 런타임에 읽어 오는 기능을 한다.
◦
이 메서드는 리플렉션의 대상이 되는 타입들, 즉 클래스(java.lang.Class<T>), 메서드(java.lang.reflect.Method), 필드(java.lang.reflect.Field) 같이 프로그램 요소를 표현하는 타입들에서 구현한다.
public <T exnteds Annotation> T getAnnotation(Class<T> annotationType);
Java
복사
•
해당 메서드는 토큰으로 명시한 타입의 애너테이션이 대상 요소에 달려 있다면 그 애너테이션을 반환하고, 없다면 null을 반환한다.
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName){
Class<?> annotationType = null; // 비한정적 타입 토큰
try {
annotationType = Class.forName(annotationTypeName);
}catch (Exception exception){
throw new IllegalArgumentException(exception);
}
return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}
Java
복사
•
컴파일 시점에는 타입을 알 수 없는 애너테이션을 asSubclass 메서드를 사용해 런타임에 읽어내는 메소드