꾸준한 스터디
article thumbnail

람다가 익명 클래스보다 나은 점 중에서 가장 큰 특징은 간결함이다.

그런데 자바에는 함수 객체를 람다보다도 더 간결하게 만드는 방법이 있으니 바로 메서드 참조(method reference)다.

 

임의의 키와 Integer 값의 매핑을 관리하는 프로그램을 예로 들자면

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

이 코드는 주어진 key가 맵 안에 없다면 key값과 1을 map에 저장한다.

key가 map에 이미 있다면 기존의 맵핑된 값과 주어진 1을 더한 결과값으로 매핑값을 변경한다.

깔끔해보이지만 매개변수 count와 incr는 크게 하는 일 없이 공간을 꽤 차지한다.

이 람다는 단순히 두인수의 합을 반환할 뿐이다.

기존값인 2와 인자값 1을 더한 값이 저장된다.
merge 메서드에 세번째 인자로 인자 2개, 결과값 1개를 받는 BiFunction을 매개변수로 받고 있다.

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

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

결과는 같다.
람다와 기능이 같은 정적 메서드 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가지 유형이 있다.

 

  1. 정적 메서드를 가리키는 메서드 참조
    • 가장 흔한 유형으로 앞의 GoshThisClassNameIsHumongous::action 처럼 정적 메서드를 가리키는 메서드 참조
  2. 수신 객체(receiving object 참조 대상 인스턴스)를 특정하는 한정적(bound) 인스턴스 메서드 참조
    • 한정적 참조는 근본적으로 정적 참조와 비슷하다. 즉, 함수 객체가 받는 인수와 참조되는 메서드가 받는 인수가 똑같다.
  3. 수신 객체를 특정하지 않는 비 한정적(unbound) 인스턴스 메서드 참조
    • 비 한정적 참조에서는 함수 객체를 적용하는 시점에 수신 객체를알려준다. 이를위해 수신 객체 전달용 매개변수가 매개변수 목록의 첫 번째로 추가되며, 그 뒤로는 참조되는 메서드 선언에 정의된 매개변수들이 뒤따른다. 비 한정적 참조는 주로 스트림 파이프라인에서의 매핑과 필터 함수에 쓰인다. 
  4. 클래스 성성자를 가리키는 메서드 참조
  5. 배열 생성자를 가리키는 메서드 참조
메서드 참조 유형 같은 기능을 하는 람다
정적 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

이처럼 함수형 인터페이스를 위한 제네릭 함수 타입은 메서드 참조 표현식으로는 구현할 수 있지만 람다식으로는 불가능하다.

제네릭 람다식이라는 문법이 존재하지 않기 때문

 

 

핵심 정리
메서드 참조는 람다의 간단명료한 대안이 될 수 있다. 메서드 참조 쪽이 짧고 명확하다면 메서드 참조를 쓰고, 그렇지 않을 때만 람다를 사용하라.
profile

꾸준한 스터디

@StudyRecord

포스팅이 유익하셨다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!