Effective Java를 읽고 정리한 정리본입니다.

📌 Item 44 : 표준 함수형 인터페이스를 사용하라

자바가 람다를 지원하면서 API를 작성하는 모범 사례도 크게 바뀌었다.

과거 템플릿 메서드의 활용이 줄어들고, 이를 대체하는 정적 팩터리나 생성자 제공에 대한 활용이 크게 증가하였다.

이때 함수 객체를 매개변수로 받는 생성자와 메서드를 만들 때 함수형 매개변수 타입을 올바르게 선택해야 한다.

🫧 예시로 알아보는 올바른 함수형 매개변수 타입

LinkedHashMap을 생각해보자.

이 클래서의 protected 메서드인 removeEldestEntry를 재정의하면 캐시로 사용할 수 있다.

맵에 새로운 키를 추가하는 put 메서드는 이 메서드를 호출하여 true가 반환되면 맵에서 가장 오래된 원소를 제거한다.

예컨데 아래와 같이 재정의할 시 맵에 원소가 100개가 될 때까지 커지다가, 그 이상이 되면 새로운 키가 더해질 때마다 가장 오래된 원소를 하나씩 제거한다.

즉, 가장 최근 원소 100개를 유지하는 것이다.

protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
  return size() > 100;
}

removeEldstEntry 선언을 보면 이 함수 객체는 Map.Entry<K, V>를 받아 boolean을 반환해야 할 것 같지만, 꼭 그렇지는 않다.

removeEldestEntry는 size()를 호출해 맵 안의 원소 수를 알아내는데, 이는 removeEldestEntry가 인스턴스 메서드기에 가능한 방식이다.

하지만 생성자에 넘기는 함수 객체는 이 맵의 인스턴스 메서드가 아니다.

팩터리나 생성자를 호출할 때는 맵의 인스턴스가 존재하지 않기 때문이다.

따라서 맵은 자기 자신도 함수 객체에 건네줘야 한다.

이를 반영한 함수형 인터페이스는 다음과 같이 선언 가능하다.

@FunctionalInterface interface EldestEntryRemovalFunction<K, V> {
  boolean remove(Map<K, V> map, Map.Entry<K, V> eldest);
}

🫧 cf) 함수형 인터페이스

함수형 인터페이스(functional interface)는 추상메서드가 1개만 정의된 인터페이스를 통칭하여 일컫는다.

이 인터페이스 형태의 목적은 자바에서 람다 표현식(Lambda Expression)Visit Website을 이용해 함수형 프로그래밍을 구현하기 위해서이다.

🫧 표준 함수형 인터페이스

이러한 인터페이스도 잘 동작하기는 하지만 굳이 사용할 필요는 없다.

자바 표준 라이브러리에 이미 같은 모양의 인터페이스가 준비되어 있기 때문이다.

이는 java.util.function 패키지를 보면 다양한 용도의 표준 함수형 인터페이스가 담겨 있다.

필요한 용도에 맞는 게 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용하라.

그렇게 한다면 익히기 쉬울 뿐더러 유용한 디폴트 메서드를 많이 제공하므로 다른 코드와의 상호운용성도 크게 좋아질 것이다.

✨ Predicate 인터페이스

예컨데 Predicate 인터페이스는 predicate들을 조합하는 메서드를 제공한다.

앞의 LinkedHashMap 예에서는 직접 만든 EldestEntryRemovalFunction 대신 표준 인터페이스인 BiPredicate<Map<K, V>, Map.Entry<K, V»를 사용할 수 있는 것이다.

🫧 java.util.function

java.util.function 패키지에는 총 43개의 인터페이스가 담겨 있다.

기본 인터페이스 6개만 기억하면 나머지를 충분히 유추해낼 수 있다.

참고로, 이 기본 인터페이스들은 모두 참조 타입용이다.

✨ 1, 2. Operator 인터페이스 (UnaryOperator, BinaryOperator)

: 반환값과 인수의 타입이 같은 함수

실제로 연산을 할 때 쓰이며, 인수가 1개인 UnaryOperator와 2개인 BinaryOperator로 나뉜다.

✨ 3. Predicate 인터페이스

: 인수 하나를 받아 boolean을 반환하는 함수

✨ 4. Function 인터페이스

: 인수와 반환 타입이 다른 함수

✨ 5. Supplier 인터페이스

: 인수를 받지 않고 값을 반환 (혹은 제공)하는 함수

✨ 6. Consumer 인터페이스

: 인수를 하나 받고 반환값은 없는 (특히 인수를 소비하는) 함수

image

✨ 기본 타입의 변형

