ch5 제네릭 - LenKIM/everyone-is-effective-java-study GitHub Wiki
-
클래스와 인터페이스 선언에 **타입 매개변수(type parameter)**가 쓰이면, 이를 제네릭 클래스 혹은 제네릭 인터페이스
-
제네릭 클래스와 제네릭 인터페이스를 통틀어 Generic type
-
제네릭 타입은 일련의 매개변수화 타입(parameterized type)을 정의. ex)
List<String> list여기서 꺽쇠 부분에 들어가는게 매개변수화 타입. -
다시 Raw 타입이란?
List<e> 에서 ListRaw타입을 쓰지 말라는 말은 무슨 말인가? > 제네릭 타입에서 타입 매개변수를 사용하지 않는 것을 말한다. -
로 타입은 타입선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작한다.
-
이런 타입을 만든 이유는 이는 제네릭이 도입되기 전 코드와 호환을 위함이다.
[컬렉션의 로 타입]
private final Collection stamps = ...;
stamps.add(new Coin(...)); // 엉뚱한 객체가 들어가도 컴파일 후 실행이 된다. (경고는 발생)[반복자의 로 타입]
for (Iterator i = stamps.iterator(); i.hasNext(); ) {
Stamp stamp = (Stamp) i.next(); // ClassCastException 발생
stamp.cancel();
}위와 같이 작성할 경우 Raw 타입을 활용할 경우 제네릭의 장인 타입 안전성과 코드 상의 표현력을 잃어버리게 된다.
제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않다면 물음표(?) 을 사용하자. - 비한정적 와일드 카드 사용하기
static int numElementsInCommon(Set s1, Set s2) {...} // 동작은 하지만 타입 안정적이지 못하다.
static int numElementsInCommon(Set<?> s1, Set<?> s2) {...} // 와일드카드를 사용함으로써 타입에 안정적이다.[참고자료]
https://javarevisited.blogspot.com/2012/04/what-is-bounded-and-unbounded-wildcards.html
Tip) Type parameter의 naming convention**
- E - Element (used extensively by the Java Collections Framework)
- K - Key
- N - Number
- T - Type
- V - Value
- S, U, V etc. - 2nd, 3rd, 4th types
반대로 Raw 타입을 쓰면 좋은 예?
-
예외 1: class 리터럴에는 로타입을 사용해야 한다.
ex) List.class, String[].class, int.class는 허용하고 List<String>.class 와 List<?>.class 는 허용 안한다. -
예외 2: instanceof 연산자를 이용한 타입 확인
if(o instanceof Set){ Set<?> s = (Set<?>) o; ... } // o의 타입이 Set임을 확인한 다음 와일드카드 타입인 Set<?>로 형변환해야 한다. 이는 검사 형변환이므로 컴파일러 경고가 뜨지 않는다.
제네릭을 사용하면 다양한 컴파일러 경고를 보게 된다.
- 비검사 형변환 검사
- 비검사 메서드 호출 경고
- 비검사 매개변수화 가변인수 타입경고
- 비검사 메서드 호출 경고
결론.
-
할수 있는한 모든 비검사 경고를 제거하는 것이 좋다.
-
경고를 제거할 수 없는 경우도 있으며 이럴 경우 개발자의 확신하에 @SuppressWarnings(“unchecked”) 애너테이션을 통해 경고를 숨긴다.
-
어노테이션의 적용 범위는 가능한 좁게 하는 것이좋다. 또한 경고를 무시해도 안전한 이유를 주석을 통해 명시해주는 것이 좋다.
배열과 제네릭 타입의 2가지 차이
- 배열은 공변, 즉- Sub가 Super의 하위 타입이라면 배열 Sub[]의 하위타입이 된다. 반면, 제네릭은 불공변. 즉, 서로 다른 Type1과 Type2가 있을 때, List은 List의 하위타입도 아니고 상위 타입 아님.
- 배열의 실체화(reify). 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인. 반면에 제네릭은 타입 정보가 런타임에는 소거(erasure)
즉, 원소 타입을 컴파일 타임에만 검사하며 런타임에는 알 수조차 없다는 뜻으로, 소거는 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 매커니즘.
예외도 존재) new List[], new List[], new E[] 식으로 작성하면 제네릭 배열 생성 오류 발생.
public class Chooser {
private final Object[] choiceArray;
public Chooser(Collection choices){
choiceArray = choices.toArray();
}
public Object choose(){
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}위 코드의 문제점은 choose 메서드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야 한다. 혹시나 타입이 다른 원소가 들어 있었다면 런타임에 형변환 오류가 날 것.
public class Chooser<T> {
private final T [] choiceArray;
public Chooser(Collection<T> choices){
choiceArray = choices.toArray();
// choiceArray = (T[])choices.toArray();
}
}toArray에서 형변환 에러가 발생할 것.
public class Chooser<T> {
private final List<T> choiceList;
public Chooser(Collection<T> choices) {
choiceList = new ArrayList<>(choices);
}
public T choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size()));
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3, 4, 5, 6);
Chooser<Integer> chooser = new Chooser<>(intList);
for (int i = 0; i < 10; i++) {
Number choice = chooser.choose();
System.out.println(choice);
}
}
}Tip)
배열과 제네릭에는 매우 다른 타입 규칙이 적용된다. 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다. 그 결과 배열은 런타임에는 타입 안전하지만 컴파일에는 그렇지 않다. 제네릭은 반대다. 그래서 둘이 섞어 쓰기란 쉽지 않다. 둘이 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가자아 먼저 배열을 리스트로 대체하는 방법을 적용해보자.
// E[]를 이용한 제네릭 스택 (170-174쪽)
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// 코드 29-3 배열을 사용한 코드를 제네릭으로 만드는 방법 1 (172쪽)
// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
// 따라서 타입 안전성을 보장하지만,
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
@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 = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
// 코드 29-5 제네릭 Stack을 사용하는 맛보기 프로그램 (174쪽)
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
for (String arg : args)
stack.push(arg);
while (!stack.isEmpty())
System.out.println(stack.pop().toUpperCase());
}
}일반 클래스를 제네릭 클래스로 만드는 첫 단게는 클래스 선언에 타입 매개변수를 추가하는 일, 여기서 여전히
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; 이 부분에서 경고가 발견 될 것.
- 컴파일러는 이 프로그램이 타입 안전한지 증명할 방법이 없지만 우리는 할 수 있다. 문제의 배열 elements는 private 필드에 저장되고, 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 전혀 없다. push 메서드를 통해 배열에 저장되는 원소의 타입은 항상 E다. 따라서 이 비검사 형변환은 확실히 안전. 그럼
@SuppressWarnings로 경고 숨기기.
Tip)
클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다. 그러니 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라. 그렇게 하려면 제네릭 타입으로 만들어야 할 경우가 많다. 기존 타입 중 제네릭이었어야 하는 게있다면 제니릭 타입으로 변경하자. 기존 클라이언트에는 아무 영향을 주지 않으면서, 새로운 사용자를 휠씬 편하게 해주는 길이다.
클래스와 마찬가지로, 메서드도 제네릭으로 만들 수 있다. 매개변수화 타입을 받는 정적 유틸리티 메서드는 보통 제네릭.
// 제네릭 union 메서드와 테스트 프로그램 (177쪽)
public class Union {
// 로 타입 사용 - 수용 불가!!!!
public static Set union(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
위 코드에서는 컴파일은 되지만 경고가 두 개 발생. 타입 안전하지 않다는 이유로
// 코드 30-2 제네릭 메서드 (177쪽)
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
// 코드 30-3 제네릭 메서드를 활용하는 간단한 프로그램 (177쪽)
public static void main(String[] args) {
Set<String> guys = Set.of("톰", "딕", "해리");
Set<String> stooges = Set.of("래리", "모에", "컬리");
Set<String> aflCio = union(guys, stooges);
System.out.println(aflCio);
}
}메서드 선언에서 세 집합(입력 2개, 반환 1개)의 원소 타입을 타입 매개변수로 명시하고, 메서드 안에서도 이 타입 매개변수만 사용하게 수정하면 된다.
(타입 매개변수들을 선언하는) 타입 매개변수 목록은 메서드의 제한자와 반환 타입 사이에 온다.
**타입 매개변수 목록은 <E>**이고 반환 타입은 Set<E> 이다.타입 매개변수의 명명 규칙은 제네릭 메서드나 제네릭 타
입이나 똑같다.
때때로 불변 객체를 여러 타입으로 활용할 수 있게 만들어야 할 때가 있다. 제네릭은 런타임에 타입 정보가 소거되므로 하나의 객체를 어떤 타입으로든 매개변수화할 수 있다. 하지만 이렇게 하려면 요청한 타입 매개변수에 맞게 매번 그 객체의 타입을 바꿔주는 정적 팩토리를 만들어야 한다.
항등 함수 객체(Function.identity)
package effectivejava.chapter5.item30;
import java.util.function.UnaryOperator;
// 제네릭 싱글턴 팩터리 패턴 (178쪽)
public class GenericSingletonFactory {
// 코드 30-4 제네릭 싱글턴 팩터리 패턴 (178쪽)
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
return (UnaryOperator<T>) IDENTITY_FN;
}
// 코드 30-5 제네릭 싱글턴을 사용하는 예 (178쪽)
public static void main(String[] args) {
String[] strings = { "삼베", "대마", "나일론" };
UnaryOperator<String> sameString = identityFunction();
for (String s : strings)
System.out.println(sameString.apply(s));
Number[] numbers = { 1, 2.0, 3L };
UnaryOperator<Number> sameNumber = identityFunction();
for (Number n : numbers)
System.out.println(sameNumber.apply(n));
}
}자기자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수있음. 바로 재귀적 타입 한정(recursive type bound)
public interface Comparable<T>{
int compareTo(T o);
}// 재귀적 타입 한전을 이용해 상호 비교할 수 있음을 표현
public static <E extends Comparable<E>> E max(Collection<E> c); 모든 타입 E는 자신과 비교할 수 있다.
재귀적 타입 한정은 휠씬 복잡해질 가능성이 있긴 하지만, 다행히 그런 일은 잘 일어나지 않는다. 이번 아이템에서 설명한 관용구, 여기에 와일드 카드를 사용한 변형, 그리고 시뮬레이트한 셀프 타입 관용구를 이해하고 나면 실전에서 마주치는 대부분의 재귀적 타입 한정을 무리없이 다룰 수 있다.
매개변수화 타입은 불공변(Invariant)이다. 즉, 서로 다른 타입 Type1과 Type2가 있을 때 List<Type1>은 List<Type2>의 하위 타입도 상위 타입도 아니다.
그렇기 때문에, 가끔 유연성이 있는 무엇가가 필요하다. 예를 들어보자.
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
// 여기서 일련의 원소를 스택에 넣는 메서드를 추가해야 한다면?
public void pushAll(Iterable<E> src){
for(E e: src)
push(e);
}문제는 없지만, Stack로 선언한 후 pushALL을 호출하게 되면?
잘 동작되는것 처럼 여겨지지만, 매개변수화는 불공변으므로 오류 메세지가 발생한다.
이런 오류 메세지를 해결하기 위한 방법으로 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원한다. pushAll의 입력 매개변수 타입은 'E의 Iterable'이 아니라 'E의 하위 타입의 Iterable'이어야 하며, 와일드 카드 타입 Iterable<? extends E> 가 정확히 이런 뜻이다.
public void pushAll(Iterable<? exntends E> src){
for(E e: src)
push(e);메세지는 분명하다. 유연성을 극대화하려면 원소의 생성자나 소비자용 입력 매개변수에 와일드 카드 타입을 사용하라.
한편, 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을 게 없다. 타입을 정확히 지정해야 하는 상황으로, 이때는 와일드카드 타입을 쓰지 말아야 한다.
PECS: producer-extends, consuper-super
- 매개변수화 타입 T가 생산자라면 <? extends T> 를 사용, 소비자라면 <? super T>를 사용
- Stack 예에서 pushAll의 src 매개변수는 Stack이 사용할 E 인스턴스를 생산하므로 src의 적절한 타입은 Iterable<? extends E>
- popAll의 dst 매개변수는 Stack으로부터 E 인스턴스를 소비하므로 dst의 적절한 타입은 Collection<? super E> 이다.
package effectivejava.chapter5.item31;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Random;
// T 생산자 매개변수에 와일드카드 타입 적용 (184쪽)
public class Chooser<T> {
private final List<T> choiceList;
private final Random rnd = new Random();
// 코드 31-5 T 생산자 매개변수에 와일드카드 타입 적용 (184쪽)
public Chooser(Collection<? extends T> choices) {
choiceList = new ArrayList<>(choices);
}
public T choose() {
return choiceList.get(rnd.nextInt(choiceList.size()));
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3, 4, 5, 6);
Chooser<Number> chooser = new Chooser<>(intList);
for (int i = 0; i < 10; i++) {
Number choice = chooser.choose();
System.out.println(choice);
}
}
}// union 메서드
package effectivejava.chapter5.item31;
import java.util.*;
// 코드 30-2의 제네릭 union 메서드에 와일드카드 타입을 적용해 유연성을 높였다. (185-186쪽)
public class Union {
public static <E> Set<E> union(Set<? extends E> s1,
Set<? extends E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
// 향상된 유연성을 확인해주는 맛보기 프로그램 (185쪽)
public static void main(String[] args) {
Set<Integer> integers = new HashSet<>();
integers.add(1);
integers.add(3);
integers.add(5);
Set<Double> doubles = new HashSet<>();
doubles.add(2.0);
doubles.add(4.0);
doubles.add(6.0);
Set<Number> numbers = union(integers, doubles);
// // 코드 31-6 자바 7까지는 명시적 타입 인수를 사용해야 한다. (186쪽)
// Set<Number> numbers = Union.<Number>union(integers, doubles);
System.out.println(numbers);
}
}public static <E> Set<E> union(Set<? extends E> s1,Set<? extends E> s2)
tip)
반환 타입은 여전히 Set<E>임을 주목. 반환 타입에는 한정적 와일드카드 타입을 사용하면 안된다. 유연성을 높여주기는 커녕 클라이언트 코드에서도 와일드카드 타입을 써야하기 때문이다.
public static<E extends Comparable<? super E>> E max(List<? extends E> list);
- 입력 매개변수에서는 E 인스턴스를 생산하므로 원래의 List<E>를 List<? extends> 로 수정
- Comparable는 E인스턴스를 소비한다. 그러므로, 매개변수화 타입 Comparable<E>를 한정적 와일드카드 타입인 Comparable<? super E> 로 대체.
-
가변인수는 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 해주는데, 구현 방식에 허점이 있음
-
가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 만들어 짐. 그런데 내부로 감춰야 했을 이 배열을 클라이언트에게 노출하는 문제가 생김. 그 결과 varargs 매개변수에 제네릭이나 매개변수화 타입이 포함되면 알기 어려운 컴파일 경고가 발생.
-
매개변수화 타입의 변수가 타입이 다른 객체를 참조하면 힙 오염이 발생한다.
static void dangerous(List<String> ... stringLists){ List<Integer> intList = List.of(42) Object [] objects = stringLists; object[0] = intList; // 힙 오염 발생 String s = stringLists[0].get[0]; // ClassCastExecption }
이 메서드에서는 형변환하는 곳이 보이지 않는데도 인수를 건네 호출하면 ClassCastException을 던진다. 마지막 줄에 컴파일러가 생성한(보이지 않는) 형변환이 숨어 있기 때문이다. 이처럼 타입 안전성이 깨지니 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않는다.
이런 안전하지 않음의 경고를 없애기 위해
@SafeVarargs사용. 해당 에노태이션은 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치다. -
그럼 안전하다는 사실을 어떻게 알까?
가변인수 메서드를 호출할 때
varargs 매개변수를 담는 제네릭 배열이 만들어진다는 사실을 기억. 메서드가 이 배열에 아무것도 저장하지 않고(그 매개변수들을 덮어쓰지 않고) 그 배열의 참조가 밖으로 노출되지 않는다면(신뢰할 수 없는 코드가 배열에 접근할 수 없다면) 타입 안전하다. 달리 말하면, 이 varargs 매개변수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면(varargs의 목적대로 쓰인다면) 그 메서드는 안전하다.
제네릭은 Set<E>, Map<K,V> 등의 콜렉션과 ThreadLocal<T>, AtomicReference<T> 등의 단일 원소 컨테이너에도 흔히 쓰인다.
-
컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면 된다. 이렇게 하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해줄 것이다. 이렇게 하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해줄 것이다. 이러한 설계 방식을 타입 안전 이종 컨테이너 패턴.
import java.util.*; // 타입 안전 이종 컨테이너 패턴 (199-202쪽) public class Favorites { // 코드 33-3 타입 안전 이종 컨테이너 패턴 - 구현 (200쪽) 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)); } // // 코드 33-4 동적 형변환으로 런타임 타입 안전성 확보 (202쪽) // public <T> void putFavorite(Class<T> type, T instance) { // favorites.put(Objects.requireNonNull(type), type.cast(instance)); // } // 코드 33-2 타입 안전 이종 컨테이너 패턴 - 클라이언트 (199쪽) public static void main(String[] args) { Favorites f = new Favorites(); f.putFavorite(String.class, "Java"); f.putFavorite(Integer.class, 0xcafebabe); f.putFavorite(Class.class, Favorites.class); String favoriteString = f.getFavorite(String.class); int favoriteInteger = f.getFavorite(Integer.class); Class<?> favoriteClass = f.getFavorite(Class.class); System.out.printf("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getName()); } }
정리, 컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되있다. 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다. 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다. 또한, 직접 구현한 키 타입도 쓸 수 있다. 예컨데 DB의 행(컨테이너)을 표현한 DatabaseRow 타입에는 제니릭 타입인 Column<T>키로 쓸 수 있다.
- 제네릭 타입에서 타입 매개변수를 사용하지 않는 것을 말한다.
List<E> //자바 5 이후 제네릭의 선언
List //로 타입로 타입은 타입선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작한다. 이는 제네릭이 도입되기 전 코드와 호환을 위함이다.
[컬렉션의 로 타입]
private final Collection stamps = ...;
stamps.add(new Coin(...)); // 엉뚱한 객체가 들어가도 컴파일 후 실행이 된다. (경고는 발생)[반복자의 로 타입]
for (Iterator i = stamps.iterator(); i.hasNext(); ) {
Stamp stamp = (Stamp) i.next(); // ClassCastException 발생
stamp.cancel();
}- List와 같은 로 타입의 사용은 지양해야 하지만 List<Object>는 사용하여도 무관하다. 그 이유는 List의 경우 제네릭 타입에 관여하지 않는 반면 List<Object>는 모든 타입을 허용한다는 것을 컴파일러에게 전달하기 때문이다.
- 그렇다면 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않다면 어떻게 할까?
static int numElementsInCommon(Set s1, Set s2) {...} // 동작은 하지만 타입 안정적이지 못하다.
static int numElementsInCommon(Set<?> s1, Set<?> s2) {...} // 와일드카드를 사용함으로써 타입에 안정적이다.- 예외 1: class 리터럴에는 로타입을 사용해야 한다.
- 예외 2: instanceof 연산자를 이용한 타입 확인
- 할수 있는한 모든 비검사 경고를 제거하는 것이 좋다.
- 경고를 제거할 수 없는 경우도 있으며 이럴 경우 개발자의 확신하에 @SuppressWarnings("unchecked") 애너테이션을 통해 경고를 숨긴다.
- 어노테이션의 적용 범위는 가능한 좁게 하는 것이좋다. 또한 경고를 무시해도 안전한 이유를 주석을 통해 명시해주는 것이 좋다.
- 배열은 공변이다. Sub가 Super의 하위 타입이라면 Sub[]도 Super[]도 하위 타입이다.
- 제네릭은 불공변이다. Type1과 Type2일 때 List과 List은 관계가 전혀없다.
- 예시 : new List<E>[] , new List<String>[], new E[]
- 이유 : 타입에 안전하지 않기 때문에
[예제 : 제네릭 배열 불허용하는 이유]
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)- 실제로 (1)번 부터 허용되지 않는다.
- of를 통해 원소가 하나인 initList를 생성 // (2)번의 동작
- Object 배열인 objects 에 1번에서 생성한 제네릭 배열을 할당
- (2) 번에서 생성한 원소가 하나인 initList를 objects[0]에 할당
- 배열의 첫번째 값에서 get을 이용하여 s에 할당 여기서 제네릭의 중요한 성질을 보여주고 있는데 바로 위에서 설명한 **소거(ensure)**이다. 소거 방식으로 구현된 제네릭은 런타임 시에 제네릭이 아닌 로 타입으로 인식한다. 따라서 위 예시의 런타임 시에 (1)번 List<String>[] stringLists는 <String>이 빠진 List[] stringLists로 인식된다.
[배열로 선언한 경우]
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
choiceArray = (T[]) choices.toArray();
}
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}[컬렉션 사용 예시]
public class Chooser<T> {
private final List<T> choiceArray;
public Chooser(Collection<T> choices) {
choiceList = new ArrayList<>(choices);
}
public T choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size());
}
}- 리스트 기반으로 타입 안정성 확보
- 제네릭 없이 구현한 Stack 소스의 경우 형변환 시 런타임 오류의 위험이 크다.
[제네릭을 사용한 Stack]
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITAL_CAPACITY = 16;
public Stack() {
elements = new E[DEFAULT_INITAL_CAPACITY] // 실체화 불가타입으로 배열을 만들 수 없다.
} // (E[]) new Object[DEFAULT_INITAL_CAPACITY] // 변환하여 사용
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null;
return result;
}
}- new E[]는 실체와 불가 타입이므로 배열로 만들 수 없다.
- 이를 해결하기 위해 아래와 같은 두가지 방법이 있다.
- Object 배열을 생성한 다음 제네릭 배열로 형변환 한다. 이방법은 경고를 발생 시키므로 안전함이 증명되었다면 @SuppressWarning 애너테이션으로 경고를 숨긴다. 단 힙 오염을 발생시킬 수 있다. (아이템 32.에서 설명)
- E[]에서 Object[]로 변경한다.
- 항상 가능하지도 않으면 더 좋지도 않다.
- 제네릭은 런타임에 타입 정보가 소거(ensure) 된다.
- 때문에 객체를 어떤 타입으로 매개변수화 할 수 있다.
- 제네릭 싱글턴 팩토리 : 요청한 타입 매개변수에 맞게 그 객체의 타입을 바꿔주 정적 팩토리
- 여기서 항등함수란 입력 값을 수정없이 반환하는 함수이다.
[제네릭 싱글턴 팩터리 패턴]
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
@SuppressWanrings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
return (UnaryOperator<T>) IDENTITY_FN;
}- 자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용범위를 한정하는 것
- 이는 우리가 자주 사용하는 Comparable에서 볼 수 있다.
public static <E extends Comparable<E>> E max(Collection<E> c);- 앞서 성명하였지만 매개변수화 타입 ( ex. List<String> ) 불공변 이다. 즉 List에서 선언된 타입은 서로 관계가 없다. (이는 리스코프 치환 원칙에 어긋난다. SOLID 원칙 참고)
[결함의 예시]
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e)
}
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ... ;
numberStack.pushAll(integers);Integer가 Number의 하위 타입이니 잘 동작할 것 같지만 앞서 설명하였 듯이 매개변수화 타입은 불공변이다. 따라서 에러를 발생 시킨다.
- 원소의 생산자나 소비자용 입력 매개변수 일 때
- 원소가 생산자와 소비자 역할을 동시에 사용하지 말 것, 이는 타입을 정확히 지정해야하는 상황을 뜻한다.
- 이 공식은 와일드카드 타입을 사용하는 기본 원칙이다.
- 생산자라면 <? extends T>를 사용
- 소비자라면 <? super T>를 사용
[swap 메서드의 두 가지 선언]
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);- 어떤 선언을 사용하는 것이 좋을까?
- 기본규칙 : 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체하라.
[두번째 선언의 문제점]
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}- 방금 넣은 리스트에 원소를 꺼낼 수 없는 이해할 수 없는 에러가 발생한다.
- 여기서 와일드카드의 특성이 하나 나오는데 List<?> null 외에는 어떤 값도 넣을 수 없다.
public static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}- swapHelper 메서드는 List<E>임을 알고 있다. 이는 List에서 꺼낸 값이 항상 E인 것을 알 수 있고 동일한 타입이라면 넣어도 안전함을 뜻한다.
- 가변인수와 제네릭은 자바 5에서 같이 추가되었다.
- 가변인수의 구현에는 헛점이 한가지 있다. 가변인수 메서드 호출 시 가변인수를 위해 배열이 자동으로 만들어진다. 헌데 이 생성된 배열이 외부에 노출된다.
[제네릭과 가변인수를 혼용하여 타입의 안전성이 깨지는 사례]
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intLists; // 힙 오염 발생
String s = stringLists[0].get(0); // ClassCastException
}- 힙오염이 발생한다.
- 실무에서 매우 유용하게 사용되기 때문이다. 실제 자바라이브러리에도 이러한 메서드를 여럿 제공한다.Arrays.asList(T… a), Collections.addAll(Collection<? Super T> c, T… elements) 등
- 자바 7전에는 제네릭 가변인수 메서드에서 발생하는 경고에 대해 해줄 수 있는 것이 없었다. 그러나 자바 7부터는 @SafeVarargs 어노테이션이 추가되어 메서드가 타입 안전함을 보장하게 할 수 있다.
- 메서드가 인자값으로 넘어온 배열에 값을 덮어쓰지 않는다.
- 배열의 참조가 밖으로 노출되지 않는다. (외부의 접근이 불가) => 즉 순수하게 인수를 전달하는 용도로만 사용할 때 메서드가 안전하다고 볼 수 있다.
static <T> T[] toArray(T... args) {
return args;
}
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();
}- 문제가 없어보이나 실행하면 ClassCastException을 발생시킨다.
- 이유는 toArray는 **Object[]**를 돌려주고 main에서는 **String[]**이를 할당하기 때문이다. 당연하게도 Object[]는 String[]의 하위 타입이 아니므로 ClassCastException이 발생한다.
[제네릭 varargs 매개변수를 안전하게 사용하는 예시]
@SafeVarargs
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;
}- 넘겨 받은 값을 원소를 리스트에 옮겨 담기만 한다.
- 안전성이 확인되 었으니 @SafeVarargs를 사용하여 헷갈리는 경고를 삭제하라. ※ 어노테이션의 삭제는 항상 주의를 요한다. 그 이유는 잘못된 어노테이션의 선언으로 꼭 필요한 경고를 놓칠 수 있기 때문
- 재정의할 수 없는 메서드에만 달아라. 재정의 시 안정성이 보장되지 않기 때문
- 자바 8에서는 오직 정적 메서드와 final 인스턴스 메서드에서만 사용가능
- 자바 9부터는 private 인스턴스 메서드에서도 사용 가능
[위의 방법 이외의 안전하게 사용하는 방법 ]
static <T> List<T> pickTwo(T a, T b, T c) {
switch(ThreadLocalRandom.current().nextInt(3)) {
case 0: return List.of(a, b); // varargs 매개변수를 List 매개변수로 변환한다.
case 1: return List.of(a, c);
case 2: return List.of(b, c);
}
throw new AssertionError();
}- 컨테이너 대신 키를 매개변수화 하고 컨테이너에 값을 넣거나 뺄 때 이 키를 함께 제공한다.
- 이를 통해 제네릭 타임 시스템이 값의 타입이 키와 같음을 보장해 준다.
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorites(String.class, "Java");
f.putFavorites(Integer.class, 0xcafebabe);
f.putFavorites(Class.class, Favorites.classs);
String favoriteString = f.getFavorites(String.class);
int favoriteInteger = f.getFavorites(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
}- 해당 코드를 보면 알겠지만 String 요청했는데 다른 타입을 반환하는 일은 없다.
[타입 안전 이종 컨테이너 패턴의 구현]
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instacne) {
favorites.put(Object.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}- favorites선언을 보자 앞서 설명하였지만 직관적으로 보기에 와일드카드 타입이라 이 맵 안에 아무것도 넣을 수 없을 것이라 생각할 수 있다. 그러나 자세히 보면 와일드 카드 타입이 중첩되어 있음을 알 수 있다.
- 이는 모든 키가 서로 다른 매개변수화 타입일 수 있다는 의미이다.
- 위 코드에는 헛점이 있는데 바로 putFavorite 메서드이다. 메서드를 잘 보면 매개변수인 instance를 그대로 넣어주는데 이는 클라이언트가 악의를 갖고 class객체를 로타입으로 넘기면 타입의의 안전성이 쉽게 깨진다. 따라서 아래와 같은 방법으로 이를 해결 할 수 있다.
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instacne) {
favorites.put(Object.requireNonNull(type), type.cast(instance)); // put할 때도 동적으로 캐스팅 해준다.
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}- 실체화 불가 타입에는 사용할 수 없다.
- 앞서 설명하였듯이 List<String>.class는 런타임 시에 소거(ensure)에 따라 타입을 갖지 않는다. 즉 List<String>.class건 List<Integer>.class건 List.class를 공유하게 된다.
- 이 문제에 대해서는 만족스러운 우회로는 없으며 닐 개프터가 고안한 “슈퍼 타입 토큰(Super Type Token)”이 우회할 수 있는 실용적인 방법 중 하나라 할 수 있다. (스프링에서는 ParameterizedTypeReference라는 클래스로 구현되어 있다.) (관련 내용 : https://homoefficio.github.io/2016/11/30/%ED%81%B4%EB%9E%98%EC%8A%A4-%EB%A6%AC%ED%84%B0%EB%9F%B4-%ED%83%80%EC%9E%85-%ED%86%A0%ED%81%B0-%EC%88%98%ED%8D%BC-%ED%83%80%EC%9E%85-%ED%86%A0%ED%81%B0/)