아이템 44. 표준 함수형 인터페이스를 사용하라. - ksw6169/effective-java GitHub Wiki

개요

자바 8부터 람다를 지원하면서 상위 클래스의 메소드를 재정의해 원하는 동작을 구현하는 템플릿 메소드 패턴의 매력이 크게 줄었다. 이를 대체하는 현대적인 해법은 같은 효과의 함수 객체를 받는 정적 팩토리나 생성자를 제공하는 것이다. 이 말을 일반화해서 말하자면 함수 객체를 매개변수로 받는 생성자와 메소드를 더 많이 만들어야 한다는 말이다.


LinkedHashMap의 removeEldestEntry() 를 개선해보자.

다음은 LinkedHashMap 의 인스턴스 메소드인 removeEldestEntry() 이다. 이를 함수형 인터페이스를 이용해 개선해보자.

/**
 * LinkedHashMap의 인스턴스 메소드로 이를 하위 클래스에서 재정의하면 캐시로 사용할 수 있다.
 * true를 반환하면 가장 오래된 원소(put 된 순서에 따라 구분)를 맵에서 제거한다.
 * 이 코드에서는 size() 가 100을 초과할 경우 가장 오래된 원소를 하나씩 제거한다.
 */
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return size() > 100;
}

removeEldestEntry()LinkedHashMap 의 생성자 혹은 put() 을 호출하는 시점에 호출되는데 함수 객체를 전달하면 함수 객체가 생성되었을 떄 LinkedHashMap 은 생성되기 전이므로 인스턴스 메소드인 size() 를 사용할 수 없다. 따라서 맵은 자기 자신도 함수 객체에 건네줘야 한다. 이를 반영한 함수형 인터페이스는 다음처럼 선언할 수 있다.

@FunctionalInterface
interface EldestEntryRemovalFunction<K,V> {
    // 전달 받은 map의 size를 계산해서 처리할 수 있도록 함
    boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}

위 인터페이스를 사용할 수도 있지만, 자바 표준 라이브러리에 이미 같은 모양의 인터페이스가 준비되어 있으므로 굳이 사용할 이유는 없다.


표준 함수형 인터페이스를 잘 활용하자.

java.util.function 패키지를 보면 다양한 용도의 표준 함수형 인터페이스가 담겨 있다. 필요한 용도에 맞는 게 있다면 직접 구현하지 말고 표준 함수형 인터페이스를 활용하라. 표준 함수형 인터페이스를 활용하였을 때 장점은 다음과 같다.

  • API가 다루는 개념의 수가 줄어들어 익히기 더 쉬워진다.
  • 유용한 디폴트 메소드를 많이 제공하므로 다른 코드와의 상호 운용성이 크게 좋아진다.

용도에 맞는 표준 함수형 인터페이스가 없다면 직접 작성하라.

이 중 하나 이상을 만족한다면 전용 함수형 인터페이스를 구현해야 하는 건 아닌지 고민해야 한다.

  • 자주 쓰이며 이름 자체가 용도를 명확히 설명해준다.
  • 반드시 따라야 하는 규약이 있다.
  • 유용한 디폴트 메소드를 제공할 수 있다.

@FunctionalInterface 를 사용하는 이유

직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface 를 사용해야 한다. 이유는 다음과 같다.

  • 해당 클래스의 코드를 보는 사람들에게 이 인터페이스가 람다용으로 설계된 것임을 알려준다.
  • 해당 인터페이스가 추상 메소드를 오직 하나만 가지고 있어야 컴파일되게 해준다.
  • 유지보수 과정에서 누군가 실수로 이 인터페이스에 메소드를 추가하지 못하게 막는다.

함수형 인터페이스를 사용할 때 주의할 점

1. 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지 말자.

표준 함수형 인터페이스 대부분은 기본 타입만 지원한다. 그렇다고 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 말자. 오토박싱, 오토언박싱에 의한 성능 저하가 발생하기 때문이다.


2. 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메소드들을 다중 정의해서는 안된다.

이유는 클라이언트에게 불필요한 모호함만 안겨주기 때문이다. 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메소드들을 다중 정의하는 경우, Client에서 원하는 메소드를 호출하기 위해 해당 함수형 인터페이스 타입으로 형변환해줘야 할 때가 많이 발생한다.

public interface ExecutorService extends Executor {
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result); 
    <T> Future<T> submit(Runnable task);
}

이런 문제를 피하는 가장 쉬운 방법은 서로 다른 함수형 인터페이스를 같은 위치의 인수로 사용하는 다중 정의를 피하는 것이다.


핵심 정리

  • 자바도 람다를 지원하므로 API를 설계할 때 람다도 염두에 두어야 한다.
  • 즉, 입력값과 반환값에 함수형 인터페이스 타입을 활용하라.
  • 보통은 java.util.function 패키지의 표준 함수형 인터페이스를 사용하는 것이 가장 좋은 선택이다.

참고 자료

  • Effective Java 3/E
⚠️ **GitHub.com Fallback** ⚠️