예전 자바에는 함수 타입을 표현할 때 추상 메서드를 하나만 담은 인터페이스를 사용했다.
이런 인터페이스의 인스턴스를 function object
라 불렀지만 JDK 1.1 이 등장하면서 익명 클래스가 되었다.
Collections.sort(word, new Comparator<String>() ) {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.lenght());
}
}
// 익명 클래스의 인스턴스를 함수 객체로 사용 - 낡은 기법이다!
낡은 기법이라니...! 학교에서는 아직도 이렇게 배우고 있다. 낡은 기법이라니 충격!!
하지만 익명 클래스 방식은 코드가 너무 길기 때문에 자바는 함수형 프로그래밍이 적합하지 않았다.
-> 자바 8에 와서 추상 메서드 하나짜리 인터페이스는 특별한 대우를 받게 되었다.
-> 람다 (람돠)
Collections.sort(word, (s1, s2) -> Integer.compare(s1.lenght(), s2.lenght()));
타입을 명시해야 코드가 더 명확할 때만 제외하고는, 람다의 모든 매개변수 타입은 생략하자.
// 대신 변수명을 잘 짓자...
람다 자리에 비교자 생성 메서드를 사용하면 더 간결해진다.
Collections.sort(words, comparingInt(String::lenght));
더 나아가 자바 8의 List 인터페이스에 추가된 sort 메서드를 이용하면 더 짧아진다.
words.sort(comparingInt(String::lenght));
// 코틀린으로 자바 코드 줄이는 수준이군
public enum Operation {
PLUS ("+", (x,y) -> x + y),
MUNUS ("-", (x,y) -> x -y);
private final String symbol;
private final DoubleBinaryOperator op;
Operation(String symbol, DoubleBinaryOperator op) {
this.symbol = symbol;
this.op = op;
}
public double apply(double x, double y){
return op.applyAsDouble(x,y);
}
}
이런 식으로 구현하면 상수별로 다르게 동작하는 코드를 쉽게 구현할 수 있다.
apply 메서드에서 필드에 저장된 람드를 호출하기만 하면 나열하는 방식보다 간결하고 깔끔해진다.
하지만!
메서드나 클래스와 달리, 람다는 이름이 없고 문서화도 못 한다. 따라서 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다를 쓰지 말아야 한다. ~~~(하지만 스트림과 람다는 꿀조합)~~~
람다가 길거나 읽기 어렵다면 더 간단히 줄이거나 람다를 쓰지 않는 쪽으로 리팩터링하길 바란다.
하지만 위임 하려고 썼다면..?
람다는 자신을 참조할 수 없다. 람다에서 this 키워드는 바깥 인스턴스를 가리킨다. 그래서 함수 객체가 자신을 참조하는 일이 생긴다면 반드시 익명 클래스를 써야 한다.
람다는 직렬화하는 일은 극히 삼가야한다. (익명 클래스의 인스턴스도 마찬가지), 직렬화해야만 하는 함수 객체가 있다면 private 정적 중첩 클래스(아이템 24)의 인스턴스를 사용하자.
람다표현식은 익명 함수가 하는 것처럼 free variable 을 사용할 수 있다. (매개변수로 넘어온 변수가 아닌 외부 지역변수를 사용할 수 있다.) 이것을 capturing lambda 라 부른다.
하지만 람다는 인스턴스 변수와 정적변수를 자유롭게 자신의 바디에서 참조할 수 있지만 지역 변수는 명시적으로 final 이거나, 그에 상응하게 사용되어야 한다. (변경이 일어나면 안된다는 제약이 있다.)
다른 언어에서 람다와 비슷한 개념으로는 클로저가 있는데 클로저는 익명 클래스와 람다와 유사하지만 위의 제약사항은 없다. 클로저는 함수의 비지역 변수를 자유롭게 참조할 수 있는 함수의 인스턴스를 가리킨다. 그래서 다른 함수의 인자로 클로저를 던질 수 있으며 람다와 익명 클래스도 비슷하게 메서드의 인자로 던질 수 있고 던져진 메서드의 지역 변수에도 접근할 수 있지만 변경할 수는 없다.
람다보다도 더 간결하게 만드는 방법 -> 메서드 참조
map.merge(key, 1, (count, incr) -> count + incr);
를 메서드 참조를 쓰면
map.merge(key, 1, Integer::sum);
로 보기 좋아진다.
매개변수 수가 늘어날수록 메서드 참조로 제거할 수 있는 코드양도 늘어난다.
하지만 매개변수명이 있으면 더 좋은 가이드가 되는 코드에서는 메서드 참조를 써서 매개변수명을 생략하기 보다는 그대로 두는 것이 더 읽기 쉽고 유지보수도 쉬울 수 있다.
람다로 구현했을 때 너무 길거나 복잡하다면 메서드 참조가 좋은 대안이 될 수 있다.
하지만 IDE가 메서드 참조로 다 바꾸라고 하겠지만 아닌 경우가 있을 순 있다.
service.execute(GoshThisClassNameIsHumongous::action);
// 이는 람다로 그냥 두는 것이 더 좋을 수 있다.
service.execute(() -> action());
// 근데 이렇게 되지 않나...?
service.execute(this::action);
같은 선상에서 Function.identity() 를 사용하기보다는 똑같은 기능의 (x -> x) 를 직접 사용하는 편이 코드도 짧고 명확하다.
메서드 참조 유형은 다섯가지가 있다.
-
정적 메서드를 가르키는 메서드 참조
Integer::parseInt
(args) -> ClassName.staticMethod(args) ClassName::staticMethod
-
수신객체를 특정하는 한정적 인스턴스 메서드 참조 (receiving object bound)
Instant.now()::isAfter -> Instant then = Instant.now(); t -> then.isAfter(t);
함수 객체가 받는 인수와 참조되는 메서드가 받는 인수가 똑같다.
(args) -> expr.instanceMethod(args) expr::instanceMethod
-
비한정적 인스턴스 메서드 참조 (unbound)
함수객체를 적용하는 시점에 수신 객체를 알려준다. 주로 스트림 파이프라인에서의 매핑과 필터 함수에 쓰인다. (아이템 45).
String::toLowerCase -> str -> str.toLowerCase()
(arg0, rest) -> arg0.instanceMethod(rest) ClassName::instanceMethod
-
클래스 생성자를 가리키는 메서드 참조
TreeMap<K,V>::new -> () -> new TreeMap<K,V>()
-
배열 생성자를 가리키는 메서드 참조
int[]::new ->. len -> new int[len]
자바 표준에도 우리가 쓸만한 왠만한 상황에 대한 인터페이스가 준비되어 있으니 굳이 불필요한 함수형 인터페이스를 만들지 말자
예제) LinkedHashMap 을 잘 재정의하면 좋은 캐시로 쓸 수 있다.
@FunctionalInterface interface EldestEntryRemovalFunction<K,V> {
boolean remove(Map<K,V> map, Map.Entry<K,V> eldest)
}
필요한 용도에 맞는 게 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용하라.
@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);
}
}
BiPredicate<Map<K,V>, Map.Entry<K,V>> 형식으로 사용할 수 있다.
java.util.function 패키지에는 총 43개의 인터페이스가 담겨있다.
UnaryOperator<T> T apply(T t) String::toLowerCase
BinaryOperator<T> T apply(T t1, T t2) BigInteger::add
Predicate<T> boolean test(T t) Collection::isEmpty
test 라는 추상 메서드를 정의하며 T 를 받아 boolean 으로 반환한다.
Predicate 인터페이스는 복잡한 프레디케이트를 만들 수 있도록 negate, and, or 세 가지 메서드를 제공한다.
Function<T,R> R apply(T t) Arrays::asList
입력을 출력으로 매핑하는 람다를 정의할 때 function 인터페이스를 활용하면 좋다.
@SafeVarargs
public static <T> List<T> asList(T... a) {
throw new RuntimeException("Stub!");
}
function<E, T> 을 받는 곳에 E methodName(T) 를 넣어도 동작한다. (메서드참조로 넣을 때)
메서드 참조를 하면 T -> { methodName(T) } 가 된 격이기 때문!!
메서드 참조 방식으로 하면 굳이 Function 타입을 반환하는 메서드나 변수를 만들 필요 없이 기존 메서드로 Function 타입을 반환할 수 있다. import 문 한줄을 더 줄일 수 있다
Supplier<T> T get() Instance::now
Consumer<T> void accept(T t) System.out::println
제네릭 T 객체를 받아서 void를 반환하는 accpet 라는 추상 메서드를 정의한다. 예를들어 Integer 리스트를 인수로 받아 각 항목에 어떤 동작을 수행하는 forEach 메서드를 정의할 때 Consumer 를 활용할 수 있다.
ArrayList<Integer> arrayList = new ArrayList<>();
forEach(arrayList, (i) -> {
i = i + 1;
});
public void forEach(List<Integer> list, Consumer<Integer> c) {
for(Integer i: list) {
c.accept(i);
}
}
추가로 기본 타입인 int, long, double 각각 3개씩 더 생겨난다.
Function 인터페이스에는 기본 타입을 반환하는 변형이 총 9개가 더 있다.
LongToIntFunction 처럼 SrcToResult 인 것이 6개
나머지는 입력이 T 이고 결과가 int, long, double 인 3개가 있다. (ToLongFunction<int[]>)
기본 함수형 인터페이스 중 3개는 인수를 2개씩 받는 변형이 있다.
BiPredicate<T,U> , BiFunction<T,U,R> , BiConsumer<T,U> 가 있고
ToIntBiPredicate<T,U> , ToLongBiPredicate<T,U> , ToDoubleBiPredicate<T,U> 와 같은 변형와
objDoubleConsumer, ObjIntConsumer, ObjLongConsumer 가 존재한다.
기본 함수형 인터페이스는 대부분 기본 형만 지원하는데, 그렇다고 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 마라. 계산량이 많을 때는 성능이 처참히 느려질 수 있다.
하지만 아래 조건 중 하나 이상을 만족한다면 전용 함수형 인터페이스를 구현하는 것을 고려하라.
- 자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다.
- 반드시 따라야 하는 규약이 있다.
- 유용한 디폴트 메서드를 제공할 수 있다.
그리고 전용 함수형 인터페이스를 작성하기로 했다면 '인터페이스' 임을 개발자가 명심해야 한다. 아주 주의해서 설계해야 한다는 뜻이다. (아이템 21). @FunctionalInterface 애너테이션을 사용하면 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일 되므로 항상 직접 만든 인터페이스에서는 사용하라
서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중 정의해서는 안된다.