반응형
이 포스팅에서 작성하는 내용은 EFFECTIVE JAVA(이펙티브자바) 에서 발췌하였습니다.
아이템 44. 표준 함수형 인터페이스를 사용하라
자바가 람다를 지원하면서 API를 작성하는 모범 사례가 바뀌었다.
상위 클래스의 기본 메서드를 재정의해 원하는 동작을 구현하는 템플릿 메서드 패턴의 매력이 크게 줄었다. 이를 대체하는 현대적인 해법은 같은 효과의 함수 객체를 받는 정적 팩터리나 생성자를 제공하는 것이다. 이 내용을 일반화해서 말하면 함수 객체를 매개 변수로 받는 생성자와 메서드를 더 많이 만들어야 하는데, 이 때 함수형 매개변수 타입을 올바르게 선택해야 한다.
ex 1) LinkedHashMap의 removeEldestEntry
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > 100;
}
- 잘 동작하지만, 람다를 사용하는 것이 좋다.
- LinkedHashMap은 오늘날 다시 구현된다면 함수 객체를 받는 정적 팩터리나 생성자를 제공했을 것이다.
- 이 함수 객체는 Map.Entry<K,V>를 받아 boolean을 반환해야할 것 같지만, 꼭 그렇지 않다.
- size()를 호출해 맵 안의 원소 수를 알아내는데, removeEldestEntry가 인스턴스 메서드라 가능하다.
- 하지만, 생성자에 넘기는 함수 객체는 이 맵의 인스턴스 메서드가 아니다. (팩터리나 생성자를 호출할 때는 맵의 인스턴스가 존재하지 않기 때문에, 맵은 자기 자신도 함수 객체에 건네줘야 함)
ex 2) 위의 내용을 토대로 작성한 함수형 인터페이스
@FunctionalInterface
public interface EldestEntryRemovalFunction<K, V> {
boolean remove(Map<K, V> map, Map.Entry<K, V> eldest);
}
- 이 인터페이스는 정상 동작하지만, 굳이 사용할 필요가 없다.
- 자바 표준 라이브러리에 이미 같은 모양의 인터페이스가 있기 때문이다.
- java.util.function 패키지에 다양한 용도의 표준 함수형 인터페이스가 담겨있으니, 필요한 용도에 맞는 게 있다면 직접 구현하지 말고 이것을 활용하자.
표준 함수형 인터페이스
- java.util.function 패키지에 43개의 인터페이스가 있으며, 아래 6개의 기본 인터페이스만 기억하면 나머지를 유추할 수 있다.
- 각각의 기본 인터페이스들은 모두 참조 타입용이며, 기본 타입인 int, long, double용으로 각 3개씩 변형이 생겨난다.
인터페이스 함수 시그니처 예
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 apply(T t) | Arrays::asList |
Supplier<T> | T get() | Instant::now |
Consumer<T> | void accept(T t) | System.out::println |
- Operator 인터페이스는 인수가 1개인 UnaryOperator와 2개인 BinaryOperator로 나뉘며, 반환값과 인수의 타입이 같은 함수를 뜻한다.
- Predicate 인터페이스는 인수 하나를 받아 boolean을 반환하는 함수를 뜻한다.
- Function 인터페이스는 인수와 반환 타입이 다른 함수를 뜻한다.
- Supplier 인터페이스는 인수를 받지 않고 값을 반환(혹은 제공)하는 함수를 뜻한다.
- Consumer 인터페이스는 인수를 하나 받고 반환값은 없는(특히 인수를 소비하는 함수)를 뜻한다.
유의점
- 표준 함수형 인터페이스 대부분은 기본 타입만 지원한다. 그렇다고 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 말자.(성능이 느려질 수 있다)
- 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중 정의해서는 안 된다. (클라이언트에게 불필요한 모호함만 줄 뿐이며, 이 때문에 실제로 문제가 일어나기도 한다.)
Comparator<T> vs ToIntBiFunction<T,U>
- 두 인터페이스는 구조적으로 동일하다.
- Comparator가 독자적인 인터페이스로 살아남은 이유는 아래와 같다.
- API에서 굉장히 자주 사용되는데, 이름이 그 용도를 아주 훌륭히 설명해준다.
- 구현하는 쪽에서 반드시 지켜야 할 규약을 담고 있다.
- 비교자들을 변환하고 조합해주는 유용한 디폴트 메서드들을 듬뿍 담고 있다.
직접 함수형 인터페이스를 작성해야하는 경우
- 표준 인터페이스 중 필요한 용도에 맞는 게 없는 경우
- 매개변수가 더 많이 필요한 경우
- 추가로, 아래의 경우 중 하나가 포함될 때
- 자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다.
- 반드시 따라야 하는 규약이 있다.
- 유용한 디폴트 메서드를 제공할 수 있다.
@FunctionalInterface
- 이 에너테이션의 목적은 @Override와 비슷하게 프로그래머에게 의도를 명시한다.
- 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려준다.
- 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 해준다.
- 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다.
- 이와 같은 이유로 직접 만든 함수형 인터페이스에는 꼭 @FunctionalInteface 애너테이션을 사용하자
정리
- API를 설계할 떄 람다도 염두에 두고, 입력값과 반환값에 함수형 인터페이스 타입을 활용하자.
- 보통 java.util.function 패키지의 표준 함수형 인터페이스를 사용하는 것이 좋다.
- 흔치는 않지만 직접 새로운 함수형 인터페이스를 만들어서 사용하는 것이 좋을 때도 있다.
반응형
'BE > Java' 카테고리의 다른 글
[Effective Java] 아이템 46. 스트림에서는 부작용 없는 함수를 사용하라 (0) | 2022.11.07 |
---|---|
[Effective Java] 아이템 45. 스트림은 주의해서 사용하라 (1) | 2022.11.04 |
[Effective Java] 아이템 43. 람다보다는 메서드 참조를 사용하라 (0) | 2022.11.02 |
[Effective Java] 아이템 42. 익명 클래스보다는 람다를 사용하라 (0) | 2022.11.01 |
[Effective Java] 아이템 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2022.10.31 |