책 읽고 정리하기/2022

Effective Java 3/E - 5장 제네릭 - 2

TwinParadox 2022. 2. 13. 16:02
728x90

30. 이왕이면 제네릭 메서드로 만들라

  • 메서드도 제네릭 메서드로 생성이 가능하다.
  • 다음과 같은 정적 유틸리티 메서드가 제네릭 메서드의 대표적인 예시다.
  • 타입 매개변수들을 선언하는 매개변수 목록은 메서드의 제한자와 반환 타입 사이에 위치한다.
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
    return !(list instanceof RandomAccess) && list.size() >= 5000 ? iteratorBinarySearch(list, key) : indexedBinarySearch(list, key);
}
public static <E> ArrayList<E> concat(ArrayList<E> list1, ArrayList<E> list2) {
   ArrayList<E> result = new ArrayList<>(list1);
   result.addAll(list2);
   return result;
}

 

제네릭 싱글턴 팩토리

  • 경우에 따라 불변 객체를 여러 타입으로 활용 가능하게 해야 하는 경우, 제네릭이 런타임에 타입 정보가 소거되기 때문에, 어떤 타입으로든 매개변수화 가능하다.
  • 단, 이를 위해 그 객체의 타입을 바꿔주는 정적 팩토리가 필요하며, 함수 객체나, 컬렉션 용으로 사용한다.
public static <T> Comparator<T> reverseOrder() {
    return Collections.ReverseComparator.REVERSE_ORDER;
}
public static final <T> Set<T> emptySet() {
    return EMPTY_SET;
}

 

재귀적 타입 한정(Recursive Type Bound)

  • 자기 자신이 들어간 표현식을 사용해 타입 매개변수의 허용 범위를 한정한다.
  • 타입의 순서 결정을 위해 Comparable과 같이 쓰이는 편이다.
// 모든 타입 E는 자신과 비교할 수 있음을 명확히 전달
public static <E extends Comparable<E>> E max(List<E> list)

 

 

31. 한정적 와일드카드를 사용해 API 유연성을 높여라

// E의 하위 타입이 Iterable임을 의미한다.
// extends와는 맞지 않는 내용이긴 하지만, 하위를 의미한다.
public void pushAll(Iterable<? extends E> src) {
	for (E e : src) {
		push(e);
	}
}
// 기존 코드
public void popAll(Collection<E> dst) {
	while (!isEmpty()) {
		dst.add(pop());
	}
}
// 한정적 와일드카드 타입 적용
// E의 상위 타입이 Collection이어야 함을 의미한다.
public void popAll(Collection<? super E> dst) {
	while (!isEmpty()) {
		dst.add(pop());
	}
}

이처럼, 유연성 극대화를 위해서, 원소의 생산자/소비자 메서드에서의 매개변수에 대해 와일드카드 타입을 사용해야 한다. 단, 이 입력 매개변수가 생산자와 소비자 역할을 동시에 할 때에는 타입을 명확히 지정해야 하므로, 사용하지 않아야 한다.

 

PECS(Producer-extends, Consumer-super)

  • 매개변수화 타입이 생산자라면, <? extends T>, 소비자라면 <? super T>
  • 와일드카드 타입 사용의 기본 원칙, 겟풋 원칙(Get and Put Principle)이라고도 한다.

 

반환 타입에는 한정적 와일드카드 타입을 사용하면 안 된다.

  • 반환 타입에도 적용하게 되면, 사용하는 클라이언트 코드에서도 와일드카드 타입을 사용하기 때문이다.
  • 와일드카드 타입으로 설계된 클래스를 사용자가 신경 써야 한다면, 문제가 있는 API라고 볼 수 있다.

 

 

PECS 적용 예시

// AS-IS
public static <E extends Comparable<E>> E max(Collection<E> coll)

// TO-BE
public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll)

Comparable은 소비자이며, Comparator도 소비자에 가깝다.

Comparable(또는 Comparator)의 직접 구현 없이, 직접 구현한 다른 타입을 확장한 타입을 지원하기 위해 필요하다.

 

 

메서드 정의할 때, 타입 매개변수와 와일드카드 중 어느 것을 사용할 것인가?

  • 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체한다.
  • 이때 비한정적/한정적인지에 따라, 그에 맞는 와일드카드를 사용한다.

 

 

