티스토리 뷰

두괄식으로 이 글의 요약을 처음에 작성하겠다.

 

  • 까불지 말고 자바에서 기본으로 제공하는 Funtional Interface를 사용해라
  • 사용자 정의 Funtional Interface를 정의해야 할 때도 있다. 그 규칙을 만족하는 경우에만 사용하는 것이 바람직하다.(그런 경우는 아래에서 다루겠다.)

 

 

과거에는 객체의 커스텀 함수를 정의하기 위해선 원본 객체를 상속받는 객체를 정의하고, 수정하고 싶은 메소드를 Overriding(재정의)해서 사용했다. 이를 템플릿 메소드 패턴이라고 한다.

 

그러나 람다가 도입되면서 클라이언트에서 커스텀 함수를 정의하는 방식에 Funtional Interface를 도입하는 것이 현대적인 방법이 됐다.

 

 

예시를 들어보자. LinkedHashMap의 removeEldestEntry 메소드이다.

LinkedHashMap의 removeEldestEntry를 사용하는 함수

removeEldestEntry를 사용하는 함수가 여기 한 군데밖에 없다. 엔트리를 삽입한 후 오래된 엔트리를 지울 것인가를 결정한다.

 

기본 removeEldestEntry는 return false로 오래 된 엔트리를 지우지 않는다. 그러나 이 함수를 아래와 같이 재정의하면,

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

 

 

엔트리의 개수를 최대 100개 유지하도록 할 수 있다.

 

 

이를 람다식으로 변경한다고 하면 아래와 같은 Funtional Interface를 정의해서 사용할 수 있다.

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

 

파라미터로 Map과 Entry를 모두 받았는데 이는 static 메소드나 생성자에서도 범용적으로 사용하기 위해서다.

 

removeEldestEntry는 인스턴스 메소드이기 때문에 size()를 호출해 Map의 크기를 알 수 있었다. 그러나 Funtional Interface의 구현체는 Map과 아무 연관이 없는 객체이다. 따라서 map의 상태를 가져와 연산을 하기 위해 파라미터를 위와 같이 정의했다.

 

이렇게 Funtional Interface를 직접 구현해서 사용할 수도 있지만 자바에서 기본적으로 제공하는 Funtional Interface인 BiPredicate를 통해 이를 완전히 대체할 수 있다. BiPredicate<Map<K,V>, Map.Entry<K,V>>를 통해 인자 두 개를 가지고 boolean 값을 반환하는 함수를 정의할 수 있다.

 

이런 식으로 자바의 기본 Funtional Interface에 용도에 맞는 것이 있으면 제공되는 것을 사용하는 편이 좋다. 그러면 알아야 할 것도 적어지고 인터페이스 내부에 유용한  default 메소드들도 활용할 수 있게 된다.

 

 

 

아니 근데 이걸 언제 외우고 있어요?;;

화가 나는 부분이 있다. 이 기본제공 인터페이스가 무려 43개나 된다. 이걸 언제 찾아보고 외우고 한단 말인가.

자바는 이를 편하게 사용하라고 용도에 따라 사용되는 이름 6개에 접두사를 붙여 표현했다.

 

용도의 근간이 되는 6개의 인터페이스는 다음과 같다.

 

  • UnaryOperator<T>
    • 시그니처 - T apply(T t)
    • ex) String::toLowerCase
    • T를 받아서 어떤 작업을 한 뒤에 같은 타입 객체를 반환
  • BinaryOperator<T>
    • 시그니처 - T apply(T t1, T t2)
    • ex) BigInteger::add
    • T 두 개를 받아서 어떤 작업을 한 뒤에 같은 타입 객체를 반환
  • Predicate<T>
    • 시그니처 - boolean test(T t)
    • ex) Collection::isEmpty
    • T를 받아서 boolean 타입을 반환
  • Function<T, R>
    • 시그니처 - R apply(T t)
    • ex) Arrays::asList
    • T를 받아서 무슨 짓을 한 뒤 R을 반환
  • Supplier<T>
    • 시그니처 - T get()
    • ex) Instant::now
    • T를 반환
  • Consumer<T>
    • 시그니처 - void accept(T t)
    • ex) System.out::println
    • T를 받아서 그냥 뭔가 함

 

 

위의 여섯 가지 기본 인터페이스의 이름에 접두사를 붙여 여러 변형 인터페이스를 정의해 뒀다.

 

1. 원시 타입 처리

자바의 고질적인 문제인 원시 타입 처리를 위해 int, long, double용 3가지 변형 인터페이스가 만들어진다.

ex) long을 받아서 boolean 값을 반환하고 싶다 -> LongPredicate

 

