람다보다는 메소드 참조를 사용하라 - Effective Java[43]
🔗 메서드 참조
-
람다가 익명 클래스보다 나은 점 중에서 가장 큰 특징은 간결함이다.
-
그런데 자바에는 함수 객체를 심지어 람다보다도 더 간결하게 만드는 방법이 있으니, 바로 메서드 참조(method reference)다.
-
메소드 참조는 말 그대로 메소드를 참조해서 매개 변수의 정보 및 리턴 타입을 알아내어, 람다식에서 불필요한 매개 변수를 제거하는 것을 말한다.
💎임의의 키와 Integer 값의 매핑을 관리하는 프로그램의 일부
-
이때 값이 키의 인스턴스 개수로 해석된다면, 이 프로그램을 멀티셋을 구현한 게 된다.
-
이 코드는 키가 맵 안에 없다면 키와 숫자 1을 매핑하고, 이미 있다면 기존 매핑 값을 증가시킨다.
map.merge(key, 1, (count, incr) -> count + incr);
-
위 코드는 자바 8 때 Map에 추가된 merge 메소드의 전형적인 쓰임을 잘 보여주고 있다.
-
merge 메소드는 키, 값, 함수를 인수로 받으며, 주어진 키가 맵 안에 아직 없다면 주어진 {키, 값} 쌍을 그대로 저장한다.
-
반대로 키가 이미 있다면 (세 번째 인수로 받은) 함수를 현재 값과 주어진 값에 적용한 다음, 그 결과로 현재 값을 덮어쓴다.
-
-
깔끔해 보이는 코드지만 아직도 거추장스러운 부분이 남아 있다.
-
매개변수인 count와 incr은 크게 하는 일없이 공간을 꽤 차지한다.
-
자바 8이 되면서 Integer 클래스(와 모든 기본 타입의 박싱 타입)은 이 람다와 기능이 같은 정적 메소드 sum을 제공하기 시작했다.
-
따라서 람다 대신 이 메소드의 참조를 전달하면 똑같은 결과를 더 보기 좋게 얻을 수 있다.
-
map.merge(key, 1, Integer::sum);
-
매개변수 수가 늘어날수록 메소드 참조로 제거할 수 있는 코드양도 늘어난다.
- 하지만 어떤 람다에서는 매개변수의 이름 자체가 프로그래머에게 좋은 가이드가 되기도 한다.
- 이런 람다는 길이는 더 길지만 메소드 참조보다 읽기 쉽고 유지보수도 쉬울 수 있다.
-
람다로 할 수 없는 일이라면 메소드 참조로도 할 수 없다.(예외가 하나 있긴함.. 제네릭 함수 타입 구현)
-
그렇더라도 메소드 참조를 사용하는 편이 보통은 더 짧고 간결하므로, 람다로 구현했을 때 너무 길거나 복잡하다면 메소드 참조가 좋은 대안이 되어준다.
-
즉, 람다로 작성할 코드를 새로운 메서드에 담은 다음, 람다 대신 그 메서드 참조를 사용하는 식이다.
-
메서드 참조에는 기능을 잘 드러내는 이름을 지어줄 수 있고 적절한 설명을 문서로 남길 수도 있다.
-
함수형 인터페이스의 추상메서드가 제네릭일 수 있듯이 함수 타입도 제네릭 일 수 있다. 다음의 인터페이스 계층 구조를 생각해보자.
-
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
- 이처럼 함수형 인터페이스를 위한 제네릭 함수 타입은 메서드 참조 표현식으로는 구현할 수 있지만, 람다식으로는 불가능하다. 제네릭 람다식이라는 문법이 존재하지 않기 때문이다.
💎 때론 람다가 메소드 참조보다 간결할 때가 있어!
- IDE들은 람다를 메소드 참조로 대체하라고 권할 것이다.
- IDE의 권고를 따르는 게 보통은 이득이지만, 항상 그런 것은 아니다.
-
주로 메서드와 람다가 같은 클래스에 있을 때 그렇다.
- ex) 다음 코드가 GoshThisClassNameIsHumongous 클래스 안에 있다고 가정해보자.
service.execute(GoshThisClassNameIsHumongous::action);
이름 람다로 대체하면 다음 처럼 된다.
service.execute(() -> action());
-
메소드 참조 쪽은 더 짧지도, 더 명확하지도 않다. 따라서 람다 쪽이 낫다.
-
같은 선상에서 java.util.function 패키지가 제공하는 제네릭 정적 팩토리 메소드인 Function.identity()를 사용하기보다는 똑같은 기능의 람다 (x -> x) 를 직접 사용하는 편이 코드도 짧고 명확하다.
💎 메소드 참조의 다섯가지 유형
- 첫 번째 가장 흔한 유형은 정적 메서드를 가리키는 메서드 참조이다.
메소드 참조
Integer::parseInt
같은 기능을 하는 람다
str -> Integer.parseInt(str)
-
두 번째 수신 객체(참조 대상 인스턴스)를 특정하는 한정적(bound) 인스턴스 메소드 참조
-
근본적으로는 정적 참조와 비슷하다.
-
즉, 함수 객체가 받는 인수와 참조되는 메서드가 받는 인수가 똑같다.
-
메소드 참조
Instant.now()::isAfter
같은 기능을 하는 람다
Instant then = Instant.now();
t -> then.isAfter(t)
-
세 번째 수신객체를 특정하지 않는 비한정적 인스턴스 메소드 참조
-
함수 객체를 적용하는 시점에 수신 객체를 알려준다.
-
이를 위해 수신 객체 전달용 매개변수가 매개변수 목록의 첫 번째로 추가되며, 그 뒤로는 참조되는 메서드 선언에 정의된 매개변수들이 뒤따른다.
-
비한정적 참조는 주로 스트림 파이프라인에서의 매핑과 필터 함수에 쓰인다.
-
메소드 참조
String::toLowerCase
같은 기능을 하는 람다
str -> str.toLowerCase()
- 네 번째 클래스 생성자를 가리키는 메서드 참조
메소드 참조
TreeMap<K,V>::new
같은 기능을 하는 람다
() -> new TreeMap<>()
- 다섯 번째 배열 생성자를 가리키는 메서드 참조
- 팩토리 객체로 사용된다.
메소드 참조
int[]::new
같은 기능을 하는 람다
len -> new int[len]
메서드 참조는 람다의 간단명료한 대안이 될 수 있다.
메서드 참조 쪽이 짧고 명확하다면 메서드 참조를 쓰고, 그렇지 않을 때만 람다를 사용하라.
참조 - 이펙티브 자바 3/E - 조슈아 블로크
Comments