Posts [이펙티브자바] 아이템30-이왕이면 제네릭 메서드로 만들자
Post
Cancel

[이펙티브자바] 아이템30-이왕이면 제네릭 메서드로 만들자

로 타입으로 이루어진 메서드를 제네릭을 사용하여 변환

아래 예제 코드를 보자.

1
2
3
4
5
6
// 코드 30-2 제네릭 메서드 (177쪽)
public static Set union(Set s1, Set s2) {
    Set result = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}

컴파일은 되지만 경고가 두 개 발생한다.

1
2
3
4
5
6
Warning:(20, 22) java: unchecked call to HashSet(java.util.Collection<? extends E>) as a member of the raw type java.util.HashSet

Warning:(21, 22) java: unchecked call to addAll(java.util.Collection<? extends E>) as a member of the raw type java.util.Set
Warning:(29, 35) java: unchecked conversion
  required: java.util.Set<java.lang.String>
  found:    java.util.Set

메서드 선언에서의 세 집합(입력 2개, 반환 1개)의 원소 타입을 타입 매개변수로 명시하고, 메서드 안에서도 이 타입 매개변수만 사용하게 수정하면 된다. (타입 매개변수들을 선언하는) 타입 매개변수 목록은 메서드의 제한자와 반환 타입 사이에 온다.

아래 코드에서 타입 매개변수 목록은 <E>이고 반환 타입은 Set<E>이다.

1
2
3
4
5
6
// 코드 30-2 제네릭 메서드 (177쪽)
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    Set<E> result = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}
  • 단순한 제네릭 메서드라면 이 정도면 충분하다.
  • 이 메서드는 경고 없이 컴파일 되며, 타입 안전하고, 쓰기도 쉽다.
  • 아래는 이 메서드를 사용하는 간단한 클라이언트 코드이다.
    • 직접 형 변환하지 않아도 어떤 오류나 경고 없이 컴파일된다.
1
2
3
4
5
6
7
// 코드 30-3 제네릭 메서드를 활용하는 간단한 프로그램 (177쪽)
public static void main(String[] args) {
    Set<String> guys = Set.of("톰", "딕", "해리");
    Set<String> stooges = Set.of("래리", "모에", "컬리");
    Set<String> aflCio = union(guys, stooges);
    System.out.println(aflCio);
}
  • 이번에는 항등함수(입력 값을 수정 없이 그대로 반환하는 특별한 함수)를 만들어보자.
  • java 라이브러리인 Function.identity를 사용하면 되지만(아이템 59), 학습을 위해 작성해보자.
  • 항등함수 객체는 상태가 없으니 요청할 때 마다 새로 생성하는 것은 낭비다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 제네릭 싱글턴 팩터리 패턴 (178쪽)
public class GenericSingletonFactory {
    // 코드 30-4 제네릭 싱글턴 팩터리 패턴 (178쪽)
    private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;

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

    // 코드 30-5 제네릭 싱글턴을 사용하는 예 (178쪽)
    public static void main(String[] args) {
        String[] strings = { "삼베", "대마", "나일론" };
        UnaryOperator<String> sameString = identityFunction();
        for (String s : strings)
            System.out.println(sameString.apply(s));

        Number[] numbers = { 1, 2.0, 3L };
        UnaryOperator<Number> sameNumber = identityFunction();
        for (Number n : numbers)
            System.out.println(sameNumber.apply(n));
    }
}
  • T가 어떤 타입이든 UnaryOperator<T>를 사용해도 타입 안전하다.
  • 실제 클라이언트 main코드를 보면 UnaryOperator<String>UnaryOperator<Number>로 사용하는 모습을 볼 수 있다.
    • 위의 예시와 마찬가지로 형변환을 하지 않아도 컴파일 오류나 경고가 발생하지 않는다.

상대적으로 드물긴 하지만, 자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있다.(재귀적 타입 한정: recursive type bound)

  • 재귀적 타입 한정은 주로 타입의 자연적 순서를 정하는 Comparable 인터페이스(아이템 14)와 함께 쓰인다.
1
2
3
public interface Comparable<T> {
    int compareTo(T o);
}
  • 여기서 타입 매개변수 T는 Comparable<T>를 구현한 타입이 비교할 수 있는 원소의 타입을 정의한다.
  • 실제로 거의 모든 타입은 자신과 같은 타입의 원소와만 비교할 수 있다.
    • 따라서 String은 Comparable<String>을 구현하고, Integer는 Comparable<Integer>를 구현하는 식이다.
  • Comparable을 구현한 원소의 컬렉션을 입력받는 메서드들은 주로 그 원소들을 정렬 혹은 검색하거나, 최솟값이나 최대값을 구하는 식으로 사용된다.
    • 이 기능을 수행하려면 컬렉션에 담긴 모든 원소가 상호 비교될 수 있어야 한다.
  • 아래 예제 코드는 이 제약을 코드로 표현한 모습이다.
1
public static<E extends Comparable<E>> E max(Collection<E> c);
  • 타입 한정인 <E extends Comparable<E>>모든 타입 E는 자신과 비교할 수 있다라고 읽을 수 있다.
  • 이와 관련된 아래 예제 코드를- 살펴보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 재귀적 타입 한정을 이용해 상호 비교할 수 있음을 표현 (179쪽)
public class RecursiveTypeBound {
    // 코드 30-7 컬렉션에서 최댓값을 반환한다. - 재귀적 타입 한정 사용 (179쪽)
    public static <E extends Comparable<E>> E max(Collection<E> c) {
        if (c.isEmpty())
            throw new IllegalArgumentException("컬렉션이 비어 있습니다."); // 빈 컬렉션이 인자로 들어오면 IllegalArgumentException을 던지니, Optional<E>를 반환하도록 고치는 편이 나을 것이다(아이템 55)

        E result = null;
        for (E e : c)
            if (result == null || e.compareTo(result) > 0)
                result = Objects.requireNonNull(e);

        return result;
    }

    public static void main(String[] args) {
        List<String> argList = Arrays.asList(args);
        System.out.println(max(argList));
    }
}

핵심 정리: 제네릭 타입과 마찬가지로, 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기도 쉽다. 타입과 마찬가지로, 메서드도 형변환 없이 사용할 수 있는 편이 좋으며, 많은 경우 그렇게 하려면 제네릭 메서드가 되어야 한다. 역시 타입과 마찬가지로, 형변환을 해줘야 하는 기존 메서드는 제네릭하게 만들자. 기존 클라이언트는 그대로 둔 채 새로운 사용자의 삶을 훨씬 편하게 만들어줄 것이다(아이템26)

This post is licensed under CC BY 4.0 by the author.

[이펙티브자바] 아이템29-이왕이면 제네릭 타입으로 만들라

[이펙티브자바] 아이템31-한정적 와일드카드를 사용해 API 유연성을 높이라