Post

Effective Java - item 44

표준 함수형 인터페이스를 사용하라

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가 그 예다. 구조적으로는 ToIntBiFunction<T,T>와 동일하지만 독자적인 인터페이스로 존재한다. 이유는 다음과 같다.

  1. 자주 쓰이며 이름 자체가 용도를 명확히 설명한다.
  2. 반드시 따라야 하는 규약이 있다.
  3. 유용한 디폴트 메서드를 제공한다.

이 중 하나 이상을 만족한다면 전용 함수형 인터페이스 구현을 고민해보자.

@FunctionalInterface

직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface를 붙여라. 이는 다음 세 가지 목적이 있다.

  1. 람다용으로 설계되었음을 알려준다.
  2. 추상 메서드를 하나만 가져야 컴파일된다.
  3. 유지보수 과정에서 메서드 추가를 막는다.

주의점

서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중정의 해선 안된다. ExecutorService의 submit 메서드처럼 클라이언트에게 모호함을 주고 형변환이 필요해지기 때문이다.

마치며

API를 설계할 때 람다를 염두에 두어라. 입력값과 반환값에 함수형 인터페이스 타입을 활용하라. 보통은 java.util.function 패키지의 표준 함수형 인터페이스를 사용하는 것이 가장 좋다. 단 직접 만들어 쓰는 편이 나을 때도 있음을 기억하자.

This post is licensed under CC BY 4.0 by the author.