32. 제네릭과 가변인수를 함께 쓸 때는 신중하라

가변인수

  • 제네릭과 함께 Java 5에 추가
  • 메서드에 넘기는 인수의 개수를 클라이언트가 조절 가능
  • 가변인수 메서드 호출 시, 이를 담기 위한 배열이 자동으로 생성
  • 이 배열을 감추지 못하고, 클라이언트에 노출되는 경우가 발생한다.

 

제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.

제네릭 배열을 직접 생성하는 건 허용하지 않지만, 이런 메서드 선언이 가능한 이유는 실무에서의 유용하기 때문이다. 아래 메서드 같은 것이 예시다.

@SafeVarargs
public static <T> List<T> asList(T... a) {
    return new Arrays.ArrayList(a);
}

 

다음과 같은 예시는 힙 오염을 메서드를 호출한 쪽의 콜 스택까지 전이하기 때문에 위험하다.

static <T> T[] toArray(T... args) {
	return args;
}

 

다음 조건을 "모두" 만족하는 제네릭 varargs 메서드는 안전하다.

  • varargs 매개변수 배열에 아무것도 저장하지 않는다.
  • 그 배열 또는 복제본을 신뢰할 수 없는 코드에 노출하지 않는다.

 

 

Java 7에 도입된 @SafeVarargs

  • 제네릭 가변인수 메서드 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있다.
  • 메서드의 작성자가 메서드의 타입 안전성을 보장하기 위해 사용한다.
  • 안전성은, 메서드가 자동 생성되는 가변인수 배열을 저장하지 않고, 그 참조가 외부로 노출되지 않을 때를 의미한다. 즉, varargs 매개변수 배열이 호출자로부터 그 메서드로 순수히 인수들을 전달하는 일만 해야 한다.
  • 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 이 어노테이션을 적용한다.
  • 재정의할 수 없는 메서드에만 달아야 한다.

 

33. 타입 안전 이종 컨테이너를 고려하라.

제네릭은 단일 원소 컨테이너에 흔히 쓰이며, 매개변수화되는 대상은 컨테이너 자신이다.

하나의 컨테이너에서는 매개변수화 가능한 타입의 수가 제한된다.

 

 

타입 안전 이종 컨테이너 패턴

  • Type Safe Heterogeneous Container Pattern
  • 컨테이너 대신 키를 매개변수화하고, 컨테이너에 값을 넣고 뺄 때 매개변수화한 키를 함께 제공
  • 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장한다.
// 구현
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(favroties.get(type));
    }
}


// API
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 f = new Favorites();
    
    f.putFavorite(Integer.class, 100);
    f.putFavorite(String.class, "Examples");
    
    Integer fInteger = f.getFavorite(Integer.class);
    String fString = f.getFavorite(String.class);
}

 

  • Key에 적용된 와일드카드 타입 - 다양한 타입 지원
    • 비한정적 와일드카드 타입(Map<Class<?>, Object>)를 사용하는데, Map 자체가 아닌 Key에 와일드카드 타입을 사용하고 있어서, 아무 타입이나 넣을 수 있다.
    • 즉, 모든 키가 서로 다른 매개변수화 타입으로, 다양한 타입을 지원할 수 있다. 
  • Object인 Value - 모든 값이 Key로 명시한 타입임을 보장하지 않음
  • getFavorite의 type.cast
    • Value는 Object이나 실제 반환은 T가 되어야 하므로, cast를 수행
    • cast 메서드가 인수 그대로를 반환하지만, Class 클래스가 제네릭이라는 이점을 활용할 수 있음

위 Favorites의 제약

  • 악의적으로 Class 객체를 Raw Type으로 넘길 경우 타입 안전하지 않다.
    • 이 문제를 해결하기 위해서는, putFavorite에서 instance에 대해 type 체크를 수행해야 한다.
    • 동적 형변환
  • 실체화 불가 타입에는 사용 불가능하다.
    • String, String[]은 가능하지만, List<String>은 불가능하다.
  • 타입 토큰이 비한정적이다.
    • 어떤 Class 객체든 받아들인다.
    • 한정적 타입 토큰의 사용으로, 한정적 타입 매개변수, 한정적 와일드카드를 사용해 제약을 줄 수 있다.

 

 

728x90
728x90