on
5장 제네릭
5장 제네릭
아이템26) 로 타입은 사용하지 말라
Raw 타입을 쓰게 되면 제너릭이 안겨주는 안전성과 표현력을 모두 잃게 된다.
List 를 쓰게 되면 아무것도 해당 list에 add 할 수는 없지만 List , List 등등을 참조 할 수 있다.
class 리터럴에는 Raw 타입을 써야한다. List.class는 되지만 , List.class는 되지 않는다.
Raw 타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안된다. Raw 타입은 제너릭이 도입되기 이전 코드와의 호환성을 위해 제공될 뿐이다.
아이템27) 비검사 경고를 제거하라
제네릭을 사용하기 시작하면 수많은 컴파일러 경골르 보게 될 것이다. 비검사형 변환경고 , 비검사 메서드 호출 경고, 비검사 매개변수화 가변인수 타입 경고, 비검사 변환 경고 등등.
경고를 제거할 수는 없지만 타입 안전한다고 확신할 수 있다면 @SuprressWarnings("unchecked") 로 숨길 수 있다.
public T[] toArray(T[] a) { if (a.length < size) // 생성한 배열과 매개변수로 받은 배열의 타입이 모두 T[]로 같으므로 // 올바른 형변환이다. @SuppressWarnings("unchekced") T[] result = (T[]) Arrays.copyOf(elementData, size, a.getClass()); return result; System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; }
전체 메소드위에거는게 아니라 @SupressWarnings 를 최소한의 범위로 걸어야한다.
아이템28) 배열보다는 리스트를 사용하라
배열과 제네릭 타입에는 중요한 차이 2가지가 있다. 첫 번쨰 배열은 공변이다. Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다.
반면 제네릭은 불공변이다. 즉 서로다른 타입 Type1, Typ2가 있을 때 List 은 List 의 하위타입도 아니고 상위 타입도 아니다.
제네릭 배열 생성을 허용하지 않는 이유는 타입이 안전하지 않기 때문이다. 이를 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다. 런타임에 CalssCastException이 발생하는 일을 막아주겠다는 제너릭 타입 시스템의 취지에 어긋나는 것이다.
아이템29) 이왕이면 제네렉 타입으로 만들라
public class Stack { private E[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACTIY = 16; public Stack() { elements = new E[DEFAULT_INITIAL_CAPACTIY]; } public void push(E e) { ensureCapacity(); elements[size++] = e; } public E pop() { if(size == 0) throw new EmptyStackException(); return elements[--size]; } private void ensureCapacity() { if(elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1); } ... // isEmpty와 ensureCapacity 메서드는 그대로 }
이런식으로 타입 매개변수를 넣어줘서 제네릭으로 만들면 좋다.
하지만 위의 코드에서는 생성자 부분인 new E[DEFAULT_INITIAL_CAPACITY] 에서 컴파일 오류가 난다.
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
이런식으로 고치면 unchecked cast warning은 뜨지만 컴파일러 오류는 넘어갈 수 있다.
2번째 방법은 elements 필드의 타입을 E[] 에서 Object[]로 바꾸는 것이다.
public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACTIY = 16; public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACTIY]; } public void push(E e) { ensureCapacity(); elements[size++] = e; } public E pop() { if(size == 0) throw new EmptyStackException(); return elements[--size]; } private void ensureCapacity() { if(elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1); } ... // isEmpty와 ensureCapacity 메서드는 그대로 }
이렇게 작성하게 되면 pop method return 문에서 incompatible types 컴파일에러가 뜬다.
그래서 return 문을 (E) elements[--size]로 변환하게 되면 컴파일오류대신 unchecked cast warning이 뜬다. E는 실체화 불가 타입이기 떄문에 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없다.
첫번 째 방식을 쓰게 되면 배열의 런타임 타입이 컴파일타임 타입과 달라 heap pollution을 일으킬 수 있어, 힙 오염이 마음에 걸리는 사람은 2번 째 방식을 사용한다.
heap pollution = Java generic type에서 변수화 된 타입이 가리키는 오브젝트의 타입이 해당 변수화 된 타입의 타입과 다를 때를 의미한다. 즉 제네릭 소거 때문에 컴파일시 타입이 변경되어, 런타임과 컴파일 타임때 서로 타입이 다를 때를 의미한다.
아이템30) 이왕이면 제네릭 메서드로 만들라
public static Set union(Set s1, Set s2) { Set result = new HashSet<>(s1); result.addAll(s2); return result; }
정규타입 매개변수는 이고 반환타입은 Set 이다.
제너릭 싱글턴 팩터리란 요청한 타입 매개변수에 맞게 그 객체의 타입을 바꿔주는 정적 팩터리 메서드 이다. 하지만 이렇게 하려면 요청한 타입 매개변수에 맞게 매번그 객체의 타입을 바꿔주는 정적 팩터리를 만들어야 한다.
@SuppressWarnings("unchecked") public static Comparator reverseOrder() { return (Comparator) ReverseComparator.REVERSE_ORDER; } private static class ReverseComparator implements Comparator>, Serializable { static final ReverseComparator REVERSE_ORDER = new ReverseComparator(); public int compare(Comparable c1, Comparable c2) { return c2.compareTo(c1); } @java.io.Serial private Object readResolve() { return Collections.reverseOrder(); } @Override public Comparator> reversed() { return Comparator.naturalOrder(); } }
비검사 형변환 경고가 reverseOrder method에서 발생하지만 이 메소드는 항등함수로 입력값을 특별한 수정없이 그대로 반환함으로 T가 어떤 타입이든 Compartor를 사용해도 타입 안전하다.
재귀적 타입 한정 , 자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있다.
public static > E max(Collection c);
아이템31) 한정적 와일드카드를 사용해 API 유연성을 높여라
PECS Producer extends consumber super 의 줄임말이다.
Iterable src 의 경우는 src가 공급자로서 해당 collection에서는 get 만 허용하겠다는 뜻이다.
Collection dst 의 경우는 dst가 소비자로서 해당 collection에서는 set 만 허용하겠다는 뜻이다.
public static void swap(List list, int i, int j); public static void swap(List list, int i, int j);
pubilc AP라면 2번째 swap이 낫다. 어떤 리스트든 이 메서드에 넘기면 명시한 인덱스의 원소들을 교환해줄 것이고, 신경 써야 할 타입 매개변수도 없다.
하지만 2번째 swap에는 문제가 있다.
public static void swap(List list, int i, int j) { list.set(i, list.set(j, list.get(i))); }
이렇게 방금 꺼낸 원소를 리스트에 다시 넣을 수 없게된다. 왜냐하면 List에는 null 외에는 어떠한 값도 넣을 수 없기 때문이다.
따라서 다음과 같이 타입 매개변수를 써서 컴파일 시점에 타입이 결정나게 해줘야한다.
public static void swap(List list, int i, int j) { swapHelper(list,i,j); } //와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드 private static void swapHelper(List list, int i, int j) { list.set(i, list.set(j, list.get(i))); }
이제는 해당 list에 무엇이 들었는지 알기에 꺼낸것을 다시 넣어 줄 수 있다. swapHelper 메서든느 리스트가 List임을 알고 있다. 즉 이 리스트에서 꺼낸 값의 타입은 항상 E이고 E 타입의 값이라면 이 리스트에 넣어도 안전함을 알고 있다.
swap 메서드 내부에서는 더 복잡한 메서드를 이용했지만 , 덕분에 외부에서는 와일드카드 기반의 멋진 선언을 유지할 수 있었다. 즉 swap 메서드를 호출하는 클라이언트는 복잡한 swapHelper의 존재를 모른 채 그 혜택을 누릴 수 있다.
from http://tonylim.tistory.com/248 by ccl(A) rewrite - 2021-09-09 19:27:24