Post

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

메서드 참조의 장점

람다의 가장 큰 장점은 간결함이다. 그런데 함수객체를 람다보다 더 간결하게 만드는 방법이 있다.

바로 메서드 참조(Method Reference)다.

자바8 Map merge 메서드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
  • 키, 값, 함수를 인수로 받으며, 주어진 키가 맵 안에 없다면 주어진 [키, 값] 쌍을 그대로 저장
  • 주어진 키가 맵 안에 있다면 remappingFunctionapply 인자로 이전 값과 현재 값을 대입하여 [키, 함수의 결과] 쌍을 저장한다.
1
2
3
// key가 맵 안에 없으면 키와 숫자 1을 매핑하고,
// 이미 있다면 기존 매핑 값을 증가시킨다.
map.merge(key, 1, (count, incr) -> count + incr);

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

자바8부터 Integer 클래스는 람다와 기능이 같은 정적 메서드 sum 을 제공하기 시작했다.

1
2
3
4
5
6
7
8
public final class Integer extends Number
    implements Comparable<Integer>, Constable, ConstantDesc {
    
    ...
    public static int sum(int a, int b) {
        return a + b;
    }
}

merge 메서드 매개변수에 람다대신 메서드 참조를 전달하면, 똑같은 결과를 더 보기 좋게 표현가능하다.

1
2
// 정적 메서드 참조
map.merge(key, 1, Integer::sum);

메서드 참조 사용 시 주의사항

메서드 참조를 사용하면 대부분 코드가 간결해지고 명확해지지만, 그렇지 않은 경우도 있다.

람다에서 사용하는 매개변수 이름이 좋은 가이드가 될 경우, 코드의 길이가 더 길어도 메서드 참조보다 읽기 쉽고 유지보수도 쉬울 수 있다.

아래 예시의 경우, 람다를 사용한 경우가 좀 더 읽기 쉽고 명확해 더 좋은 코드인것 같다.

1
2
Consumer<String> printString1 = (str) -> System.out.println(str);  // 람다 사용
Consumer<String> printString2 = System.out::println;               // 메서드 참조

메서드 참조에 사용하는 클래스 이름이 길다면 오히려 람다가 더 간결하게 표현될 수 있다.

예를 들어 GoshThisClassNameIsHumongous 라는 클래스에 action 메서드를 참조한다고 해보자.

1
2
// 메서드 참조
service.execute(GoshThisClassNameIsHumongous::action);

너무 긴 이름 때문에 오히려 가독성이 떨어진다.

이를 람다로 표현하면 훨씬 간결해진다.

1
2
// 람다
service.execute(() -> action());

💡 결론

따라서 람다와 메서드 참조를 선택할 때, 메서드 참조 쪽이 짧고 명확한 표현을 할 수 있다면 메서드 참조를 쓰고, 그렇지 않을 때만 람다를 사용하면 된다.


메서드 참조의 유형

메서드 참조 유형같은 기능을 하는 람다
정적Integer::parseIntstr → Integer.parseInt(str)
한정적(인스턴스)Instant.now()::isAfterInstant then = Instant.now(); t -> then.isAfter(t)
비한정적(인스턴스)String::toLowerCasestr -> str.toLowerCase()
클래스 생성자TreeMap<K,V>::new() -> new TreeMap<K,V>()
배열 생성자int[]::newlen -> new int[len]
  • 한정적 (인스턴스) : 수신 객체(참조 대상 인스턴스)를 특정하는 한정적 인스턴스 메서드 참조
  • 비한정적 (인스턴스) : 수신객체를 특정하지 않음

한정적 메서드 참조와 비한정적 메서드 참조의 차이점

1) 비한정적 메서드 참조

1
2
3
// 비한정적 메서드 참조
// ClassName::method
String::toUpperCase
  • 비한정적(unbound) 메서드 참조 시, 정적 메서드 참조처럼 매개변수의 클래스 타입명을 기재한다.
  • 비한정적이라는 표현은 특정한 객체를 참조하기 위한 변수를 지정하지 않는다는 의미이다.
  • String 클래스의 toUpperCase 메서드는 정적메서드가 아닌 인스턴스 메서드이므로 반드시 String 클래스가 객체화되어야만 호출가능하다.
1
2
// 람다로 표현
(String str) -> str.toUpperCase()
  • 람다로 내용을 풀어서 보면, 객체의 생성을 매개변수로 받는다.
  • 람다 표현 식 내부에서 객체 생성이 일어났기 때문에 객체를 참조할만한 변수가 외부에 존재하지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MethodReferencesExamples {
		
    public static <T> T mergeThings(T a, T b, BiFunction<T, T, T> merger) {
        return merger.apply(a, b);
    }

    public static void main(String[] args) {

        System.out.println(MethodReferencesExamples.
                mergeThings("Hello ", "World!", (String a, String b) -> a.concat(b)));
				
        System.out.println(MethodReferencesExamples.
                mergeThings("Hello ", "World!", String::concat));
    }
}
  • 처리해야 하는 데이터가 여러개라면 조금 복잡해질 수 있다.
  • 문자열 2개를 합치는 방식을 작성하면 다음과 같다. 함축적인 편이라 이해하고 해석하는데 어려울 수 있다.

2) 한정적 메서드 참조

  • 한정적(bound)이라는 단어를 사용한 이유는 메서드 참조 시 특정 객체의 변수로 제한되기 때문이다.
1
2
3
4
5
6
7
Calendar.getInstance()::getTime

Calendar cal = Calendar.getInstance();  // 객체 생성
() -> cal.getTime()  // 람다

Calendar cal = Calendar.getInstance();  // 객체 생성
cal::getTime  // 메서드 참조 구문. cal 변수를 참조

3) 정리

한정적 메서드 참조는 외부에서 정의한 객체의 메서드를 참조할 때 사용하며, 비한정적 메서드 참조는 람다 표현식 내부에서 생성한 객체의 메서드를 참조할 때 사용한다.

참고

https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html

This post is licensed under CC BY 4.0 by the author.