이 중 Funtion의 경우에만 반환타입에 제네릭을 사용한다.

ex) long을 받아서 int[]를 반환하고 싶다 -> LongFuntion<int[]>

 

Funtion은 기본 타입을 받아 기본 타입을 반환하는 인터페이스 6가지 (3 x 2)가 정의돼 있다.

같은 타입을 받아 같은 타입을 반환하는(int를 받아 int를 반환) 역할은 UnaryOperator가 담당한다. 따라서 Funtion은 타입을 받고 다른 타입을 반환하는 SrcToResultFuntion이 정의돼 있다.

ex) LongToIntFuntion -> long 받아서 int 반환

 

레퍼런스 타입(제네릭)을 받아 원시 타입을 반환하는 경우에는 ToResultFuntion으로 사용된다.

ex) ToLongFuntion<int[]> -> int[] 받아서 long 반환

 

2. 인자로 여러 개를 받는 경우

Predicate, Funtion, Consumer의 경우는 인자를 2개씩 받는 변형이 있다. 그런 변형에는 접두사에 Bi가 붙는다.

ex) BiPredicate<T, U> / BiFuntion<T, U, R> / BiConsumer<T, U>

 

BiFuntion은 다시 기본 타입을 반환하는 규칙이 적용된 ToResultBiFuntion이 존재한다.

ex) ToLongBiFuntion<T, U>

 

BiConsumer는 객체 하나, 기본 타입 하나를 받는 변형이 존재한다.

ex) ObjDoubleConsumer<T>

 

3. Supplier 변형

마지막으로 BooleanSupplier는 boolean을 반환하는 Supplier의 변형이다.

 

 

이런 식으로 접두사를 붙이는 변형 조합을 통해 기본적으로 43개의 Funtional Interface를 제공한다. 위의 변형 규칙을 생각해 보며 목적에 맞는 Funtional Interface를 찾아보면 좋을 것 같다.

 

 

 

 

나만의 Funtional Interface를 만들어야 하는 경우

물론 위의 43가지의 경우 중에 목적에 맞는 것이 없으면 직접 작성해야 한다.

ex) 인자를 세 개 받아야 됨, Exception을 던져야 됨 등등

 

그러나 목적에 맞는 인터페이스가 존재해도 따로 만들어야 하는 경우가 있다.

그 예시가 Comparator<T>이다. 이 친구의 경우 ToIntBiFuntion<T, U>를 사용하면 완전하게 대체할 수 있다. 그러나 Compartor를 새로 정의한 이유는 아래 세 가지와 같다.

  • 엄청 자주 쓰인다. 그리고 이름이 명시적이다.
    • Comparator라는 이름을 통해 무언가 비교해 주는 친구임을 알 수 있다. 반면에 ToIntBiFuntion? 명시적이지 않다.
  • 반드시 따라야 하는 규약이 있다.

Comparator는 비교를 위해 탄생한 인터페이스이다. 따라서 클라이언트 측에서 올바른 비교를 하기 위해선 지켜야 할 규약들이 정의돼 있다. 

  • 유용한 디폴트 메소드를 제공한다.
    • 비교를 위한 유용한 디폴트 메소드들을 다양하게 제공한다.

 

Comparator를 재정의한 이유들을 보면서 만약 내가 사용하고 싶은 Funtional Interface가 위의 세 가지 중 하나를 만족한다면 새로 정의할까? 고민해 볼 수 있겠다.

 

 

 

@FuntionalInterface 어노테이션

새로 정의한 Funtional Interface에는 @FuntionalInterface 어노테이션을 꼭 붙여야 한다.

지난 글에서 다뤘지만 그 이유는 아래의 세 가지와 같다.

  • 람다용 인터페이스임을 명시적으로 알 수 있게 한다.
  • 추상 메소드가 하나인지 컴파일 타임에 알 수 있다.
  • 누군가 유지보수할 때 메소드 추가하는 것을 막을 수 있다.

 

 

사용 시 주의점

Funtional Interface를 사용하는 메소드를 Overloading(다중 정의)하면 안 된다. 뭐 해도 되는데 클라이언트 입장에서 이를 해석하기 정말 힘들어진다.

 

예시로 gimchi()라는 메소드의 파라미터로 gimchi(ToIntBiFuntion<T, U> tbf)를 만들고 gimchi(ToLongFuntion<T> tlf)와 같이 다중 정의 했다고 치자. 클라이언트의 입장에서 gimchi 메소드에 어떤 람다식을 채택할지 알기 쉽지 않다. 따라서 메소드 이름을 다르게 한다든지 해서 명시적으로 사용할 수 있게 하는 편이 좋겠다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함