Effective Java를 읽고 정리한 정리본입니다.

📌 Item 43 : 람다보다는 메서드 참조를 사용하라

🫧 람다 vs 메서드 참조

람다가 익명 클래스보다 나은 점 중 가장 큰 특징은 간결함이다. (Item 42)

그러나 람다보다 메서드 참조 (method reference)가 코드를 더 간결하게 만들 수 있다.

다음 코드는 임의의 키와 Integer 값의 매핑을 관리하는 프로그램의 일부이다.

이때 값이 키의 인스턴스 개수로 해석된다면, 이 프로그램은 멀티셋(multiset)을 구현한 것이 된다.

map.merge(key, 1, (count, incr) -> count + incr);

키가 맵 안에 없다면 키와 숫자 1을 매핑하고, 이미 있다면 기존 매핑 값을 증가시킨다.

참고로, Map 클래스의 merge는 다음과 같은 코드로 작성되어 있다.

merge 메서드는 키, 값, 함수를 인수로 받아온다.

key가 map 안에 아직 없다면 value를 {key, value} 형태로 매핑하고, 이미 있다면 세 번째 인수로 받은 함수를 현재 값과 주어진 값에 적용한 다음, 그 결과로 현재 값을 덮어쓴다.

즉, 맵에 {키, 함수의 결과} 쌍을 저장한다.

default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
  Objects.requireNonNull(remappingFunction);
  Objects.requireNonNull(value);
  V oldValue = get(key);
  V newValue = (oldValue == null) ? value : remappingFunction.apply(oldValue, value);
  
  if (newValue == null) {
    remove(key);
  } else {
    put(key, newValue);
  }
  return newValue;
}

깔끔해 보이는 코드지만 아직도 거추장스러운 부분이 남아있다.

두 인수의 합을 단순히 반환할 뿐, 매개변수인 count와 incr는 크게 하는 일 없이 공간을 차지하고 있다.

자바 8이 되면서 Integer 클래스와 모든 기본 타입의 박싱 타입은 이 람다와 기능이 같은 정적 메서드 sum을 제공하기 시작했다.

따라 람다 대신 이 메서드의 참조를 전달하면 똑같은 결과를 더 간결하게 얻을 수 있다.

map.merge(key, 1, Integer::sum);

하지만 어떤 람다에서는 매개변수의 이름 자체가 프로그래머에게 좋은 가이드가 되기도 한다.

이런 람다는 길이는 더 길지만 메서드 참조보다 읽기 쉽고 유지보수도 쉬울 수 있다.

🫧 메서드 참조 > 람다

람다로 할 수 없는 일이라면 메서드 참조로도 할 수 있다.

유일한 예외인 제네릭 함수 타입을 제외하고 말이다.

이 부분은 맨 아래에서 추가로 설명할 예정이다.

람다로 작성할 코드를 새로운 메서드에 담은 다음, 람다 대신 그 메서드 참조를 사용하는 식으로 사용도 가능하다.

메서드 참조에는 기능을 잘 드러내는 이름을 지어줄 수 있고, 친절한 설명을 문서로 남길 수 있으므로 람다보다는 메서드 참조가 더 효과적일 것이다.

🫧 메서드 참조 < 람다

때로는 람다가 메서드 참조보다 간결할 때가 있다.

주로 메서드와 람다가 같은 클래스에 있을 때 그렇다.

예를 들어 다음 코드가 GoshThisClassNameIsHumongous 클래스 안에 있다고 하자.

service.execute(GoshThisClassNameIsHumongous::action);

이를 람다로 대체하면 다음과 같다.

service.execute(() -> action());

또한, java.util.function 패키지가 제공하는 제네릭 정적 팩터리 메서드인 Function.identity()를 사용하기보다는 똑같은 기능의 람다(x -> x)를 직접 사용하는 편이 더 나을 것이다.

🫧 메서드 참조의 유형

메서드 참조의 유형은 다섯 가지가 존재하는데, 그 중 가장 흔한 유형은 앞의 예에서 본 것처럼 정적 메서드를 가리키는 메서드 참조다.

이 메서드 참조를 포함한 다섯 가지의 메서드 참조를 알아보고자 한다.

image

해당 사진에서의 인스턴스 메서드 참조는 수신 객체(참조 대상 인스턴스)를 특정하는 한정적 인스턴스 메서드 참조와 수신 객체를 특정하지 않는 비한정적 인스턴스 메서드 참조로 나뉜다.

✨ 한정적 참조

한정적 참조는 근본적으로 정적 참조와 비슷한데, 함수 객체가 받는 인수와 참조되는 메서드가 받는 인수가 똑같기 때문이다.

여기서 수신 객체란 참조 대상 인스턴스를 말한다.

map.merge(key, 1, (count, incr) -> count + incr);

가령 위에서 key 같은 것들이 수신 객체가 될 수 있다.

✨ 비한정적 참조

비한정적 참조에서는 함수 객체를 적용하는 시점에 수신 객체를 알려준다.

이를 위해 수신 객체 전달용 매개변수가 매개변수 목록의 첫 번째로 추가되며, 그 뒤로는 참조되는 메서드 선언에 정의된 매개변수들이 뒤따른다.

비한정적 참조는 주로 스트림 파이프라인에서의 매핑과 필터 함수에 쓰인다.

✨ 클래스 생성자 & 배열 생성자

클래스 생성자를 가리키는 메서드 참조와 배열 생성자를 가리키는 메서드 참조는 모두 생성자 참조로, 이들은 팩터리 객체로 사용된다.

여기서 팩토리는 객체 생성을 처리하는 클래스를 뜻한다.

🫧 제네릭 함수 타입 (람다 X, 메서드 참조 O)

앞서 살펴보았듯, 람다로는 불가능하나 메서드 참조로는 가능한 예가 단 한 가지 있다고 했다. 그것이 바로 제네릭 함수 타입이다.

함수형 인터페이스의 추상 메서드가 제네릭일 수 있듯이, 함수 타입도 제네릭일 수 있다.

다음의 인터페이스 계층 구조를 생각해보자.

interface G1 {
  <E extends Exception> Object m() throws E;
}
interface G2 {
  <F extends Exception> String m() throws Excepton;
}
interface G Extends G1, G2 {}

이때 함수형 인터페이스 G를 함수 타입으로 표현하면 다음과 같다.

<F extends Exception> () -> String throws F

함수형 인터페이스를 위한 제네릭 함수 타입은 메서드 참조 표현식으로만 가능하다. 제네릭 람다식이라는 것은 애초에 존재하지 않기 때문이다.

🫧 참고 자료

  • [람다식을 더 짧게 - 메소드::참조 문법 Tistory Inpa Dev](https://inpa.tistory.com/entry/JAVA8-%E2%98%95-%EB%9E%8C%EB%8B%A4%EC%8B%9D%EC%9D%84-%EB%8D%94-%EC%A7%A7%EA%B2%8C-%EB%A9%94%EC%86%8C%EB%93%9C-%EC%B0%B8%EC%A1%B0Method-Reference)
  • [[헤드 퍼스트 디자인 패턴] 04. 팩토리 패턴 Velog akim](https://velog.io/@akimcse/%ED%97%A4%EB%93%9C-%ED%8D%BC%EC%8A%A4%ED%8A%B8-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-04.-%ED%8C%A9%ED%86%A0%EB%A6%AC-%ED%8C%A8%ED%84%B4)