기본 인터페이스는 기본 타입인 int, long, double용으로 각 3개씩 변형이 생겨난다.

그 이름도 기본 인터페이스의 이름 앞에 해당 기본 타입 이름을 붙여 지었다.

예컨대 int를 받는 Predicate는 IntPredicate가 되고 long을 받아 long을 반환하는 BinaryOperator은 LongBinaryOperator이 되는 식이다.

이 변형들 중 유일하게 Function 변형만 매개변수화됐다.

정확히는 반환 타입만 매개변수화되었는데, 예를 들어 LongFunction<int[]>은 long 인수를 받아 int[]을 반환한다.

✨ Function 인터페이스

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

Function 인터페이스는 입력과 결과의 타입이 항상 다르다.

이때 입력과 결과 타입이 모두 기본 타입이면 접두어로 SrcToResult를 사용한다. (총 6개)

ex) LongToIntFunction : long을 받아 int를 반환

나머지는 입력이 객체 참조이고 결과가 int, long, double인 변형들로, 앞서와 달리 입력을 매개변수화하고 접두어로 ToResult를 사용한다.

ex) ToLongFunction<int[]> : int[]를 인수로 받아 long을 반환

✨ 기본 함수형 인터페이스

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

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

이 중에서도 BiFunction에는 다시 기본 타입을 반환하는 세 변형 ToIntBiFunction<T, U>, ToLongBiFUnction<T, U>, ToDoubleBiFunction<T, U> 가 존재한다.

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

이렇게 기본 인터페이스의 인수 2개짜리 변형은 9개가 존재한다.

✨ BooleanSupplier

BooleanSupplier 인터페이스는 boolean을 반환하도록 한 Supplier의 변형이다.

이것이 표준 함수형 인터페이스 중 boolean을 이름에 명시한 유일한 인터페이스지만, Predicate와 그 변형 4개도 boolean 값을 반환할 수 있다.

🫧 표준 함수형 인터페이스와 박싱된 기본 타입

이렇듯 표준 함수형 인터페이스는 총 43개가 존재한다.

표준 함수형 인터페이스 대부분은 기본 타입만 지원한다.

그렇다고 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용해서는 안 된다.

계산량이 많을 때는 성능이 현저하게 떨어지기 때문이다.

🫧 전용 함수형 인터페이스 구현

이제 대부분의 상황에서는 직접 작성하는 것보다 표준 함수형 인터페이스를 사용하는 편이 나았음을 알 것이다.

그렇다면 코드를 직접 작성해야 할 때는 언제인가?

아래와 같은 상황에서는 직접 작성이 필요하다.

  1. 표준 인터페이스 중 필요한 용도에 맞는 것이 없을 경우
  2. 구조적으로 똑같지만 완전히 다른 동작을 필요로 할 때

✨ Comparator

앞서 말했던 2번처럼, 구조적으로 똑같은 표준 함수형 인터페이스가 있더라도 직접 작성해야만 할 때가 있다.

그 대표적인 예가 바로 Comparator 인터페이스이다.

아래는 Comparator의 세 가지 특성이다.

이 중 하나 이상을 만족한다면 전용 함수형 인터페이스를 구현해야 하는 건 아닌지 진중히 고민해야 한다.

  1. 자주 쓰이며 이름 자체가 용도를 명확히 설명해준다.
  2. 반드시 따라야 하는 규약이 있다.
  3. 유용한 디폴트 메서드를 제공할 수 있다.

🫧 전용 함수형 인터페이스 작성

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

인터페이스 설계 시 다음과 같은 규약을 따라 설계할 필요가 있다.

✨ 1. @FunctionalInterface 애너테이션 사용

앞서 보았던 EldestEntryRemovalFunction 인터페이스에 @FunctionalInterface 애너테이션이 달려있던 것처럼 애너테이션을 달아줘야 한다.

  1. 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려준다.
  2. 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 해준다.
  3. 그 결과 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다.

✨ 2. 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중 정의해서는 안 된다.

클라이언트에게 불필요한 모호함만 안겨줄 뿐이며, 이 모호함으로 인해 실제 문제가 발생하기도 한다.

그래서 올바른 메서드를 알려주기 위해 형변환해야 할 가능성도 생긴다.

🫧 참고 자료

  • [함수형 인터페이스 표준 API 총정리 Tistory Inpa Dev](https://inpa.tistory.com/entry/%E2%98%95-%ED%95%A8%EC%88%98%ED%98%95-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-API)
  • [[Java] 함수형 인터페이스와 람다 Tistory 좋은 경험 훔쳐먹기](https://xxeol.tistory.com/20)