Effective Java - item 44
표준 함수형 인터페이스를 사용하라
들어가며
자바가 람다를 지원하면서 API를 작성하는 모범 사례가 크게 바뀌었다. 템플릿 메서드 패턴의 매력이 줄고 함수 객체를 매개변수로 받는 생성자와 메서드를 더 많이 만들게 되었다. 따라서 함수형 매개변수 타입을 올바르게 선택해야 한다.
LinkedHashMap
LinkedHashMap의 removeEldestEntry를 재정의하면 캐시로 사용할 수 있다. 다음처럼 재정의하면 맵에 원소가 100개를 넘어가면 가장 오래된 원소를 제거한다.
1
2
3
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > 100;
}
이를 람다로 구현하려면 함수형 인터페이스가 필요하다.
1
2
3
4
@FunctionalInterface
interface EldestEntryRemovalFunction<K,V> {
boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}
하지만 굳이 이렇게 만들 필요가 없다. 자바 표준 라이브러리에 이미 같은 모양의 BiPredicate<Map<K,V>, Map.Entry<K,V»가 있기 때문이다. 필요한 용도에 맞는 게 있다면 직접 구현하지 말고 표준 함수형 인터페이스를 활용하면 된다.
표준 함수형 인터페이스를 활용하면 다음과 같이 코드의 양을 줄일 수 있다.
6개의 기본 인터페이스
UnaryOperator
1
2
3
4
5
6
// 값을 변환하거나 가공할 때 사용
UnaryOperator<String> toUpperCase = String::toUpperCase;
toUpperCase.apply("hello"); // "HELLO"
// 리스트의 모든 요소를 변환
list.replaceAll(s -> s.toUpperCase()); // UnaryOperator 사용
BinaryOperator
1
2
3
4
5
6
// 두 값을 합치거나 비교할 때 사용
BinaryOperator<Integer> max = (a, b) -> a > b ? a : b;
max.apply(10, 20); // 20
// 스트림에서 reduce 연산 시 사용
list.stream().reduce(0, (a, b) -> a + b); // BinaryOperator 사용
Predicate
1
2
3
4
5
6
// 조건을 검사하거나 필터링할 때 사용
Predicate<String> isEmpty = String::isEmpty;
isEmpty.test(""); // true
// 스트림에서 filter 연산 시 사용
list.stream().filter(s -> s.length() > 5); // Predicate 사용
Function<T,R>
1
2
3
4
5
6
// 타입을 변환하거나 매핑할 때 사용
Function<String, Integer> parseInt = Integer::parseInt;
parseInt.apply("123"); // 123
// 스트림에서 map 연산 시 사용
list.stream().map(s -> s.length()); // Function 사용
Supplier
1
2
3
4
5
6
// 값을 생성하거나 제공할 때 사용 (지연 초기화에 유용)
Supplier<Instant> now = Instant::now;
now.get(); // 현재 시간
// Optional에서 값이 없을 때 기본값 제공
optional.orElseGet(() -> new ArrayList<>()); // Supplier 사용
Consumer
1
2
3
4
5
6
// 값을 소비하거나 출력할 때 사용
Consumer<String> println = System.out::println;
println.accept("Hello"); // 출력
// 스트림에서 forEach 연산 시 사용
list.forEach(s -> System.out.println(s)); // Consumer 사용
기본 타입용 변형으로 IntPredicate, LongBinaryOperator 같은 인터페이스도 있다. 인수를 2개 받는 BiPredicate<T,U>, BiFunction<T,U,R>, BiConsumer<T,U>도 있다.
전용 함수형 인터페이스를 구현해야 하는 경우
표준 인터페이스로 해결되지 않는다면 직접 작성해야 한다. 하지만 구조적으로 똑같은 표준 인터페이스가 있어도 직접 작성해야 할 때가 있고 Comparator
- 자주 쓰이며 이름 자체가 용도를 명확히 설명한다.
- 반드시 따라야 하는 규약이 있다.
- 유용한 디폴트 메서드를 제공한다.
이 중 하나 이상을 만족한다면 전용 함수형 인터페이스 구현을 고민해보자.
@FunctionalInterface
직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface를 붙여라. 이는 다음 세 가지 목적이 있다.
- 람다용으로 설계되었음을 알려준다.
- 추상 메서드를 하나만 가져야 컴파일된다.
- 유지보수 과정에서 메서드 추가를 막는다.
주의점
서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중정의 해선 안된다. ExecutorService의 submit 메서드처럼 클라이언트에게 모호함을 주고 형변환이 필요해지기 때문이다.
마치며
API를 설계할 때 람다를 염두에 두어라. 입력값과 반환값에 함수형 인터페이스 타입을 활용하라. 보통은 java.util.function 패키지의 표준 함수형 인터페이스를 사용하는 것이 가장 좋다. 단 직접 만들어 쓰는 편이 나을 때도 있음을 기억하자.