티스토리 뷰
자바가 람다를 지원하면서 바뀐 것
상위 클래스의 기본 메서드를 재정의해 원하는 동작을 구현하는 템플릿 메서드 패턴
→ 함수 객체를 받는 정적 팩터리나 생성자를 제공하는 것 (함수 객체를 매개변수로 받는 생성자와 메서드를 더 많이 만들어야함
ex) LinkedHashMap 의 removeEldestEntry 메서드 (맵의 가장 오래된 원소 제거)
//재정의함 -> 원소가 100개를 넘어가면 가장 오래된 원소를 제거
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > 100;
}
위 코드를 람다를 사용하여 함수형 인터페이스를 만들 경우에는 아래와 같다.
//불필요한 함수형 인터페이스 - 대신 표준 함수형 인터페이스를 사용하라
@FuntionalInterface
interface EldestEntryRemovalFunction<K, V> {
boolean remove(Map<K, V> map, Map.Entry<K, V> eldest);
}
위 인터페이스도 잘 작동하긴 하나, 자바 표준 라이브러리(java.util.function)에 비슷한 표준 함수형 인터페이스가 있으니 그걸 사용하자.
위에 직접 만든 EldestEntryRemovalFunction 메서드 대신 표준 인터페이스인 BiPredicate<Map<K, V>, Map.Entry<K, V>> 를 사용할 수 있다.
java.util.function 패키지의 기본 인터페이스 6개
이 패키지에는 총 43개의 인터페이스가 있지만, 기본 인터페이스 6개만 기억하면 나머지는 유추할 수 있다.
인터페이스 | 함수 시그니처 | 예 | 설명 |
UnaryOperator | T apply(T t) | String::toLowerCase | 반환값과 인수의 타입이 같은 함수(인수가 1개) |
BinaryOperator | T apply(T t1, T t2) | BigInteger::add | 반환값과 인수의 타입이 같은 함수(인수가 2개) |
Predicate | boolean test(T t) | Collection::isEmpty | 인수 하나를 받아 boolean을 반환하는 함수 |
Function<T, R> | R apply(T t) | Arrays::asList | 인수와 반환 타입이 다른 함수 |
Supplier | T get() | Instant::now | 인수를 받지 않고 값을 반환/제공하는 함수 |
Consumer | void accept(T t) | System.out::println | 인수를 하나 받고 반환값은 없는(특히 인수를 소비하는) 함수 |
기본 인터페이스는 기본 타입인 int,long,double 용으로 각 3개씩 변형이 있음(ex.IntPredicate)
표준 함수형 인터페이스는 대부분 기본 타입만 지원한다. 그렇다고 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 말아라!
기본 함수형 인터페이스의 변형
1) UnaryOperator
- 기본타입용 : DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator
2) BinaryOperator
- 기본타입용 : DoubleBinaryOperator, IntBinaryOperator, LongBinaryOperator
3) Predicate
- 기본타입용 : DoublePredicate, IntPredicate, LongPredicate
- 인수를 2개받고 boolean 반환 : BiPredicate<T, U>
4) Function<T, R>
- 유일하게 Function의 변형만 매개변수화됨 → ex.LongFunction<int[]>은 long 인수를 받아 int[]를 반환
- 입력은 기본타입, 출력은 R타입 : DoubleFunction, IntFunction, LongFunction
- 입력과 출력 모두 기본타입(..To..Function) : LongToIntFunction, DoubleToLongFunction 등등
- 출력이 기본타입 : ToDoubleFunction, ToIntFunction, ToLongFunction
- 인수를 2개받고 R타입 반환 : BiFunction<T,U,R>
- 인수를 2개받고 기본타입 반환 : ToDoubleBiFunction<T,U>, ToIntBiFunction<T,U>, ToLongBiFunction<T,U>
5) Supplier
- 기본타입용 : DoubleSupplier, IntSupplier, LongSupplier, BooleanSupplier
6) Consumer
- 기본타입용 : DoubleConsumer, IntConsumer, LongConsumer
- 인수 2개 받음 : BiConsumer<T, U>
- T타입, 기본타입 받음 : ObjDoubleConsumer, ObjIntConsumer, ObjLongConsumer
직접 함수형 인터페이스를 작성해야할 때
표준 함수형 인터페이스가 있더라도 용도에 맞는게 없다면 직접 작성해야할 때가 있다.
예를 들어, 자주 본 Comparator 인터페이스는 구조적으로는 ToIntBiFunction<T,U> 인터페이스와 같지만 직접 작성한 예이다.
다음 세가지 조건 중에 하나 이상을 만족한다면, 직접 작성해야하는 것이 아닌지 고민해보자.
- 자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다.
- ex) Comparator 이름이 ToIntBiFunction<T,U>보다 용도를 명확히 설명한다.
- 반드시 따라야 하는 규약이 있다.
- ex) compare() 는 따라야하는 규약이 많다.
- 유용한 디폴트 메서드를 제공할 수 있다.
- ex) Comparator 는 reversed(), thenComparing() 등등의 메서드를 제공한다
@FunctionalInterface
직접 작성할때 인터페이스 위에 @FunctionalInterface 애너테이션을 꼭 붙여주자.
이는 @Override를 붙여주는 것과 같은 이유로 다음과 같은 목적이 있다.
- 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려주기 위해
- 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일 될 수 있도록 하기 위해
- 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막기 위해
함수형 인터페이스를 API에서 사용할 때 주의사항
public interface ExecutorService extends Executor {
// 생략
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
// 생략
}
서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중 정의해서는 안된다. → 모호함
올바르메서드를 알려주기 위해 형변환 해야할때가 생긴다.
이런 상황은 피하자.
결론
자바도 이제 람다를 지원하기 때문에 API를 설계할 때 람다를 염두에 두고 설계하자.
입력값과 반환값에 함수형 인터페이스 타입을 활용하라.
대게 표준 함수형 인터페이스를 쓰도록 하고, 직접 새로운 함수형 인터페이스를 만들어 쓰는 편이 나을 때도 있다.
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] 46. 스트림에서는 부작용 없는 함수를 사용하라 (0) | 2022.04.01 |
---|---|
[Effective Java] 45. 스트림은 주의해서 사용하라 (0) | 2022.04.01 |
[Effective Java] 43.람다보다는 메서드 참조를 사용하라 (0) | 2022.04.01 |
[Effective Java] 42.익명 클래스보다는 람다를 사용하라 (0) | 2022.03.31 |
[Effective Java] 41.정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2022.03.31 |