프로그래밍/책

[이펙티브 자바, Effeective Java] 5장 제네릭

시간이nullnull한 가장 2021. 12. 22. 08:57

26 raw type은 사용하지 말라


클래스와 인터페이스 선언에 타입 매개변수가 쓰이면 이를 제네릭 클래스 혹은 제네릭 인터페이스라 한다.

예를 들어 List<String>은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입이다. 여기서 String이 정규 타입 매개변수 E에 해당하는 실제 타입 매개변수다.

 

제네릭 타입을 정의하면 그에 딸린 raw type도 함께 정의된다. raw type이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다.

예를 들어 List<E>의 raw type은 List다.

 

raw type은 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작하는데, 제네릭이 보편화되기 전 코드들과 호환되도록 하기 위한 궁여지책이라 할 수 있다.

 

오류는 가능한 즉시, 컴파일할 때 발견하는 것이 좋다. raw type을 사용하면 제네릭이 안겨주는 안전성과 표현력을 모두 잃게 된다. 


 

27 비검사 경고를 제거하라


비검사 경고는 런타임에 exception을 일으킬 수 있는 잠재적 가능성을 뜻한다. 할 수 있는 한 모든 비검사 경고를 제거하는게 좋다. (모두 제거한다면 그 코드는 타입 안전성이 보장된다.)

 

경고를 제거할 수는 없지만 타입이 안전하다고 확신할 수 있다면 @SuppressWarnings(”unchecked”) 어노테이션을 달아서 경고를 숨기자.

 

@SuppressWarnings(”unchecked”) 어노테이션은 개별 지역변수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있다. 하지만 이 어노테이션은 항상 가능한 한 좁은 범위에 적용하자. 변수 선언, 짧은 메서드, 혹은 생성자가 될 수 있다.

 

@SuppressWarnings(”unchecked”) 어노테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 한다.

 


 

28 배열보다는 리스트를 사용하라


배열과 제네릭 타입에는 중요한 차이가 두 가지 있다.

 

첫 번째, 배열은 공변이다. 어려워 보이는 단어지만 뜻은 간단하다. 함께 변한다는 뜻이다. (Sub가 Super의 하위 타입이라면 배열 Sub[]는 Super[]의 하위 타입이 된다.)

 

반면 제네릭은 불공변이다. 서로 다른 타입 Type1과 Type2가 있을 때 List<Type1>과 List<Type2>는 서로의 하위 타입도 상위 타입도 아니다.

Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다."; // ArrayStoreException을 던진다.
List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입이다.
ol.add("타입이 달라 넣을 수 없다.");

 

어느쪽이든 Long용 저장소에 String을 넣을 수는 없다.

그러나 위쪽 배열 코드는 그 실수를 런타임에서 알 수 있고 아래쪽 리스트는 컴파일하면서 바로 알 수 있다.

 

두 번째 차이로 배열은 실체화된다. 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인하기에 런타임에서 에러가 발생한다.

 

그러나 제네릭은 타입 정보가 런타임에는 소거된다. 원소 타입을 컴파일 당시에만 검사하며 런타임에서는 알 수 없다.

 

이 두 가지 차이점 때문에 배열과 제너릭은 상생할 수 없다. (제너릭 배열을 만들 수 없다.)


 

29 이왕이면 제너릭 타입으로 만들라


클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다. 그러니 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라.

그렇게 하려면 제네릭 타입으로 만들어야 할 경우가 많다. 기존 타입 중 제네릭이었어야 하는 게있다면 제네릭 타입으로 변경하자.


 

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


클래스와 마찬가지로, 메서드도 제네릭으로 만들 수 있다.

public static Set union(Set s1, Set s2) {
 Set result = new HashSet(s1);
 result.addAll(s2);
 return result;
}

위 메서드는 컴파일은 되지만 제너릭 타입을 사용하지 않아 경고가 발생한다.

아래와 같이 수정해서 사용하자.

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
 Set<E> result = new HashSet<>(s1);
 result.addAll(s2);
 return result;
}

