자바가 람다를 지원하면서 API를 작성할 때 기존의 오버라이딩을 통한 템플릿 메서드 패턴보다, 같은 효과의 함수 객체를 받는 정적 팩토리나 생성자를 제공하는 방식이 좋아졌다.
함수형 인터페이스는 람다식으로 만든 객체에 접근하기 위해서 사용한다.
불필요한 함수형 인터페이스

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

함수형 인터페이스
- 함수형 인터페이스(Functional Interface)는 자바에서 함수형 프로그래밍을 지원하기 위한 개념이다.
- 자바 8부터 함수형 인터페이스가 도입되었으며, 람다식과 메서드 참조를 활용하여 함수를 일급 객체로 다룰 수 있으며, 이를 통해 코드의 간결성과 가독성을 높일 수 있다.
- 함수형 인터페이스는 딱 하나의 추상 메서드를 가지는 인터페이스로, 이 추상 메서드를 함수형 인터페이스의 시그니처라고 한다.
- 함수형 인터페이스는 @Functionallnterface 어노테이션으로 명식적으로 표시될 수 있다.
표준 함수형 인터페이스
- 자바 표준 라이브러리 java.util.function 패키지에 다양한 용도의 표준 함수형 인터페이스가 담겨 있다.
- 필요한 용도에 맞는게 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용하자.
- API가 다루는 개념의 수가 줄어들어 익히기 더 쉬워진다.
- 표준 함수형 인터페이스들은 유용한 디폴트 메서드를 많이 제공하므로 다른 코드와 상호 운용성도 좋아진다.
아래는 기본 함수형 인터페이스들을 정리한 표이며, 각 인터페이스들은 반환값과 인수의 타입 등에 따라 구분된다.
인터페이스 | 함수 시그니처 | 예 |
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 |
기본 인터페이스는 기본 타입인 int, long, double용으로 각 3개씩 변형이 생긴다.
그 이름은 기본 인터페이스의 이름 앞에 해당 기본 타입 이름을 붙여 짓는다.
Ex) int를 받는 Predicate는 IntPredicate
long을 받아 long을 반환하는 BinaryOperator는 LongBinaryOperator
Function은 입력과 결과 타입이 항상 다르므로, 입력과 결과 타입이 모두 기본 타입이면 접두어로 SrcToResult를 사용한다.
Ex) long을 받아 int를 반환하면 LongToIntFunction
그렇지 않으면 접두어로 ToResult를 사용한다.
Ex) int[] 인수를 받아 long을 반환하면 ToLongFunction<int[]>
BiPredicate<T, U>, BiFunction<T, U, R>, BiConsumer<T, U>은 인수를 2개씩 받는다.
이런 식으로 총 43개의 표준 함수형 인터페이스가 존재한다.
즉, 대부분 상황에서는 직접 작성하는 것보다 표준 함수형 인터페이스를 사용하는 편이 낫다.
(표준 함수형 인터페이스는 대부분 기본 타입만 지원하지만 기본 함수형 인터페이스에 박싱 된 기본 타입을 넣어 사용하지는 말자. 매우 느려진다.)
인터페이스를 직접 작성해야 할 때
그렇다면 직접 코드를 작성해야 할 때는 언제일까?
1. 매개변수 3개를 받는 Predicate처럼 표준 인터페이스 중 필요한 용도에 맞는 게 없다면 직접 작성하자.
2. Comparator<T> 인터페이스를 보며 생각해 보자.
Comparator<T>인터페이스는 구조적으로 ToIntBiFunction<T, U>와 동일하다.
Comparator 특성을 정리하면 다음과 같은 3가지인데, 이 중 하나 이상을 만족한다면 전용 함수형 인터페이스를 구현해야 하는 건 아닌지 고민해 보자.
- 자주 쓰이며, 이름 자체가 용도를 명확히 설명해 준다.
- 반드시 따라야 하는 규약이 있다.
- 유용한 디폴트 메서드를 제공할 수 있다.
@FunctionalInterface 애너테이션
@FunctionalInterface 애너테이션을 사용하는 이유는 @Override를 사용하는 이유와 비슷하다.
- 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려준다.
- 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 해 준다.
- 그 결과 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다.
직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface를 사용하자
서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중정의해서는 안된다.
ExecutorService의 submit 메서드는 Callable<T>를 받는 것과 Runnable을 받는 것을 다중정의 했다.
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
- 첫 번째 메서드는 Callable<T>를 매개변수로 받아서 Future<T>를 반환
- 두 번째 메서드는 Runnable을 매개변수로 받아서 Future<?>를 반환
메서드 시그니처가 서로 다르기 때문에, 호출하는 쪽에서 매개변수의 타입에 따라 메서드를 선택할 수 있긴 하지만
이는 클라이언트에게 불필요한 모호함만 안겨줄 뿐이며, 이로 인해 문제가 일어나기도 한다.
결론
- 자바도 람다를 지원하니 API를 설계할 때 람다도 염두에 두자.
- 입력값과 반환값에 함수형 인터페이스 타입을 활용하자.
- 보통은 java.util.function 패키지의 표준 함수형 인터페이스를 사용하는 것이 가장 좋은 선택이다.
- 흔치는 않지만 직접 새로운 함수형 인터페이스를 만들어 쓰는 편이 나을 수도 있다.
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] Item 46. 스트림에서는 부작용 없는 함수를 사용하라 (1) | 2023.07.09 |
---|---|
[Effective Java] Item 45. 스트림은 주의해서 사용하라 (0) | 2023.07.08 |
[Effective Java] Item 43. 람다보다는 메서드 참조를 사용하라 (0) | 2023.07.08 |
[Effective Java] Item 42. 익명 클래스보다는 람다를 사용하라 (0) | 2023.07.02 |
[Effective Java] Item 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2023.06.30 |