public static Set union(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
이렇게 매개변수의 타입이나 반환 타입으로 로 타입을 사용하게 되면 컴파일은 가능하지만 new HashSet을 하는 과정과 result에 s2를 addAll 하는 과정에서 로 타입에 대한 경고가 발생한다.
메서드에서도 적절한 제네릭 사용으로 타입 안전을 보장하자.
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
Collections.binarySearch 메서드는 제네릭을 사용해 작성되어있다. 일반적으로 리스트에서 이진 탐색을 하기 위해서는 리스트 내부 원소의 타입이 다 같아야 하고 비교 정렬할 수 있어야 하며, 찾으려는 원소와 같은 타입이어야 한다. 하지만 리스트의 구성 요소와 찾으려는 요소의 타입이 같기만 하면 될 뿐 이 타입을 특정지어줄 필요는 없다. 따라서 타입을 제네릭으로 만들어서 사용해줄 필요가 있다.
메서드에 제네릭을 사용할 때도 앞선 아이템 29에서 클래스에 제네릭을 사용했던 것 처럼 타입이 오는 자리에 제네릭을 넣어주면 된다. 다만, 메서드에 제네릭을 사용할 때는 메서드의 제한자와 반환 타입 사이에 타입 매개변수 목록을 넣어주어야 한다는 것을 잊지 말아야 한다.
예를 들어, 앞선 binarySearch에서
public static
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
...
}
와 같이 public static과 int 사이에 <T>를 쓰지 않게 되면 Cannot resolve symbol 'T'
라는 경고가 뜨며 컴파일 할 수 없게 된다.
만약 메서드에 여러 개의 서로 다른 제네릭 타입을 사용해야 한다면 사용하는 모든 타입 매개변수를 <> 사이에 넣어서 명시해주어야 한다.
단, 클래스가 제네릭을 사용하는 클래스라면, 메서드에서 같은 제네릭을 사용할 때는 타입 매개변수를 지정해주지 않아도 된다.
public class Stack<E> {
private E[] elements;
...
public E pop() {
if (size == 0) {
throw new EmptyStackException();
}
E result = elements[--size];
elements[size] = null;
return result;
}
...
}
pop 메서드는 E라는 타입을 반환한다. 위에서 본 제네릭 메서드 규칙에 의하면 public <E> pop()
이 되어야 할 것 같지만, Stack 클래스 선언 자체에 지정된 E 타입을 사용해주기 위해 매겨변수 목록으로 <E>를 넣어주게 되면 오히려 기존에 클래스에서 유지하고 있던 제네릭 E와 같은 타입이 아니라고 판단하게 된다.
제네릭을 매 번 설명할 때 마다 강조하는 부분이지만 제네릭은 런타임 시점에 Object 타입으로 타입이 소거된다. 이로 인해 하나의 객체를 어떤 타입으로든 매개변수화 할 수 있다. 하지만 이렇게 하려면 요청한 타입에 맞게 매번 그 객체의 타입을 바꿔주는 정적 팩터리를 만들어야 한다.
public class GenericFactoryMethod {
private static final Set IMMUTABLE_EMPTY_SET = Set.copyOf(new HashSet());
@SuppressWarnings("unchecked")
public static <T> Set<T> immutableEmptySet() {
return (Set<T>) IMMUTABLE_EMPTY_SET;
}
}
immutableEmptySet은 원소 타입으로 어떤 타입을 요청해도 불변의 빈 Set을 돌려주어야 한다. 또한 새로 element가 추가되거나 할 여지도 없다. 따라서 미리 빈 불변의 HashSet을 만들어 놓고 요청이 들어오면 반환하는 형태로 사용할 수 있다. 이 때, Set 내부의 element가 없으므로 T에 어떤 타입 요청이 오더라도 비검사 형변환을 통해 내보내도 타입 안전을 보장할 수 있다.
public class GenericFactoryMethod {
private static final Set IMMUTABLE_EMPTY_SET = Set.copyOf(new HashSet());
public static void main(String[] args) {
Set<String> immutableEmptyStringSet = immutableEmptySet();
// String을 element 타입으로 가지는 불변의 빈 Set 타입 객체가 반환됨
Set<Integer> immutableEmptyIntSet = immutableEmptySet();
// int를 element 타입으로 가지는 불변의 빈 Set 타입 객체가 반환됨
}
@SuppressWarnings("unchecked")
public static <T> Set<T> immutableEmptySet() {
return (Set<T>) IMMUTABLE_EMPTY_SET;
}
}
Collection의 요소들 중 가장 큰 요소를 반환하는 max 라는 메서드를 작성한다고 하면, 매개변수로 받아오는 컬렉션의 요소들은 '비교 및 정렬이 가능해야 한다.' 라는 조건이 필요하다. 즉, Comparable을 구현한 객체들의 컬렉션에 대해서만 max 메서드를 사용할 수 있는데, 제네릭에 이 부분을 반드시 명시해 줄 필요가 있다. 그런데, 대부분의 타입은 같은 타
public static <E extends Comparable<E>> E max(Collection<E> c) {
...
}
매개변수 타입을 넣어주는 부분에서 그냥 E를 넣는 것이 아니라 <E extends Comparable<E>>
를 넣어줌으로써 Comparable의 하위 구현체인 타입만 올 수 있다는 것을 명시해 주었으며 정확한 표현은 "모든 타입 E는 자신과 비교할 수 있다." 정도가 될 수 있다.
메서드에서 제네릭을 사용하지 않고 로 타입이나 Object 타입을 사용한다면 입력 매개변수와 반환값을 명시적 형변환해야 하며, 비검사 형변환 과정에서 예외 상황이 발생할 수 있어 안전하지 않다. 따라서 불필요한 형변환을 없애기 위해 메서드의 매개변수와 반환값에 적절히 제네릭을 사용하여 제네릭 메서드로 만들면 편의성과 안정성을 모두 잡는 좋은 방법이 될 수 있다.