제네릭 타입과 마찬가지로, 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기도 쉽다.

 

타입과 마찬가지로, 메서드도 형변환 없이 사용할 수 있는 편이 좋으며, 많은 경우 그렇게 하려면 제네릭 메서드가 되어야 한다.  형변환을 해줘야 하는 기존 메서드는 제네릭하게 만들자. 


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


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);
}

이 메서드는 깨끗이 컴파일되지만 완벽하진 않다.

Iterable src의 원소 타입이 스택의 원소 타입과 일치하면 잘 작동하지만 Stack<Number>로 선언한 후 pushAll(intVal)을 호출하면 어떻게 될까? (여기서 intVal은 Integer 타입이다.)

 

Integer는 Number의 하위 타입이니 잘 동작해야 할 것 같지만 실제로는 다음의 오류 메시지가 뜬다. 매개변수화 타입이 불공변이기 때문이다.

Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers);

StackTest.java:7: error: incompatible types: Iterable<Integer>
cannot be converted to Iterable<Number>
 numberStack.pushAll(integers);
										 ^

 

한정적 와일드카드 타입이라는 매개변수화 타입을 이용해 위 예시를 수정해보자.

public void pushAll(Iterable<? extends E> src) {
 for (E e : src)
 push(e);
}

 

pushAll을 만들었으니 popAll을 만들어보자. popAll 메서드는 Stack 안의 모든 원소를 주어진 컬렉션으로 옮겨 담는다.

public void popAll(Collection<E> dst) {
 while (!isEmpty())
 dst.add(pop());
}

 

마찬가지로 주어진 컬렉션의 원소 타입이 스택의 원소 타입과 일치한다면 말끔히 컴파일되고 문제없이 동작하지만 Stack<Number>의 원소를 Object용 컬렉션으로 옮긴다 가정해보자.

 

제너릭 타입은 불공변이기 때문에 Collection<Object>는 Collection<Number>의 하위 타입이 아니다와 같은 에러메세지를 받게 된다.

 

와일드카드 타입을 적용한 popAll을 만들어보자. (이번에는 popAll의 입력 매개변수의 타입이 ‘E의 Collection’이 아니라 ‘E의 상위 타입의 Collection’이어야 한다)

public void popAll(Collection<? super E> dst) {
 while (!isEmpty())
 dst.add(pop());
}

 

펙스(PECS) producer-extends, consumer-super 규칙을 통해 와일드카드 타입을 적용할 시점을 판단할 수 있다.

 

매개변수화 타입 T가 생산자라면 <? extends T>를 사용하고, 소비자라면 <? super T>를 사용한다.

 

Stack 예에서 pushAll의 src 매개변수는 Stack이 사용할 E 인스턴스를 생산하므로 src의 적절한 타입은 Iterable<? extends E>이다.

 

한편, popAll의 dst 매개변수는 Stack으로부터 E 인스턴스를 소비하므로 dst의 적절한 타입은 <? super E>이다.

 

*반환 타입에는 한정적 와일드카드 타입을 사용하면 안 된다. 클라이언트 코드에서도 와일드카드 타입을 써야 하기 때문이다


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


static void dangerous(List<String>... stringLists) {
 List<Integer> intList = List.of(42);
 Object[] objects = stringLists;
 objects[0] = intList; // 힙 오염 발생
 String s = stringLists[0].get(0); // ClassCastException
}

이 메서드에서는 형변환하는 곳이 보이지 않는데도 인수를 건네 호출하면 ClassCastException을 던진다. 마지막 줄에 컴파일러가 생성한 (보이지 않는) 형변환이 숨어 있기 때문이다.

 

이처럼 타입 안전성이 깨지니 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.

 

가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열과 제네릭의 타입 규칙이 서로 다르기 때문이다.

 

메서드에 제네릭 (혹은 매개변수화된) varargs 매개변수를 사용하고자 한다면, 먼저 그 메서드가 타입 안전한지 확인한 다음 SafeVarargs 어노테이션을 달아 사용하는 데 불편함이 없게끔 하자.