Skip to content

Item 44. 표준 함수형 인터페이스를 사용하라 #47

@youngkimi

Description

@youngkimi

Chapter : 7. 람다와 스트림

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

Assignee : youngkimi


🍑 서론

함수형 인터페이스를 새로 구현하기 보다는 표준 함수형 인터페이스를 활용하라.

  • 자바가 람다를 지원하며 API를 작성하는 모범 사례도 바뀌었다.
  • 기존처럼 상위 클래스의 기본 메서드를 재정의하는 템플릿 메서드 패턴 보다는, 함수 객체를 받는 정적 팩터리생성자를 제공하는 것이 보다 현대적이다.
  • 일반화하여 말하면, 함수 객체를 매개변수로 받는 생성자와 메서드를 더 만들어야 한다.

🍑 본론

LinkedHashMap

  • 이 클래스의 protected 메서드인 removeEldestEntry재정의하면 캐시로 사용할 수 있다.
  • Map에 새로운 키를 추가하는 put() 메서드는 이 메서드를 호출하여 true가 반환되면 맵에서 가장 오래된 원소를 제거한다.
// 상위 클래스의 removeEldestEntry를 재정의.
// Entry Size가 100을 넘으면, true를 반환할 것이다.
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
	return size() > 100;
}
  • 이는 잘 동작하지만, 람다를 사용하면 더 좋을 것이다. LinkedHashMap을 오늘날 다시 구현한다면, 함수 객체를 받는 정적 팩터리나 생성자를 제공했을 것이다.

표준 함수형 인터페이스를 사용해라

  • removeEldestEntry 선언을 보면, 이 함수 객체는 Entry인 Map.Entry<K, V> eldest을 받아 boolean을 반환할 것 같은데, 꼭 그렇지는 않다.
  • 이 메서드는 size()를 호출해 맵 안의 원소 수를 알아내는데, removeEldestEntry인스턴스 메서드기에 가능한 방식이다.
  • 하지만 생성자에 넘기는 함수 객체는 이 맵의 인스턴스 메서드가 아니다. 팩터리나 생성자를 호출할 때는 맵의 인스턴스가 존재하지 않기 때문이다. 맵은 자기 자신도 함수 객체에 건네줘야 한다.
@FunctionalInterface interface EldestEntryRemovalFunction<K, V> {
	boolean remove(Map<K, V> map, Map.Entry<K, V> eldest);
}
  • 이 인터페이스도 잘 작동하지만, 자바 표준 라이브러리에 이미 같은 라이브러리가 준비되어 있다.
  • 표준 함수형 인터페이스를 사용하라. 유용한 디폴트 메서드를 제공하고, 다른 코드와의 상호 운용성도 좋아진다.
  • Predicate 인터페이스는 predicate를 조합하는 메서드를 제공한다.
  • 앞의 예에서 직접 만든 EldestEntryRemovalFunction 대신 표준 인터페이스인 BiPredicate<Map<K, V>, Map.Entry<K, V>>를 사용할 수 있다.

자주 사용하는 기본 인터페이스 6가지

  • java.util.functional에는 43개의 인터페이스가 담겨 있다.
  • 전부 기억하기는 어렵지만, 기본 인터페이스 6개만 기억하면 나머지는 유추 가능하다.

Operator(Unary, Binary)

  • 반환값과 인수의 타입이 같은 함수를 의미한다.
  • 인수가 한 개인 UnaryOperator, BinaryOperator로 나뉜다.

Predicate

  • 앞서 말한 것처럼, 인수 하나를 받아 boolean을 반환하는 함수를 말한다.

Function

  • 인수와 반환 타입이 다른 함수를 뜻한다.

Supplier

  • 인수는 받지 않고, 값을 반환하는 함수를 뜻한다.

Consumer

  • 인수는 받고, 반환값은 없는(특히 인수를 소비하는) 함수를 뜻한다.

기본 인터페이스 변형

다 외우기에는 수도 많고 규칙성도 부족하다.

  • 기본 인터페이스는 Primitive Type인 int, long, double용으로 각 3개씩 변형이 생긴다.

  • 기본 인터페이스 앖에 해당 기본 타입

  • Function 인터페이스의 변형은 입력과 반환값의 타입이 항상 다르므로 기본 타입을 반환하는 변형이 총 9개 (3*3) 있다. (기본 변형의 접두사는 입력타입을 나타냄)

    • 6개 : 인수와 같은 타입을 반환하는 함수는 UnaryOperator이므로, Function 인터페이스의 변형은 입력, 결과 타입이 항상 다르다. SrcToRes 접두사가 붙은 Function 인터페이스는 6가지가 있다. LongToIntFunction
    • 3개 : 입력이 객체 참조이고 결과가 int, long, double인 변형들. 앞서와는 달리 입력을 매개변수화하고 접두어로 ToResult를 사용한다. ToLongFunction<int[]>
  • 인수를 두 개씩 받는 인터페이스 변형 : BiPredicate<T, U>, BiFunction<T, U, R>, BiConsumer<T, U>

  • 기본 타입(int, long, double)으로 반환하는 BiFunction의 변형 : ToIntBiFunction<T, U>

  • 객체 참조와 기본 타입(int, long, double) 하나 받는 변형 : ObjIntConsumer<T>

  • boolean을 반환하는 Supplier : BooleanSupplier

  • 표준 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 말자. 동작은 하지만, 계산량이 많으면 성능이 느려질 수 있다. (item 61)

함수형 인터페이스를 직접 구현해야 하는 경우

아래 조건 중 하나 이상을 만족한다면 고민해보아야 한다.

  1. 자주 사용하며, 이름이 그 용도를 훌륭히 설명해주는 경우
  2. 구현하는 쪽에서 반드시 지켜야할 규약을 담고 있는 경우
  3. 유용한 디폴트 메서드를 가지고 있는 경우

Comparator<T>를 떠올려보자.

  • 구조적으로는 ToIntBiPredicate<T, U>와 동일하다.
  • Comparator<T>를 자바 라이브러리에 추가할 당시 ToIntBiPredicate<T, U>가 있었음에도 사용하지 않았다. (않아야 했다.)

직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface 를 사용하라

@Override를 사용하는 이유와 비슷하다.

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

함수형 인터페이스를 API에서 사용할 때의 주의점

  • 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중 정의하지 말라.
    • 클라이언트에게 모호함을 안겨준다. 모호함으로 실제로 문제가 발생하기도 한다.
    • ExecutorServicesubmit() 메서드는 Callable<T>, Runnable<T> 를 받는 경우를 다중 정의한다.
    • 이로 인해 올바른 메서드를 알려주기 위해 형변환 해야 하는 경우가 생긴다 (item 52)

🍑 결론

자바가 람다를 지원함을 생각하고 API를 설계하라.

  • 입력값과 반환값에 함수형 인터페이스 타입을 활용하라.
  • 일반적으로는 표준 함수형 인터페이스를 사용하라.
  • 간혹 가다 직접 사용해야 하는 경우가 있을 수 있다. 위 조건을 참고하라.

Referenced by

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions