[이펙티브 자바] 아이템 44. 표준 함수형 인터페이스를 사용하라
1) 개요
- 자바가 람다를 지원하면서 API를 작성하는 모범 사례도 크게 바뀌었다.
예컨대, 상위 클래스의 기본 메서드를 재정의해 원하는 동작을 구현하는
템플릿 메서드 패턴의 매력이 크게 줄었다.
이를 대체하는 현대적인 해법은 같은 효과의 함수 객체를 받는 정적 팩터리나 생성자를 제공하는 것이다.
이 내용을 일반화해서 말하면 함수 객체를 매개변수로 받는 생성자와 메서드를 더 많이 만들어야 한다.
이 때, 함수형 매개변수 타입을 올바르게 선택해야 한다.
- LinkedHashMap을 생각해보자.
이 클래스의 protected 메서드인 removeEldestEntry를 재정의하면 캐시로 사용할 수 있다.
맵에 새로운 키를 추가하는 put 메서드는 이 메서드를 호출하여 true가 반환되면
맵에서 가장 오래된 원소를 제거한다.
예컨대, removeEldestEntry를 다음처럼 재정의하면 맵에 원소가 100개가 될 때까지 커지다가,
그 이상이 되면 새로운 키가 더해질 때마다 가장 오래된 원소를 하나씩 제거한다.
즉, 가장 최근 원소 100개를 유지한다.
protected boolean removeEldestEntry(Map.Entry<K,V> eldest){
return size() > 100;
}
- 잘 동작하지만, 람다를 사용하면 훨씬 잘 해낼 수 있다.
LinkedHashMap을 오늘날 다시 구현한다면 함수 객체를 받는 정적 팩터리나 생성자를 제공했을 것이다.
removeEldestEntry 선언을 보면 이 함수 객체는 Map.Entry<K,V>를 받아 boolean을 반환해야 할 것 같지만,
꼭 그렇지는 않다.
removeEldestEntry는 size()를 호출해 맵 안의 원소 수를 알아내는데,
removeEldestEntry가 인스턴스 메서드라서 가능한 방식이다.
- 하지만 생성자에 넘기는 함수 객체는 이 맵의 인스턴스 메서드가 아니다.
팩터리나 생성자를 호출할 때는 맵의 인스턴스가 존재하지 않기 때문이다.
따라서 맵은 자기 자신도 함수 객체에 건네줘야 한다.
이를 반영한 함수형 인터페이스는 다음처럼 선언할 수 있다.
@FunctionalInterface interface EldestEntryRemovelFunction<K, V>{
boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}
- 이 인터페이스도 잘 동작하기는 하지만, 굳이 사용할 이유는 없다.
자바 표준 라이브러리에 이미 같은 모양의 인터페이스가 준비되어 있기 때문이다.
java.util.function 패키지를 보면 다양한 용도의 표준 함수형 인터페이스가 담겨 있다.
필요한 용도에 맞는 게 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용하라.
그러면 API가 다루는 개념의 수가 줄어들어 익히기 더 쉬워진다.
- 또한, 표준 함수형 인터페이스들은 유용한 디폴트 메서드를 많이 제공하므로
다른 코드와의 상호운용성도 크게 좋아질 것이다.
예컨대, Predicate 인터페이스는 프레디키트(predicate)들을 조합하는 메서드를 제공한다.
앞의 LinkedHashMap 예에서는 직접 만든 EldestEntryRemovalFunction 대신
표준 인터페이스인 BiPredicate<Map<K,V>, Map.Entry<K,V>>를 사용할 수 있다
- java.util.function 패키지에는 총 43개의 인터페이스가 담겨 있다.
전부 기억하긴 어렵겠지만, 기본 인터페이스 6개만 기억하면 나머지를 충분히 유추해낼 수 있다.
이 기본 인터페이스들은 모두 참조 타입용이다. 하나씩 살펴보자.
- Operator 인터페이스는 인수가 1개인 UnaryOperator와 2개인 BinaryOperator로 나뉘며,
반환값과 인수의 타입이 같은 함수를 뜻한다.
Predicate 인터페이스는 인수 하나를 받아 boolean을 반환하는 함수를 뜻하며,
Function 인터페이스는 인수와 반환 타입이 다른 함수를 뜻한다.
- Supplier 인터페이스는 인수를 받지 않고 값을 반환(혹은 제공)하는 함수를,
Consumer 인터페이스는 인수를 하나 받고 반환값은 없는(특히 인수를 소비하는) 함수를 뜻한다.
다음은 이 기본 함수형 인터페이스들을 정리한 표다.
- 기본 인터페이스는 기본 타입인 int, long, double용으로 각 3개씩 변형이 생겨난다.
그 이름도 기본 인터페이스의 이름 앞에 해당 기본 타입 이름을 붙여 지었다.
예컨대, int를 받는 Predicate는 IntPredicate이 되고,
long을 받아 long을 반환하는 BinaryOperator는 LongBinaryOperator가 되는 식이다.
- 이 변형들 중 유일하게 Function의 변형만 매개변수화됐다.
정확히는 반환 타입만 매개변수화됐는데, 예를 들어 LongFunction<int[]>>은
long 인수를 받아 int[]을 반환한다.