본문 바로가기
Effective Java

[Effective Java] 아이템44 표준 함수형 인터페이스를 사용하라

by byeongoo 2021. 6. 27.

■ 표준 함수형 인터페이스

자바가 람다를 지원하면서 API를 작성하는 모범 사례도 크게 바뀌었다. 상위 클래스의 기본 메서드를 재정의해 구현하던 템플릿 메서드 패턴의 매력이 크게 줄어쏙, 이를 대체하는 현대적 해법은 함수 객체를 받는 정적 팩터리나 생성자를 제공하는 것이다. 이 내용을 일반화하면 함수 객체를 받는 생성자와 메서드를 더 많이 만들어야한다.

 

자바 표준 라이브러리에는 이미 정의해둔 표준 함수형 인터페이스가 담겨있다. 필요한 용도에 맞는게 있다면, 직접구현하지 말고 표준 함수형 인터페이스를 활용하라

 

java.util.function 패키지에는 총 43개의 인터페이스가 담겨있다. 기본 인터페이스 6개만 기억하고 나머지를 충분히 유추해서 사용할 수 있다.

 

  1. UnaryOperator
    • 반환값과 인수의 타입이 같은 함수.
    • Unary는 인수값이 하나.
  2. BinaryOperator
    • 반환값과 인수의 타입이 같고 인수값이 두개이다.
  3. Predicate
    • 인수값 하나를 받아 boolean을 리턴한다.
  4. Function
    • 인수값과 반환타입이 다른 함수
  5. Supplier
    • 인수를 받지 않고 값을 반환한다.
  6. Consumer
    • 들어온 인수값을 소비하고 아무것도 반환하지 않는다.

기본 인터페이스에 람다식을 정의해본 코드이다.

UnaryOperator<Integer> unaryOperator = x->x+1;

BinaryOperator<Integer> binaryOperator = (x,y)->x+y;

Predicate<Integer> predicate = x-> x==1;

Function<Integer,String> function = (x) -> String.valueOf(x);

Supplier<Integer> supplier = ()->1;

Consumer<Integer> consumer = x ->{};   

 

■ Operator

기본 인터페이스는 기본 타입인 int, long, double용으로 각 3개씩 변형이 생겨난다. 그 이름도 기본 인터페이스의 이름 앞에 해당 깁ㄴ 타입 이름을 붙여 지었다.

Int Predicate p = i -> ( i > 3);
LongBinaryOperator bo = Long::sum;

 

■ SrcToResult, ToResult

Function 인터페이스는 기본 타입을 반환하는 변형이 총 9개가 더 있다. 

 

입력 결과 타입이 기본 타입이면 접두어로 SrcToResult를 사용한다. 예를 들어 long을 받아 int로 반환하면 LongToIntFunction이 된다.  (총 6개)

 

나머지는 입력이 객체 참조이고 결과가 int, long, doube인 변형들이다. 예를 들어 int[] 인수를 받아 long을 반환하면 ToLongFunction<int[]> 이다. (총 3개)

 

■ 기본 함수형 인터페이스 변형

기본 함수형 인터페이스 중 3개는 인수를 2개씩 받는 변형이 있다. 

  • BiPredicate<T, U> 
  • BiFunction<T, U, R>
  • BiConsumer<T, U>

BiFunction에는 다시 기본 타입을 반환하는 세 변형  ( ex : ToIntBiFuction<T, U> ...) 이 존재한다.

 

Consumer에도 객체 참조와 기본 타입 하나, 즉 인수를 2개 받는 변형인 ObjDoubleConsumer<T>, ObjIntConsumer<T>, ObjLongConsumer<T>가 존재한다. 

 

이렇게 해서 기본 인터페이스의 인수 2개짜리 변형은 총 9개이다.

 

 

표준 함수형 인터페이스는 대부분 기본 타입만 지원한다. 그렇다고 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 말자. 동작은 하지만 계산량이 많을 때는 성능이 현저히 느려진다.

 

■ Comparator

Comparator<T> 인터페이스는 구조적으로 ToIntBiFunction<T, U>와 동일하다. Comparator가 독자적인 인터페이스로 살아남은 이유 3가지가 있다.

 

1. 자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다.

2. 반드시 따라야 하는 규약이 있다.

3. 유용한 디폴트 메서드를 제공할 수 있다.

 

전용 함수형 인터페이스를 작성하기로 했다면, 자신이 작성하는게 다른 것도아닌 '인터페이스'임을 명심해야한다. 아주 유의깊게 설계해야한다는 뜻이다.

 

■ FunctionalInterface

이 애노테이션을 사용하는 이유는 다음과 같다.

 

1. 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것을 알려준다

2. 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 해준다.

3. 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다.

 

따라서 직접 만든 함수형 인터페이스는 항상 @FunctionalInterface 애노테이션을 사용하라.

 

■ 주의 사항

서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중 정위해서는 안된다. 클라이언트에게 불필요한 모호함을 안겨준다. 

Executor의 submit 메서드는 Callable<T>를 받는 것과 Runnable을 받는 것을 다중정의했다. 그래서 올바른 메서드를 알려주기 위해 형변환해야 할 때가 종종 생긴다.

 

■ 핵심정리

  • API를 설계할 때 람다도 염두에 두어야 한다.
  • 입력값과 반환값에 함수형 인터페이스 타입을 활용하라. 
  • 보통은 java.util.function 패키지의 표준 함수형 인터페이스를 사용하는 것이 가장 좋은 선택이다..
  • 흔치는 않지만 직접 새로운 함수형 인터페이스를 만들어 쓰는 편이 나을 수도 있음을 잊지 말자.