[Effective Java] 아이템30 - 이왕이면 제네릭 메서드로 만들라

개요

앞서 이왕이면 제네릭 타입으로 만들어라에 이어 이번엔 제너릭 메서드다.

매개변수화 타입 (parameterized type)을 받는 static utility method에서 제너릭을 볼 수 있다.

다음은 Collections.class에 있는 binarySearch 메서드이다.

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

T 라는 매개변수화 타입을 받아 이진탐색을 하고 있다.

아이템31에서 다룰 것이지만 여기서 제너릭 인수인 List<? extends Comparable<? super T>>를 보면, Comparable을 확장한 타입만 허용하게 되어있다. (비한정적 와일드타입) 왜냐하면 이 메서드는 자기와 동일한 값을 비교해서 반복적으로 탐색해나가는 로직인데, Comparable은 본래 본인의 타입만 비교할 수 있기 때문에 Comparable을 확장한 오브젝트만 받을 수 있게 하는 것이다.

제너릭 메서드 작성법

먼저 두 파리미터를 받아서 합집합으로 리턴해주는 아래 메소드를 보자.

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

Set이라는 Raw Type을 사용해서 Type Safety 하지 않는다. 따라서 아래와 같은 Warning 문구가 뜬다.

Set is a raw type. References to generic type Set<E> should be parameterized

Type safety: The constructor HashSet(Collection) belongs to the raw type HashSet. References to generic type HashSet<E> should be parameterized

Type safety: The method add(Object) belongs to the raw type Set. References to generic type Set<E> should be parameterized

경고를 없애려면 Type Safety 하게 만들면 되는데, union 메서드에 제너릭 타입 매개변수를 선언해주면 된다.

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

타입 매개변수 목록 (<E>)은 메서드의 제한자 (public, private 등)와 return type 사이에 온다.

제네릭 싱글턴 팩터리

불변 객체 (immutable object)를 여러 타입으로 활용할 수 있게 하는 게 제네릭 싱글턴 팩터리다.

아래는 Collections.emptySet 메서드의 내용이다.

@SuppressWarnings("unchecked")
public static final <T> Set<T> emptySet() {
    return (Set<T>) EMPTY_SET;
}

불변 객체를 여러 타입으로 활용할 수 있도록 parameterized type인 <T>를 받고 그에 맞도록 형변환해서 리턴해주고 있다.

(제너릭을 활용한) 항등함수

항등함수란, 본인과 동일한 (identity) 객체를 반환하는 함수를 뜻한다. 항등함수 객체는 상태가 없으니 요청할 때마다 새로 생성하는 것은 낭비다.

제너릭은 소거 방식을 활용하기 때문에, 일일이 타입별로 객체를 하나씩 만들 필요가 없다.

private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;

@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
  return (UnaryOperator<T>) IDENTITY_FN;
}

UnaryOperator 은 인터페이스이다.

  • Returns a unary operator that always returns its input argument.

입력 값을 수정없이 그대로 반환하는 특별한 함수이므로, @SuppressWarnings 어노테이션을 추가할 수 있다.

이 항등함수를 한번 테스트해보자. 형변환을 하지 않아도 자동으로 타입별로 객체가 리턴되는 것을 확인할 수 있다.

public static void main(String[] args) {
  String[] strings = {"엠블", "웨이브", "시아코인"};
  UnaryOperator<String> sameString = identityFunction();
  for(String s  : strings) {
    System.out.println(sameString.apply(s));
    // UnaryOperator 가 Function 인터페이스를 상속하기 때문에 apply 메서드를 쓸 수 있다.
  }

  Number[] numbers = {1, 2.0, 3L};
  UnaryOperator<Number> sameNumber = identityFunction();
  for(Number n : numbers) {
    System.out.println(sameNumber.apply(n));
  }
}

재귀적 타입 한정 (recursive type bound)

자기 자신이 들어간 표현식을 사용해서 허용범위를 한정하는 방법을 말한다.

주로, Comparable 인터페이스와 함께 쓰인다. Comparable은 타입의 자연적 순서를 정하는 인터페이스인데, 주로 자신과 같은 타입의 원소와만 비교할 수 있다.

따라서 Comparable을 재귀적으로 활용하면 허용범위를 한정할 수 있는 것이다.

public static <E extends Comparable<E>> E max(Collection<E> c);

<E extends Comparable<E>> : 모든 타입 E는 자신과 비교할 수 있다. 라는 뜻이다.

Summary

제너릭 메서드를 활용하면 명시적으로 형변환해야 하지 않아도 되서 안전하며 사용하기도 쉽다. 따라서 형변환 해줘야 하는 메서드가 있다면 제너릭하게 만들자.

댓글남기기