[교재 EffectiveJava] 아이템 44. 표준 함수형 인터페이스를 사용하라

반응형
728x90
반응형

함수형 매개변수 타입

자바가 람다를 지원하면서 템플릿 메서드 패턴의 매력이 크게 줄었다.

* 템플릿 메서드 패턴
상위 클래스의 기본 메서드를 재정의해 원하는 동작을 구현하는 디자인패턴

이를 대체하는 현대적인 해법은 같은 효과의 함수 객체를 받는 정적 팩터리나 생성자를 제공하는 것이다. 이때 함수형 매개변수 타입을 올바르게 선택해야한다.

 

LinkedHashMap 의 protect 메서드인 removeEldestEntry 를 재정의하면 캐시로 사용할 수 있다. 맵에 새로운 키를 추가하는 put 메서드는 이 메서드를 호출하여 true가 반환되면 맵에서 가장 오래된 원소를 제거한다.

 

LinkedHashMap의 removeEldestEntry()
...
    /**
     * <p>Sample use: this override will allow the map to grow up to 100
     * entries and then delete the eldest entry each time a new entry is
     * added, maintaining a steady state of 100 entries.
     * <pre>
     *     private static final int MAX_ENTRIES = 100;
     *
     *     protected boolean removeEldestEntry(Map.Entry eldest) {
     *        return size() &gt; MAX_ENTRIES;
     *     }
     * </pre>
     */
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }
...

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

 

put 메서드에서 호출되는 removeEldestEntry()
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return size() > 100;
}

위 코드를 람다를 사용하면 훨씬 낫다. removeEldestEntry 는 size()를 호출해 맵 안의 원소 수를 알아내는데, removeEldestEntry가 인스턴스 메서드라서 가능한 방식이다. 하지만 생성자에 넘기는 함수 객체는 이 맵의 인스턴스 메서드가 아니다. 팩터리나 생성자를 호출할 때는 맵의 인스턴스가 존재하지 않기 때문이다. 따라서 맵은 자기 자신도 함수 객체에 건네줘야 한다.

 

함수형 인터페이스를 아래와 같이 선언할 수 있다.

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

이미 자바 표준 라이브러리에 같은 모양의 인터페이스가 존재한다. java.util.function 패키지를 보면 다양한 용도의 표준 함수형 인터페이스가 존재한다. 필요한 용도에 맞는게 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용하자.

예를 들어, Predicate 인터페이스는 프레디키트들을 조합하는 메서드를 제공하고있다. 유용한 디폴트 메서드를 많이 제공하므로 다른 코드와의 상호 운용성도 좋아진다.

 

위 EldestEntryRemovalFunction 인터페이스와 동일한 기능을 하는 표준 인터페이스 BiPredicate
@FunctionalInterface
public interface BiPredicate<T, U> {

    boolean test(T t, U u);

    default BiPredicate<T, U> and(BiPredicate<? super T, ? super U> other) {
        Objects.requireNonNull(other);
        return (T t, U u) -> test(t, u) && other.test(t, u);
    }

    default BiPredicate<T, U> negate() {
        return (T t, U u) -> !test(t, u);
    }

    default BiPredicate<T, U> or(BiPredicate<? super T, ? super U> other) {
        Objects.requireNonNull(other);
        return (T t, U u) -> test(t, u) || other.test(t, u);
    }
}

 

 

기본 인터페이스 6개

java.util.function 패키지에는 총 43개 쯤의 인터페이스가 존재한다. 이 중, 기본 인터페이스 6개를 알아보자. 아래 기본 인터페이스 외에, int 를 받는 IntPredicate 등 int, long, double형 기본 인터페이스의 이름 앞에 해당 기본 타입 이름을 붙여 지어진 인터페이스 변형이 존재한다. 이 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지 말자. 

 

1) UnaryOperator 인터페이스

String::toLowerCase
@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
}
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {

    /**
     * Returns a unary operator that always returns its input argument.
     *
     * @param <T> the type of the input and output of the operator
     * @return a unary operator that always returns its input argument
     */
    static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}

 

2) BinaryOperator

BigInteger::add
@FunctionalInterface
public interface BiFunction<T, U, R> {
    /**
     * Applies this function to the given arguments.
     *
     * @param t the first function argument
     * @param u the second function argument
     * @return the function result
     */
    R apply(T t, U u);
    
    ...
}
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
	...
}

 

3) Predicate 인터페이스

Collection::isEmpty
@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
    
    ....
}

 

4) Function 인터페이스

Arrays.asList
@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
}

 

5) Supplier 인터페이스

Instant::now
@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

 

6) Consumer 인터페이스

System.out::println
@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

    ...
}

 

 

표준 함수형 인터페이스를 사용하지 않고 코드를 직접 작성해야할 경우

1) 표준 인터페이스 중 필요한 용도에 맞는게 없을 경우

예를 들어, 매개변수 3개를 받는 Predicate 라든가, 검사 예외를 던지는 경우

 

2) 구조적으로 똑같은 표준 함수형 인터페이스가 있더라도 직접 작성해야할 경우

Comparator<T> 인터페이스

구조적으로는 ToIntBiFunction<T, U>와 동일하다. 자바 라이브러리에 Comparator<T>를 추가할 당시 ToIntBiFunction<T, U>가 이미 존재했더라도 ToIntBiFunction<T, U> 를 사용하면 안됬다. Comparator 가 독자적인 인터페이스로 살아남아야하는 이유가 몇개 있다.

1) API 에서 굉장히 자주 사용되는데, 지금의 이름이 그 용도를 아주 잘 설명해준다.
2) 구현하는 쪽에서 반드시 지켜야할 규약을 담고있다.
3) 비교자들을 변환하고 조합해주는 유용한 디폴트 메서드가 많이 존재한다.

이러한 Comparator 특성을 정리하면 아래 가지인데, 이중 1개 이상을 만족한다면 전용 함수형 인터페이스를 구현해야할지 고민해봐야한다.

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

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

 

 

@FunctionalInteface

위 어노테이션은 함수형 인터페이스임을 뜻하는 어노테이션이다. 이를 사용하는 이유는 3가지가 있다.

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

직접 만든 함수형 인터페이스에는 @FunctionalInteface 어노테이션을 항상 사용하자.

 

 

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

서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중 정의해서는 안된다.클라이언트에게 불필요한 모호함만 안겨주고, 이 모호함으로 인해 문제가 발생할 수 있다. 

 

ExecutorService 의 submit()
public interface ExecutorService extends Executor {

    ...

    <T> Future<T> submit(Callable<T> task);

    <T> Future<T> submit(Runnable task, T result);

    Future<?> submit(Runnable task);

    ...
}

 

Callable<T>를 받는것과 Runnable 을 받는 것을 다중정의 했다. 그래서 올바른 메서드를 알려주기 위해 형변환해야할 때가 자주 발생한다. 이 문제를 피하기 위해서는, 서로 다른 함수형 인터페이스를 같은 위치의 인수로 사용하는 다중정의를 피하는 것이다.

 

 

 

반응형

Designed by JB FACTORY