본문 바로가기
Java/Effective Java 3E

[이펙티브자바 3판] ITEM44. 표준 함수형 인터페이스를 사용하라

by 잭피 2020. 12. 2.

이번장의 핵심은...

입력값과 반환값에 함수형 인터페이스 타입을 활용하라

보통은 java.util.function 패키지의 표준 함수형 인터페이스를 사용하는 것이 가장 좋은 선택이다

단, 흔지는 않지만 직접 새로운 함수형 인터페이스를 만들어 쓰는 편이 나을 수 있음을 잊지 말자


함수형 매개변수 타입을 올바르게 선택하자

템플릿 메서드 패턴의 매력은 크게 줄었습니다

이를 대체하는 요즘 스타일은 같은 효과의 함수 객체를 받는 정적 팩터리나 생성자를 제공하는 것입니다

그러니까 함수 객체를 매개변수로 받는 생성자와 메서드를 더 많이 만들어야 합니다

 

ex) LinkedHashMap

// 맵에 원소가 100개가 될 때까지 커지다가, 그 이상이 되면 맵에서 가장 오래된 원소를 제거
// 즉, 가장 최근 원소 100개 유지  
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
	return size() > 100;
}

LinkedHashMap을 오늘날 다시 구현한다면 함수 객체를 받는 정적 팩터리나 생성자를 제공했을 것입니다

removeEldestEntry가 인스턴스 메서드라서 size()를 호출해 맵 안의 원소 수를 알 수 있습니다

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

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

 

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

// 불필요한 함수형 인터페이스 - 대신 표준 함수형 인터페이스를 사용하라
@FunctionalInterface 
interface EldestEntryRemovalFunction<K,V> {
	boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}

하지만 굳이 사용할 이유는 없습니다

자바 표준 라이브러리에 이미 같은 모양의 인터페이스가 준비되어 있습니다 (java.util.function)

 

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

 

따라서 위 코드 대신 표준 인터페이스인 BiPredicate<Map<K,V>, Map<Entry<K,V>>를 사용할 수 있습니다

BiPredicate<T,U> : 서로 다른 타입의 2개의 인자를 받아 boolean 타입으로 반환합니다

 

java.util.function

총 43개 인터페이스가 담겨있는데,

기본 인터페이스 6개만 기억하면 나머진 충분히 유추할 수 있습니다 (나머지는 추론가능)

필요한 용도에 맞다면 직접 구현하기보단 표준 함수형 인터페이스를 사용하면 됩니다

관리할 대상도 줄어들고 많은 유용한 디폴트 메서드가 부담을 줄여줍니다 

 

UnaryOperator<T> 

T apply(T t) : 반환값과 인수의 타입이 같은 함수, 인수는 1개 

ex) String::toLowerCase

 

BinaryOperator<T> 

T apply(T t1, T t2) : 반환값과 인수의 타입이 같은 함수, 인수는 2개 

ex) BigInteger::add

 

Predicate<T> 

boolean test(T t) : 한 개의 인수를 받아서 boolean을 반환하는 함수 

ex) Collection::isEmpty

 

Function<T,R> 

R apply(T t) : 인수와 반환 타입이 다른 함수 

ex) Arrays::asList

 

Supplier<T> 

T get() : 인수를 받지 않고 값을 반환, 제공하는 함수 

ex) Instant::now

 

Consumer<T> 

void accept(T t) : 한 개의 인수를 받고 반환값이 없는 함수 

ex) System.out::println

 

표준 함수형 인터페이스는 대부분 기본타입만 지원합니다

기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 말자

동작은 하겠지만 계산이 많아지는 경우 성능이 매우 느려질 수 있음

 

코드를 직접 작성해야 할 때는 언제?

Ex) Comparator<T>

 

// Comparator
@FunctionInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

// ToIntBiFunction
@FunctionalInterface
public interface ToIntBiFunction<T, U> {
    int applyAsInt(T t, U u);
}

두 개의 함수형 인터페이스는 구조적으로 동일합니다

인자 2개를 받아서(Bi), 정수형을 반환하는(ToInt), 인수와 반환 타입이 다른 함수(Function)

 

Comparator

1. API에서 굉장히 자주 쓰이며, 이름 자체가 용도를 명확히 설명해줍니다

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

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

 

따라서 이처럼 3가지 중 하나 이상의 이유가 있다면 직접 함수형 인터페이스를 구현할지 고민해도 좋습니다

 

@FunctionalInterface

@FunctionalInterface 어노태이션이 달린 인터페이스는 람다용으로 설계합니다

따라서 누군가 실수로 메서드를 추가하지 않도록, 직접 만든 함수형 인터페이스에는 항상 어노테이션을 사용해야합니다

 

함수형 인터페이스 주의점

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

클라이언트에게 모호함을 주며 문제가 발생할 소지가 많습니다

예를 들어 아래코드처럼 작성하면, submit을 사용할 때마다 형변환이 필요합니다

public interface ExecutorService extends Executor {
    // Callable<T>와 Runnable을 각각 인수로 하여 다중정의했다.
    // submit 메서드를 사용할 때마다 형변환이 필요해진다.
    <T> Future<T> submit(Callback<T> task);
    Future<?> submit(Runnable task);
}

 

 

이 글은 “이펙티브 자바 3판” 책 내용을 정리한 글입니다.

만약 저작권 관련 문제가 있다면 “shk3029@kakao.com”로 메일을 보내주시면, 바로 삭제하도록 하겠습니다.

 

댓글