Effective Java - Chapter 5
Updated:
Effective Java
[Effective Java 3/E] 5장 제네릭
Item 26. 로 타입은 사용하지 말라
제네릭 타입
클래스와 인터페이스 선언에 타입 매개변수가 쓰이는 것
제네릭을 활용하면 컬렉션 타입에 해당 인스턴스만 넣어야 함을 컴파일러가 인지하게 된다.
로 타입
제네릭 타입에서 타입 매개변수를 전혀 사용하지 않은 것.Set
로 타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용해서는 안된다.
=> 그럼에도 사용하는 이유는 제네릭이 도입되기 이전 코드와의 호완성을 위해 제공한다.
매개변수화 타입
어떤 타입의 객체도 저장할 수 있다.Set<Object>
비 한정적 와일드카드 타입
모종의 타입 객체만 저장할 수 있다. Set<?>
Set<?>
, Set<Object>
은 안전하지만, 로타입 Set
은 안전하지 않다.
Item 27. 비검사 경고를 제거하라
모든 비검사 경고는 중요하다.
비검사 경고는 런타임에 ClassCastException을 일으킬수 있는 잠재적 가능성을 뜻하니, 할 수 있는 한 모든 비검사 경고를 제거하라.
모두 제거한다면 그 코드는 타입 안정성이 보장된다.
경고를 제거할 수는 없지만 타입 안전하다고 확신 한다면 @SuppressWarings("unchecked")
에너테이션을 달아 경고를 숨긴다.
SuppressWarings 에너테이션은 항상 가능한 좁은 범위에 적용한다.
그런다음 경고를 숨기기로 한 근거를 주석으로 남겨라.
Item 28. 배열보다는 리스트를 사용하라
배열과 제네릭 타입의 차이
- 배열
공변이고 실체화 된다.
런타임에는 타입 안전하지만 컴파일 타임에는 그렇지 않다. - 제네릭
불공변이고 런타임에 타입 정보가 소거된다.(실체화 불가 타입)
컴파일 타임에는 타입 안전하지만 런타임에는 그렇지 않다.
따라서 둘을 섞어 쓰는 제네릭 배열을 생성하지 못한다.new List<E>[]
둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 배열을 리스트로 대체하는 방법을 사용하자.E[] -> List<E>
Item 29. 이왕이면 제네릭 타입으로 만들라
직접 형변환 보다 제네릭 타입이 더 안전하고 쓰기 편하다.
기존 타입 중 제네릭이었어야 하는 것이 있다면 제네릭 타입으로 변경하자.
제네릭 클래스로 만들기
- 타입 매개변수를 만들자.
- 타입의 이름으로 보통 E를 사용한다.
- Object를 적절한 타입 매개변수로 변경한다.
public class Stack<E> {
private E[] elements; // Object를 E로, 두번째 방법은 Object[] elements로
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack(){
elements = new E[DEFAULT_INITIAL_CAPACITY]; // 첫번째 방법, (E[]) new Object [DEFAULT_INITIAL_CAPACITY]로 형변환
}
public void push(E e){
ensureCapacity();
elements[size++] = e;
}
public E pop(){
if(size == 0){
throw new EmptyStackException();
}
E result = elements[--size]; // 두번째 방법, E result = (E) 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);
}
}
}
컴파일 오류 발생!!!
E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다.
따라서, 두가지 방법으로 위를 해결한다.
첫번째 방법
Object 배열을 생성한 다음, 제네릭으로 형변환한다.
public Stack(){
// 배열 elements 는 push(E)로 넘어온 E 인스턴스만 담는다.
// 따라서 타입 안전성을 보장하지만,
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
@SuppressWarnings("unchecked")
elements = (E[]) new Object [DEFAULT_INITIAL_CAPACITY];
}
장점
가독성 더 좋음
오직 E 인스턴스만 배열로 받음을 선언
코드 더 짧음
형번환을 배열 생성시 단 한번만 하면 됨
두번째 방법
elements 필드의 타입을 E[]에서 Object[]로 바꾼다.
public class Stack<E> {
private Object[] elements;
~~~
public E pop(){
if(size == 0){
throw new EmptyStackException();
}
@SuppressWarnings("unchecked")
E result = (E) elements[--size]; // Object를 E로 형변환 하여 리턴
elements[size] = null;
return result;
}
}
장점
힙 오염이 발생하지 않아 이를 더 선호하는 프로그래머들이 있음
타입 매개 변수의 한계
대다수의 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않는다.
ex) Stack<Object>, Stack<int[]>, Stack<List<String>>, Stack
등.. 어떤 참조타입도 가능
단, 기본타입은 사용할 수 없다.
ex) Stack<int>, Stack<double>
등.. 컴파일 오류 발생
이는 박싱된 기본타입으로 사용해 우회 가능하다.
Item30. 이왕이면 제네릭 메서드로 만들라
제네릭 타입과 마찬가지로, 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기도 쉽다.
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
재귀적 타입 한정(Recursive type bound)
자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정.
재귀적 타입 한정은 주로 타입의 자연적 순서를 정하는 Comparable 인터페이스와 함께 쓰인다.
public interface Comparable<T> {
int compareTo(T o);
}
Comparable 인터페이스의 타입 매개변수 T는 해당 인터페이스를 구현한 타입이 비교할 수 있는 원소의 타입을 정의
public static <E extends Comparable<E>> E max(Collection<E> c) {
if (c.isEmpty())
throw new IllegalArgumentException("컬렉션이 비어 있습니다.");
E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result Object.requireNonNull(e);
return result;
}
타입 한정인 <E extends Comparable<E>>
는 “모든 타입 E는 자신과 비교할 수 있다”라고 해석
Item31. 한정적 와일드 카드를 사용해 API 유연성을 높여라
매개변수화 타입은 불공변이다.
따라서 List<Object>
와 List<String>
은 아무 관계도 아니다.
위와 같은 상,하위 타입을 제네릭에서 표현하는 방법이 한정적 와일드카드 타입이다.
PECS : producer-extends, consumer-super
매개변수화 타입 T가 생산자면 <? extends T>를 사용하고 소비자라면 <? super T>를 사용하라
public void pushAll(Iterable<? extends E> src) { // 생산자
for (E e : src)
push (e);
}
외부 타입의 데이터를 Stack의 내부 인스턴스인 Number[]에 넘겨준다.
public void popAll(Collection<? super E> dst) { // 소비자
dst.addAll(list);
list.clear();
}
Object 타입에 Stack의 Number 타입 데이터를 넘겨준다.
Comparable과 Comparator는 모두 소비자이다.
Item32. 제네릭과 가변인수를 함께 쓸 때는 신중하라
가변인수 메서드
메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 해준다.
하지만, 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 만들어진다.
이는 제네릭 배열이 생성되며 이는 힙 오염의 위험이 있다.
제네릭 varargs 매개변수는 타입 안전하지는 않지만, 허용된다.
메서드에 제네릭(혹은 매개변수화된) varargs 매개변수를 사용하고자 한다면, 먼저 그 메서드가 타입 안전한지 확인한 다음 @SafeVarags 애너테이션을 달아 사용하는데 불편함이 없게끔 하자.
- varargs 매개변수 배열에 아무것도 저장하지 않는다.
- 그 배열의 참조가 밖으로 노출되지 않는다.
위 조건을 만족하여 순수하게 인수들을 전달하는 일만하다면 그 메서드는 안전하다.
Item33. 타입 안전 이종 컨테이너를 고려하라
-
컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다.
EX) Set, Map<K,V> -
하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다.
public class Favorites { // 타입 이종 컨테이너 구현
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
- 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런식으로 쓰이는 Class 객체를 타입 토큰이라 한다.
EX) String.class, Integer.class
출처
Joshua Bloch. Effective Java 3/E. n.p.: 인사이트, 2018년 11월 1일.