////
Search

5장 - 제네릭

Created
2022/09/03 11:38
Tags

아이템 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 유연성을 높이라

매개변수화 타입은 불공변이다.
즉 서로 다른 타입 AB가 있을때 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
복사
IntegerNumber의 하위타입으로 위 코드는 논리적으로 잘 동작해야 할것 같지만 실제로는 타입 에러가 나게 된다.
이러한 상황에 한정적 와일드카드 타입이 유용하게 사용될 수 있다.
public void pushAll(Iterable<? extends E> src) { for (E e : src) { push(E); } }
Java
복사
Iterable<? extends E>매개변수는 IterableE가 아닌 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
복사
이번 상황 역시 NumberInteger간 타입 오류가 발생한다.
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가 사용하는 타입 토큰은 비한정적이다.
getFavoriteputFavorite는 어떤 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 메서드를 사용해 런타임에 읽어내는 메소드