핵심 정리
함수형 인터페이스를 사용하면 람다보다 더 간결하고 명확한 코드를 얻게될 수도 있다. 자주 사용되는 함수형 인터페이스는 표준 함수형 인터페이스로 구현돼있으므로 Comparator과 같은 특정한 경우가 아니라면 표준 함수형 인터페이스를 사용하자.
예시
자바에서는 람다에 활용하기 위해 만들어둔 표준 함수형 인터페이스가 존재한다.
예시 1
removeEldestEntry() 메소드는 LinkedHashMap 클래스에 정의된 메소드이다. 맵에 새로운 요소를 추가할 때마다 removeEldestEntry() 메소드가 호출되는데, 이 메소드가 true를 반환하면 가장 오래된 요소를 제거한다.
기본적으로 removeEldestEntry() 메소드는 항상 false를 반환한다. 그러나 해당 메소드를 오버라이드하여 맵의 최대 크기를 제한하거나 다른 제거 정책을 구현할 수 있다.
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > 100;
}
위 코드는 메소드를 오버라이딩하여 맵의 최대 크기를 100으로 제한한 코드이다.
@FunctionalInterface
interface EldestEntryRemovalFunction<K, V> {
boolean remove(Map<K, V> map, Map.Entry<K, V> eldest);
}
위 코드는 동일한 동작을 하는 함수형 인터페이스이며, remove() 메소드는 맵과 가장 오래된 요소를 인자로 받아서 boolean 값을 반환한다.
함수형 인터페이스를 이용한 방법은 LinkedHashMap 이외의 다른 종류의 맵에서도 사용할 수 있으며, 인터페이스를 구현하는 람다 표현식이나 메소드 참조를 사용하면 코드가 더 간결하고 이해하기 쉬워진다.
// 람다 표현식
EldestEntryRemovalFunction<String, Integer> removalFunction = (map, eldest) -> map.size() > 100;
// 메소드 참조
class MyRemovalPolicy {
static <K, V> boolean shouldRemove(Map<K, V> map, Map.Entry<K, V> eldest) {
return map.size() > 100;
}
}
EldestEntryRemovalFunction<String, Integer> removalFunction = MyRemovalPolicy::shouldRemove;
그러나 위 함수형 인터페이스는 이미 표준 함수형 인터페이스로 구현돼있다. (BiPredicate<Map<K, V>, Map.Entry<K, V>>)
표준 함수형 인터페이스
: java.util.function 패키지의 43개의 인터페이스
기본 인터페이스 6개만 기억하면 나머지는 적절히 유추할 수 있다.
표준 함수형 기본 인터페이스
Operator 인터페이스(UnaryOperator 인터페이스 , BinaryOperator 인터페이스)
- 반환값과 인수의 타입이 같은 함수
- UnaryOperator(인수가 1개)와 BinaryOperator(인수가 2개)로 나뉜다.
- UnaryOperator<T>
- 함수 시그니처: T apply(T t)
- 예: String::toLowerCase
- BinaryOperator<T>
- 함수 시그니처: T apply(T t1, T t2)
- 예: BigInteger:add
Predicate 인터페이스
- 인수 하나를 받아 boolean을 반환하는 함수
- Predicate<T>
- 함수 시그니처: boolean test(T t)
- 예: Collection::isEmpty
Function 인터페이스
- 인수와 반환 타입이 다른 함수
- Function<T, R>
- 함수 시그니처: R apply(T t)
- 예: Arrays::asList
Supplier 인터페이스
- 인수를 받지 않고 값을 반환(혹은 제공)하는 함수
- Supplier<T>
- 함수 시그니처: T get()
- 예: Instant::now
Consumer 인터페이스
- 인수를 하나 받고 반환값은 없는(특히 인수를 소비하는) 함수
- Consumer<T>
- 함수 시그니처: void accept(T t)
- 예: System.out::println
기본형 타입을 위한 함수형 인터페이스의 변형
기본 인터페이스는 기본 타입인 int, long, double용으로 각 3개씩 변형이 생긴다.
(ex. int를 받는 PIntPredicate, LongBinaryOperator)
Function 인터페이스는 인수와 반환 타입이 다르므로 9개 더 존재한다.
Function 인터페이스
- SrcToResult
- 입력과 결과 타입이 모두 기본 타입일 경우
- ex. long을 받아 int를 반환하면 LongToIntFunction
- ToResult
- 입력이 객체 참조이고 결과가 int, long, double인 경우
- ex. ToLongFunction<int[]>은 int[] 인수를 받아 long을 반환
BiPredicate<T, U>, BiFunction<T, U, R>, BiConsumer<T, U>
위 3개의 기본 함수형 인터페이스는 인수를 2개씩 받는다.
ObjDoubleConsumer<T>, ObjIntConsumer<T>, ObjLongConsumer<T>
객체 참조와 기본 타입, 총 인수를 2개 받는 Consumer 변형이다.
BooleanSupplier
boolean을 반환하도록 한 Supplier의 변형이다.
기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 말자. (아이템 61)
함수형 인터페이스를 직접 구현해야 할 경우
vs
표준 함수형 인터페이스를 사용해야 할 경우
함수형 인터페이스를 직접 구현해야 할 경우
- 표준 인터페이스 중 필요한 용도에 맞는 게 없는 경우
- 아래 세 가지 중 하나 이상을 만족할 경우 고려해보자(ex. Comparator)
- 자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다.
- 반드시 따라야 하는 규약이 있다.
- 유용한 디폴드 메소드를 제공할 수 있다.
주의
인터페이스는 구현하는 쪽을 생각해 설계하라.(아이템 21)
함수형 인터페이스를 직접 구현할 경우 @FunctionalInterface를 항상 사용하라.
해당 어노테이션을 사용하는 이유는 아래와 같다.
- 해당 인터페이스가 람다용으로 설계된 것임을 명시한다.
- 해당 인터페이스가 추상 메소드를 오직 하나만 가지고 잇어야 컴파일되게 해준다.
- 유지보수 과정에서 메소드를 추가하지 못하게 막아준다.
함수형 인터페이스 API 사용할 경우 주의점
서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메소드들은 다중으로 오버로드하지 말자. 보통 람다를 넘기기 떄문에 강제 형변환이 필요할 수도 있고, 코드가 더러워진다.(아이템 52)
'Language > Java' 카테고리의 다른 글
[effective java] 아이템 46. 스트림에서는 부작용 없는 함수를 사용하라. (0) | 2023.07.03 |
---|---|
[effective java] 아이템 45. 스트림은 주의해서 사용하라. (0) | 2023.07.03 |
[effective java] 아이템 43. 람다보다는 메서드 참조를 사용하라. (0) | 2023.07.03 |
[effective java] 아이템 42. 익명 클래스보다는 람다를 사용하라. (0) | 2023.07.02 |
[effective java] 아이템 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라. (0) | 2023.07.02 |