람다가 익명 클래스보다 나은 점 중에서 가장 큰 특징은 간결함이다.
그런데 자바에는 함수 객체를 람다보다도 더 간결하게 만드는 방법이 있으니 바로 메서드 참조(method reference)다.
임의의 키와 Integer 값의 매핑을 관리하는 프로그램을 예로 들자면
map.merge(key, 1, (count, incr) -> count + incr);
이 코드는 주어진 key가 맵 안에 없다면 key값과 1을 map에 저장한다.
key가 map에 이미 있다면 기존의 맵핑된 값과 주어진 1을 더한 결과값으로 매핑값을 변경한다.
깔끔해보이지만 매개변수 count와 incr는 크게 하는 일 없이 공간을 꽤 차지한다.
이 람다는 단순히 두인수의 합을 반환할 뿐이다.
자바 8이 되면서 Integer 클래스(모든 기본 타입의 박싱 타입)는 위의 람다와 기능이 같은 정적 메서드 sum을 제공하기 시작했다.
map.merge(key, 1, Integer::sum);
Integer::sum이 BiFunction에 들어갈 수 있는 이유는 해당 식이 BinaryOperator로 변환되는데 BinaryOperator는 BiFunction을 상속받은 객체이다.
메서드 참조 vs 람다
- 매개변수의 수가 늘어날수록 메서드 참조로 제거할 수 있는 코드양도 늘어난다.
- 하지만 어떤 람다에서는 매개변수의 이름 자체가 프로그래머에게 좋은 가이드가 되기도 한다.
- 이런 람다는 길이는 더 길지만 메서드 참조보다 읽기 쉽고 유지보수도 쉬울 수 있다.
- 람다로 할 수 없는 일이라면 메서드 참조로도 할 수 없다.
- 예외가 하나 있는데, 제네릭 함수 타입의 구현이다.
- 메서드 참조를 사용하는 편이 보통 더 짧고 간결하므로, 람다로 구현했을 때 너무 길거나 복잡하다면 메서드 참조가 좋은 대안이다.
- 람다로 작성할 코드를 새로운 메서드에 담은 다음, 람다 대신 그 메서드 참조를 사용한다.
- 메서드 참조에는 기능을 잘 드러내는 이름을 지어줄 수 있고 설명을 문서로 남길 수 있다.
- IDE들은 람다를 메서드 참조로 대체하라고 권하는데, 권고를 따르는게 보통 이득이지만 때론 람다가 메서드 참조보다 간결할 때가 있다.
- 주로 메서드와 람다가 같은 클래스에 있을 때 그렇다.
service.excute(GoshThisClassNameIsHumongous::action);
위의 코드가 GoshThisClassNameHumongous 클래스 안에 있다고 했을 때 람다로 대체하면
service.execute(() -> action());
메서드 참조보다 람다로 간결하게 표현할 수 있다.
메서드 참조 쪽은 더 짧지도, 더 명확하지도 않다.
java.util.function 패키지가 제공하는 제네릭 정적 팩터리 메서드인 Function.identity()를 사용하기 보다 똑같은 기능의 람다 (x -> x)를 사용하는 편이 코드도 짧고 명확하다.
메서드 참조 유형
메서드 참조 유형은 5가지 유형이 있다.
- 정적 메서드를 가리키는 메서드 참조
- 가장 흔한 유형으로 앞의 GoshThisClassNameIsHumongous::action 처럼 정적 메서드를 가리키는 메서드 참조
- 수신 객체(receiving object 참조 대상 인스턴스)를 특정하는 한정적(bound) 인스턴스 메서드 참조
- 한정적 참조는 근본적으로 정적 참조와 비슷하다. 즉, 함수 객체가 받는 인수와 참조되는 메서드가 받는 인수가 똑같다.
- 수신 객체를 특정하지 않는 비 한정적(unbound) 인스턴스 메서드 참조
- 비 한정적 참조에서는 함수 객체를 적용하는 시점에 수신 객체를알려준다. 이를위해 수신 객체 전달용 매개변수가 매개변수 목록의 첫 번째로 추가되며, 그 뒤로는 참조되는 메서드 선언에 정의된 매개변수들이 뒤따른다. 비 한정적 참조는 주로 스트림 파이프라인에서의 매핑과 필터 함수에 쓰인다.
- 클래스 성성자를 가리키는 메서드 참조
- 배열 생성자를 가리키는 메서드 참조
메서드 참조 유형 | 예 | 같은 기능을 하는 람다 |
정적 | Integer::parseInt | str -> Integer.parseInt(str) |
한정적 | Instant.now()::isAfter | Instant then = Instant.now(); t -> then.isAfter(t) |
비한정적 | String::toLowerCase | str -> str.toLowerCase() |
클래스 생성자 | TreeMap<K, V)::new | () -> new TreeMap<K, V>() |
배열 생성 | int[]::new | len -> new int[len] |
제네릭 함수 타입 (generic function type)
람다로는 불가능하나 메서드 참조로 가능한 유일한 예는 바로 제네릭 함수 타입 구현이다.
함수형 인터페이스의 추상 메서드가 제네릭일 수 있듯이 함수 타입도 제네릭일 수 있다.
interface G1 {
<E extends Exception> Object m() throws E;
}
interface G2 {
<F extends Exception> String m() throws Exception;
}
interface G extends G1, G2 {}
위 코드에서 함수형 인터페이스 G를 함수 타입으로 표현하면
<F extends Exception> () -> String throws F
이처럼 함수형 인터페이스를 위한 제네릭 함수 타입은 메서드 참조 표현식으로는 구현할 수 있지만 람다식으로는 불가능하다.
제네릭 람다식이라는 문법이 존재하지 않기 때문
핵심 정리
메서드 참조는 람다의 간단명료한 대안이 될 수 있다. 메서드 참조 쪽이 짧고 명확하다면 메서드 참조를 쓰고, 그렇지 않을 때만 람다를 사용하라.
'개인룸 > 도윤' 카테고리의 다른 글
Item47. 반환 타입으로는 스트림보다 컬렉션이 낫다 (0) | 2023.06.25 |
---|---|
Item45. 스트림은 주의해서 사용하라 (0) | 2023.06.20 |
Item42. 익명 클래스보다는 람다를 사용하라 (0) | 2023.06.07 |
Item41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2023.05.30 |
Item40. @Override 애너테이션을 일관되게 사용하라 (1) | 2023.05.30 |