본문 바로가기

Reading/Effective Java

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

LinkedHashMap으로 알아보기


자바가 람다를 지원하며 상위 클래스의 기본 메소드를 재정의해 원하는 동작을 구현하는 템플릿 메소드 패턴의 사용이 줄었다.

이를 대체하는 방법은 같은 효과의 함수 객체를 받는 정적 팩토리나 생성자를 제공하는 것이고, 함수 객체를 매개변수로 받는 생성자와 메소드를 더 많이 만들어야한다. 이때 함수형 매개변수 타입을 올바르게 선택해야한다.

 

LinkedHashMap의 protected 메소드인 removeEldestEntry를 재정의하면 캐시로 사용할 수 있다. 어떻게 가능할까?

 

LinkedHashMap은 HashMap을 상속한다. HashMap에서 put 메소드는 afterNodeInsertion 메소드를 호출한다.

 

 

HashMap의 afterNodeInsertion 메소드는 void로 아무 작업을 하지 않지만 LinkedHashMap에서는 이를 구현해서 사용한다.

 

 

HashMap의 put 메소드에서 매개변수 evict는 true로 넘기고, LinkedHashMap이 비어있지 않고 removeEldestEntry 메소드의

호출결과만 true라면 제일 앞에 있는 노드를 삭제하는 동작을한다. 기존의 removeEldestEntry는 다음과 같다.

 

 

항상 false만 반환하기때문에, 재정의해서 true인 조건만 갖춘다면 캐시로 사용할 수 있다.

 

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

 

위처럼 재정의하면 맵의 원소가 100개를 초과하면 새로운 키가 더해질때마다 가장 오래된 원소를 하나씩 제거한다.

 

 

만약

LinkedHashMap을 다시 구현한다면 함수 객체를 받는 정적 팩토리나 생성자를 제공했을 것이다.

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

 

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

팩토리나 생성자를 호출할 때는 맵의 인스턴스가 존재하지 않기 때문에 맵은 자기 자신도 함수 객체에 건네줘야한다.

 

@FunctionalInterface
public 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>>를 사용할 수 있기 때문이다.

 

    public void mayFunctional() {
        CashMap<Integer,Integer> cm = new CashMap<>();
        for(int i=1;i<=10;cm.put(i++,0));
        BiPredicate<CashMap<Integer,Integer>, Map.Entry<Integer,Integer>> delete = (c, v) -> c.size() > 5;
        // 실제론 없음: put을 할때 eldest의 조건을 같이 넘겨주거나
        // cm.put(1,2,delete);
        // 실제론 없음: 생성자를 만들때 함수 객체를 넣어주는 방법을 쓰거나?
        // CashMap<Integer,Integer> cm2 = new CashMap<>(delete);
    }

 

아마 이런 방식이 추가됐을때 put을 할 때 넘겨주기보단 생성자에서 넘겨줄 확률이 더 크니 쓰인다면 저렇게 쓸 것 같다!

 

 

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

 

 

 

자바의 표준 함수형 인터페이스


인터페이스 함수 시그니처 설명
UnaryOperator<T> T apply(T t) String::toLowerCase 반환값과 인수의 타입이 같은 함수 (인수 1개)
BinaryOperator<T> T apply(T t1, T t2) BigInteger::add 반환값과 인수의 타입이 같은 함수 (인수 2개)
Predicate<T> boolean test(T t) Collection::isEmpty 인수 하나를 받아 boolean을 반환하는 함수
Function<T,R> R apply(T t) Arrays::asList 인수와 반환 타입이 다른 함수
Supplier<T> T get() Instant::now 인수를 받지 않고 값을 반환(제공)하는 함수
Consumer<T> void accept(T t) System.out.println 인수를 하나 받고 반환값은 없는(인수 소비) 함수

위 표의 기본 인터페이스 6개 = 6

+저 기본 6개에 기본 타입인 int, long, double용으로 각 3개씩 변형. 저 6개 각각 3개씩 = 3*6 = 18

+Function에는 각각을 SrcToResult형태로 2개씩 변형.(LongToIntFunction,LongToDoubleFunction...) = 3*2 = 6

+Function에는 각각을 ToResult 형태로(ToLongFunction,...) 3개의 변형 = 3

+Predicate, Function, Consumer의 앞에 Bi가 붙는, 즉 인수 2개씩 받는 변형 = 3

+BiFunction에는 기본 타입을 반환하는 세 변형(ToIntFunction,...) = 3 

+Consumer에도 객체 참조와 기본 타입 하나, 즉 인수 2개 받는 변형(ObjDoubleConsumer,..) = 3

+BooleanSupplier = 1

 

즉 java.util.function 패키지에 총 43개의 인터페이스가 있다. 전부 기본 인터페이스 6개에서 확장된다.

 

 

표준 함수형 인터페이스 주의사항


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

--> 아이템 61

 

2. 표준 함수형 인터페이스 중 필요한 용도가 없다면 직접 작성해서 사용하자

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

 

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

이 중 하나 이상을 만족한다면 전용 함수형 인터페이스를 구현해야할지 고민하자.

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

Comparator<T> 인터페이스는 구조적으로는 ToIntBiFunction<T,U>와 동일하다. 

당시 ToIntBiFunction<t,u>가 있음에도 사용하지 않았던 이유는 위 3가지를 전부 충족하기 때문에 독자적인 인터페이스로 살아남았다.

 

전용 함수형 인터페이스를 작성하기로 했다면 자신이 작성하는게 인터페이스임을 명심하고 주의해서 설계하자.

 

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

 이 애노테이션을 사용하는 이유는 @Override 사용 이유와 같고 의도를 명시하는 것이다. 크게 세가지 목적이 있다.

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

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

 

Runnable을 받는 것과 Callable을 받는 것을 다중 정의했다.

 

위 코드와 같이, 클라이언트에게 불필요한 모호함을 안겨주고 올바른 메소드 사용을 위해서는 형변환을 해야한다.

이런 문제를 피하기 위해선 서로 다른 함수형 인터페이스를 같은 위치의 인수로 사용하는 다중정의를 피해야한다.

 

 

 

* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.