이펙티브 자바 #item30 이왕이면 제네릭 메서드로 만들라

임현규·2023년 2월 6일
0

이펙티브 자바

목록 보기
30/47

타입에 유연한 메서드

Collections에서 제공하는 정적 메서드는 대부분 제네릭 메서드이다. 제네릭 메서드에 장점은 타입에 유연하다는 점이다. 물론 제네릭을 사용한다면 유의해야 할 점이 있다.

이전에도 나온 내용이지만 로 타입을 사용하지 말것, 타입 경고를 반드시 해결해야 한다.

사용 방법

1. 기본 예제

public class Hello {

    public static <E> List<E> convert(Queue<E> queue) {
        return new ArrayList<>(queue);
    }

}

아주 간단하게 Queue를 List로 변환하는 정적 메서드를 만들어봤다. 위와 같이 E 매개변수 타입을 정의해서 사용하면 된다.

2. 제네릭 싱글턴 팩터리 패턴

때로는 불변 객체를 여러 타입으로 활용할 수 있게 만들어야 할 때가 있다. 제네릭은 런타입에 타입 정보가 소거되므로 하나의 객체를 어떤 타입으로든 매개변수화 할 수 있다. 그러나 이렇게 하려면 타입 매개변수에 맞게 매번 그 객체의 타입을 바꿔주는 정적 팩터리를 만들어주어야 한다. 이를 정적 싱글턴 팩터리라 한다. java에 Collections.reverseOrder()가 이에 해당한다.

위의 코드를 살펴보면 reverseOrder른 Comparator를 리턴하는데 이를 ReverseComparator의 상수 REVERSE_ORDER를 호출함을 알 수 있다. 이때 ReverseComparator는 싱글턴 구조이며 compare를 보면 일반적인 compare와 반대로 이루어진 클래스임을 알 수 있다. 이를 Comparator<T>로 형변환해서 사용한다.

이번엔 항등함수에 이를 적용해보자. 항등함수의 경우 상태가 없기 때문에 매번 호출해주어야 하지만 제네릭을 활용하면 최적화할 수 있다. 우선 Object 타입으로 상수형태의 항등함수를 미리 만들어둔다. 그리고 이를 정적 제네릭 메서드로 호출한다. 마치 싱글턴과 비슷하다.

prvate static UanaryOperator<Object> IDENTITY_FN = (t) -> t;

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

IDENTITY_FN을 UnaryOperator<T>로 형변환하면 비검사 형변환 경고가 발생한다. T가 어떤 타입이든 UnaryOPerator<Object>는 UnaryOperator<T>가 아니기 때문이다. 하지만 항등함수란 입력 값을 수정 없이 그대로 반환하는 특별한 함수이므로 T가 어떤 타입이든 간에 안전하다.

2. 재귀적 타입 한정

사용 빈도가 드물긴 하지만 자기 자신이 들어간 표현식을 사용해서 타입 매개변수의 허용 범위를 제한할 수 있다. 그 방법중 하나도 재귀적 타입 한정(recursive type bound) 개념이다. 이는 주로 타입의 순서가 필요한 Comparable 인터페이스와 함께 쓰인다.

class에 Comparable 인터페이스를 구현하면 해당 클래스는 대소관계 비교가 가능해진다. 같은 타입의 원소를 비교하는 데 보통 이를 적용하는 메서드는 최솟값, 최대값, 정렬과 같은 곳에서 사용한다.

여기에 일반 제네릭은 활용할 수 없는데 그 이유는 매개변수 타입이 Comparable을 구현한 E임을 보장할 수 없기 때문이다. 그래서 단순히 E를 정의하고 내부에서 max, min, sort등을 호출하면 컴파일 에러가 발생한다.

사용 방법은 다음과 같다.

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

Comparable<E>의 하위 타입의 E라는 뜻으로 재귀적으로 쓰였지만
모든 타입 E는 비교가 가능한 E 타입 이렇게 해석하면 된다.

public class Hello {

    public static <E extends Comparable<E>>  E max(Collection<E> c) {
        if (c.isEmpty()) {
            throw new IllegalArgumentException("해당 컬렉션이 비어 있습니다.");
        }
        E result = null;
        for (E element : c) {
            if (result == null || element.compareTo(result) > 0) {
                result = Objects.requireNonNull(element);
            }
        }
        return result;
    }

}

해당 코드를 해석하면 Collection에서 최대값을 구하는 정적 메서드를 구현한 것이다. 범용적으로 사용할 수 있기 때문에 좋다.

하지만 실제 자바에서 구현은 위의 코드와 다르게 하나가 더 추가되어 있다.

<E extends Comparable<? super T>> public static

왜 이런 방식으로 정의한 것일까?
제네릭은 기본적으로 불공변이다. 만약 어떤 클래스의 Comparable을 구현했는데 해당 클래스를 상속한 하위 클래스가 존재한다면 어떨까? 하위 클래스는 Comparable을 구현하지 않았기 때문에 상위 클래스에서 정의했다고 그대로 사용하면 작동하지 않는다. 그 이유는 Comparable이 제네릭 인터페이스이고 타입에 대해 불공변이기 때문에 상속이든 뭐든 컴파일러는 이해하지 못한다.

이 때 Comparable<? super T> 를 활용하는 것이다. 위 코드의 뜻은 T의 상위 클래스 Comparable이 구현했다면...? 이라는 비한정적 와일드카드 타입을 활용하면 상속에 대해서도 안전한 제네릭 타입을 사용할 수 있다.

이래서 ... 상속은 최대한 지양하라는 것 같다.. 예측치 못한 부작용... 그로 인한 복잡한 제네릭...

profile
엘 프사이 콩그루

0개의 댓글