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

가능한 람다를 사용하자!

📌 1. 발표 전 알아야 할 개념

✅ 수신 객체(receiver object)

  • 메서드가 호출되는 대상 객체

  • kim.toUpperCase() 라고 하면 -> 수신 객체는 kim

✅ 람다에 대해 다시 한 번 짚고 넘어가자!

  • 람다 : java 8 이후에 도입된, 익명 함수를 간단하게 표현하는 방식

// Consumer<T> 람다 구현
List<String> carList = List.of("Tesla", "GV70", "Porshce");
carList.forEach(car -> System.out.println(car));

📕 2. 메서드 참조를 사용하라

2-1. 메서드 참조란?

  • 기존의 메서드 이름을 그대로 사용하는 표현 방식

  • 간결하고, 의도를 명확하게 표현할 수 있다

위의 식을 바꿔보면

carList.forEach(System.out::println);
// System.out 객체의 println 메서드를 각각의 요소에 대해 호출하라

(예시)

// compareToIgnoreCase 구현(대소문자를 무시하고 비교)
// 람다 표현식
list.sort((s1, s2) -> s1.compareToIgnoreCase(s2));

// 메서드 참조
list.sort(String::compareToIgnoreCase);

2-2. 메서드 참조의 5가지 종류

  1. 정적 메서드를 가리키는 메서드 참조

  • ClassName::staticMethod 형식

  • 정적 메서드를 참조함

  • ex) Math::abs, Integer::parseInt

Function<String, Integer> parse = Integer::parseInt;
System.out.println(parse.apply("123"));
  1. 수신 객체를 특정하는 한정적 인스턴스 메서드 참조

  • instance::instanceMethod 형식

  • 수신 객체가 이미 정해져 있음

  • 인스턴스를 대상으로 메서드 호출

  • ex) System.out::println

PrintStream out = System.out;
Consumer<String> printer = out::println;
  1. 수신 객체를 특정하지 않는 비한정적 인스턴스 메서드 호출

  • ClassName::instanceMethod 형식

  • 수신 객체가 지금은 정해져 있지 않지만, 실행시 첫 번째 인자가 수신 객체

  • 클래스 타입 기준으로 메서드를 참조

  • ex) String::compareToIgnoreCase, String::equalsIgnoreCaes

List<String> names = List.of("Kim", "Lee", "Park");
names.sort(String::compareToIgnoreCase); // 각 요소끼리 비교
  1. 클래스 생성자를 가리키는 메서드 참조

  • ClassName::new 형식

  • 생성자를 참조

  • ex) ArrayList::new, HashMap::new

Supplier<List<String>> listSupplier = ArrayList::new;
List<String> list = listSupplier.get(); // 새 ArrayList 생성
  1. 배열 생성자를 가리키는 메서드 참조

  • Type[]::new 형식

  • 배열을 생성하는 생성자 참조

  • ex) String[]::new, int[]::new

IntFunction<String[]> arrayCreator = String[]::new;
String[] arr = arrayCreator.apply(5);

🤔 3. 메서드 참조의 장단점

3-1. 왜 메서드 참조를 선호해야 할까?

  • 불필요한 매개변수, 람다 본문 생략으로 간결함

  • 메서드를 그대로 사용하기 때문에 의도가 더 명확하게 드러난다

  • 중복을 줄일 수 있다

3-2. 메서드 참조가 안 되는 경우?

  • 단순한 메서드 호출이 아닌 경우

  • 조건문이나 루프를 포함하는 경우

  • 변수 캡쳐를 포함하는 경우

예시)

list.forEach(name -> {
    log(name);
    sendEmail(name);
})

메서드 참조로 바꾸기 위해서는 람다 본문을 별도의 메서드로 추출하고, 메서드를 메서드 참조로 넘겨야 한다.

람다식을 process라는 메서드로 추출하고,

public class Service {
    public static void process(String name) {
        log(name);
        sendEmail(name);
    }

    private static void log(String name) {
        System.out.println("Logging: " + name);
    }

    private static void sendEmail(String name) {
        System.out.println("Sending email to: " + name);
    }
}

메서드 참조로 바꾸면 된다!

list.forEach(Service::process);

💨 향후 확장 포인트

  • 람다로 할 수 없는 일이라면 메서드 참조로도 할 수 없다(애매한 예외 단 한가지를 빼고...)

  • 람다로 표현할 때 타입 인자를 일반적으로 생략해야하는데, 제네릭의 경우 타입을 직접 지정해야 한다. 다만 메서드 참조의 경우 문맥에서 타입을 추론할 수 있으므로 훨씬 깔끔함

public class MyUtils {
    public static <T> T identity(T t) {
        return t;
    }
}
// 타임 명시를 안할 경우 - x의 타입이 확실하지 않음
UnaryOperator<String> identity1 = x -> MyUtils.identity(x);
// 람다 - 타입 명시를 해야함 <String>
UnaryOperator<String> identity1 = x -> MyUtils.<String>identity(x);
// 메서드 참조 - 타입 추론 가능!
UnaryOperator<String> identity2 = MyUtils::identity;

🤖 최종 결론

메서드 참조가 짧고 명확하다면 메서드 참조를 써라!


❗어려웠던 점

  • 이전의 학습 내용을 계속 확장하면서 공부해서 이해가 안되는 부분이 있거나 헷갈리는 부분에서 이해가 바로 안됐지만, 오히려 한 번 더 정리할 수 있었다!

  • 제네릭의 경우 메서드 참조가 유리하다는 점에서 컴파일의 해석 방식을 생각해야 하는 부분이 두개가 비슷하게 느껴져 어려웠다.


😶‍🌫️ 느낀점

  • 단순히 짧게 쓰는 문법이 아니라 의도가 더 명확하게 드러나고, 가독성과 의미 전달에서 메서드 참조를 사용해야겠다고 생각했다!

Last updated