자바가 람다를 지원하면서 상위 클래스의 기본 메서드를 재정의해 원하는 동작을 구현하는 템플릿 메서드 패턴의 매력이 크게 줄었다.
같은 효과의 함수 객체를 받는 정적 팩터리나 생성자를 제공하는 것으로 대체할 수 있기 때문이다.
이때, 함수형 매개변수 타입을 올바르게 선택해야 한다.
ex)LinkedHashMap에서 removeEldestEntry 메서드를 재정의하는 경우
-> removeEldestEntry메서드를 재정의함으로써, 캐시로 사용할 수 있다.
public class CacheExample { public static void main(String[] args) { // 익명 클래스에는 <> 처럼 제네릭 타입 생략을 할 수 없다. LinkedHashMap<String, Integer> map = new LinkedHashMap<String, Integer>() { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > 3; } }; map.put("a", 1); map.put("b", 2); map.put("c", 3); map.put("d", 4); // 결과: {b=2, c=3, d=4} System.out.println(map); } }
removeEldestEntry는 잘 동작하지만 람다를 이용하여 다시 구현한다면, 함수 객체를 받는 정적 팩터리나 생성자를 제공했을 것이다.
이때, 재정의한 removeEldestEntry는 size 메서드를 호출하는데, 이는 인스턴스 메서드라 가능하다.하지만 팩터리나 생성자를 호출할 때는 Map의 인스턴스가 존재하지 않아 Map 자신도 함수 객체에 넘겨주어야 한다.
이를 함수형 인터페이스로 선언하면 아래와 같다.
(람다 표현식으로 구현이 가능한 인터페이스는 오직 추상 메서드가 1개인 인터페이스만 가능하다.@FunctionInterface interface EldestEntryRemovalFunction<K, V> { boolean remove(Map<K,V> map, Map.Entry<K, V> eldest); }
이 맥락에서 추상 메서드가 1개인 인터페이스를 함수형 인터페이스라고 한다.)
위의 인터페이스는 잘 동작하지만, 굳이 사용할 이유는 없다.
자바 표준 라이브러리에 이미 같은 모양의 인터페이스가 준비되어 있기 때문이다.
필요한 용도에 맞는 게 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용하도록 한다.
- API가 다루는 개념의 수가 줄어들어 익히기 더 쉬워질 것이다.
- 표준 함수형 인터페이스들은 유용한 디폴트 메서드를 많이 제공하므로 다른 코드와의 상호운용성도 좋아질 것이다.
표준 함수형 인터페이스
java.util.function
- java.util.function 패키지에는 총 43개의 인터페이스가 있다. 아래는 기본 함수형 인터페이스들을 정리한 표다.
- 각각의 기본 인터페이스들은 기본 타입인 int, long, double용에 맞게 변형된 형태가 존재한다.
인터페이스 | 함수 시그니처 | 의미 | 예시 |
UnaryOperator<T> | T apply(T t) | 반환값과 인수의 타입이 같은 함수, 인수는 1개 | String::toLowerCase |
BinaryOperator<T> | T apply(T t1, T t2) | 반환값과 인수의 타입이 같은 함수, 인수는 2개 | BigInteger::add |
Predicate<T> | boolean test(T t) | 한 개의 인수를 받아서 boolean을 반환하는 함수 | 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 |
언제 표준 함수형 인터페이스를 사용해야 할까?
- 표준 함수형 인터페이스 대부분은 기본 타입만 지원한다. 그렇다고 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 않도록 한다.
- 계산량이 많을 때는 성능이 처참히 느려질 수 있다.
- 표준 인터페이스 중 필요한 용도에 맞는 게 없다면 직접 작성해야 하며, 구조적으로 똑같은 표준 함수형 인터페이스가 있더라도 직접 작성해야만 할 때가 있다.
인터페이스를 직접 작성해야 하는 경우
ex) Comparator<T>와 ToIntBiFunction<T, U> 비교
// Comparator @FunctionInterface public interface Comparator<T> { int compare(T o1, T o2); } // ToIntBiFunction @FunctionalInterface public interface ToIntBiFunction<T, U> { int applyAsInt(T t, U u); }
Comparator<T> 인터페이스의 경우, 구조적으로 ToIntBiFunction<T, U>와 동일하지만 독자적인 인터페이스로 존재해야 하는 이유가 몇 개 있다.
- API에서 굉장히 자주 사용되는데, 이름이 그 용도를 아주 훌륭히 설명해준다.
- 구현하는 쪽에서 반드시 지켜야 할 규약을 담고 있다.
- 비교자들을 변환하고 조합해주는 유용한 디폴트 메서드들을 많이 담고 있다.
Comparator의 특성을 정리하면 아래와 같다. 이 중 하나 이상을 만족한다면 전용 함수형 인터페이스를 구현해야 하는 건 아닌지 고민해보도록 해야 한다.
- 자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다.
- 반드시 따라야 하는 규약이 있다.
- 유용한 디폴트 메서드를 제공할 수 있다.
@FunctionalInterface
이 애너테이션을 사용하는 이뉴는 @Override를 사용하는 이유와 비슷하다.
프로그래머의 의도를 명시하는 것으로, 크게 세 가지 목적이 있다.
- 첫 번째. 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려준다.
- 두 번째. 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 해준다.
- 세 번째. 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다.
따라서, 직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface 애너테이션을 사용하도록 한다.
함수형 인터페이스를 사용할 때 주의점
서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중정의해서는 안 된다.
클라이언트에게 모호함을 주며 문제가 발생할 소지가 많다.
public interface ExecutorService extends Executor {
// Callable<T>와 Runnable을 각각 인수로 하여 다중정의했다.
// submit 메서드를 사용할 때마다 형변환이 필요해진다.
<T> Future<T> submit(Callback<T> task);
Future<?> submit(Runnable task);
}
핵심 정리
- 자바 8부터 람다를 지원하기 때문에, 입력값과 반환값에 함수형 인터페이스 타입을 활용하도록 한다.
- 보통은 java.util.function 패키지의 표준 함수형 인터페이스를 사용하는 것이 가장 좋은 선택이다.
- 흔치 않지만, 가끔은 직접 새로운 함수형 인터페이스를 만들어 쓰는 편이 나을 수도 있다.
참고:
https://madplay.github.io/post/favor-the-use-of-standard-functional-interfaces
'Effective Java' 카테고리의 다른 글
[Effective Java] 아이템 46 - 스트림에서는 부작용 없는 함수를 사용하라 (0) | 2022.03.28 |
---|---|
[Effective Java] 아이템 45 - 스트림은 주의해서 사용하라 (0) | 2022.03.25 |
[Effective Java] 아이템 43 - 람다보다는 메서드 참조를 사용하라 (0) | 2022.03.23 |
[Effective Java] 아이템 42 - 익명 클래스보다는 람다를 사용하라 (0) | 2022.03.23 |
[Effective Java] 아이템 41 - 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2022.03.23 |