@FunctionalInterface
Functional Interface는 구현해야 할 추상 메소드가 하나만 정의된 인터페이스를 가리킨다.
[Why]
자바가 람다를 지원하면서 API를 작성하는 모범 사례도 크게 바뀌었다.
예컨대 상위 클래스의 기본 메서드를 재정의해 원하는 동작을 구현하는 템플릿 메서드 패턴의 매력이 크게 줄었다.
- 이를 대체하는 현대적인 해법은 같은 효과의 함수 객체를 받는 정적 팩터리나 생성자를 제공하는 것이다.
- 따라서 함수 객체를 매개변수로 받는 생성자와 메서드를 많이 만들어야 한다.
- 이때 함수형 매개변수 타입을 올바르게 선택해야 한다.
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > 100;
}
// protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
// return false;
// }
// LinkedHashMap에 정의된 removeEldestEntry
- LinkedHashMap의 메서드인 removeEldestEntry를 재정의하면 캐시로 사용할 수 있다.
- 맵에 새로운 키를 추가하는 put 메서드는 이 메서드를 호출하여 true가 반환되면 맵에서 가장 오래된 원소를 제거한다.
- 이 함수 객체는 Map.Entry<K,V>를 받아 boolean을 반환해야 할 것 같지만, 꼭 그렇지는 않다.
- removeEldestEntry는 size()를 호출해 원소 수를 알아내는데, removeEldestEntry가 인스턴스 메서드라서 가능한 방식이다.
- 하지만 생성자에 넘기는 함수 객체는 이 맵의 인스턴스 메서드가 아니다.
- 팩터리나 생성자를 호출할 때는 맵의 인스턴스가 존재하지 않기 때문이다.
- 따라서 맵은 자기 자신도 함수 객체에 건네줘야 한다.
- 람다를 사용하면 훨씬 잘 해낼 수 있다.
@FunctionalInterface
interface EldestEntryRemovalFunction<K,V> {
boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}
- 이 인터페이스도 잘 동작하기는 하지만, 굳이 사용할 이유가 없다.
- 자바 표준 라이브러리에 이미 같은 모양의 인터페이스가 준비되어 있기 때문이다.
BiPredicate<Map<K,V>, Map.Entry<K,V>>
필요한 용도에 맞는 게 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용하라.
[When]
표준 인터페이스 중 필요한 용도에 맞는 게 없다면 직접 작성해야 한다. (그게 아니라면 표준 인터페이스를 사용하자)
- Comparator<T> 인터페이스를 생각해보자.
- 자바 라이브러리에 Comparator<T>를 추가할 때 이미 ToIntBiFunction<T,U>가 존재했더라도 사용하면 안됐다.
- 이유는 다음 세 가지 때문인데, 이 중 하나 이상을 만족한다면 전용 함수형 인터페이스를 구현해야 하는지 고민해봐야 한다.
- 자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다.
- 반드시 따라야 하는 규약이 있다.
- 유용한 디폴트 메서드를 제공할 수 있다.
[How]
인터페이스 | 함수 시그니처 | 예 |
UnaryOperator<T> | T apply(T t) | String::toLowerCase |
BinaryOperator<T> | T apply(T t1, T t2) | BigInteger::add |
Predicate<T> | boolean test(T t) | Collection::isEmpty |
Function<T,R> | R apply(T t) | Arrays::asList |
Supplier<T> | T get() | Instant::now |
Consumer<T> | void accept(T t) | System.out::println |
- java.util.function 패키지에는 총 43개의 인터페이스가 담겨 있다.
- 전부 기억하긴 어렵겠지만, 기본 인터페이스 6개만 기억하면 나머지를 충분히 유추해낼 수 있다.
- 기본 인터페이스는 기본 타입인 int, long, double 용으로 각 3개씩 변형이 생겨난다.
e.g.) Predicate → IntPredicate, BinaryOperator → LongBinaryOperator
기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 말자.
- 동작은 하지만 "박싱된 기본 타입 대신 기본 타입을 사용하라"라는 아이템61의 조언을 위배하여 계산량이 많을 때 성능이 처참해진다.
직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface 애너테이션을 사용하라.
- 첫 번째, 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려준다.
- 두 번째, 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 해준다.
- 세 번째, 그 결과 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다.
함수형 인터페이스를 API에서 사용할 때의 주의점
- 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중정의해서는 안 된다.
- 클라이언트에게 불필요한 모호함만 안겨줄 뿐이며, 이 모호함으로 인해 실제로 문제가 일어나기도 한다.
'독서찰기(讀書札記) > 이펙티브 자바' 카테고리의 다른 글
[아이템 46] 스트림에서는 부작용 없는 함수를 사용하라 (0) | 2022.03.05 |
---|---|
[아이템 45] 스트림은 주의해서 사용하라 (0) | 2022.03.05 |
[아이템 43] 람다보다는 메서드 참조를 사용하라 (0) | 2022.03.03 |
[아이템 42] 익명 클래스보다는 람다를 사용하라 (0) | 2022.03.02 |
[아이템 37] ordinal 인덱싱 대신 EnumMap을 사용하라 (0) | 2022.02.24 |