본문 바로가기
Dev Books/Effective Java

[item 43] 람다보다는 메서드 참조를 사용하라

by Thumper 2025. 6. 30.

메서드 참조

람다가 익명 클래스보다 나은 점 중에서 가장 큰 특징은 간결함이다. 그런데 자바에는 함수 객체를 메서드 참조로 더 간결하게 만들 수 있다.

예시

list.forEach(System.out::println);  // 람다 대신 메서드 참조

자바 8 Map.merge() 메서드

자바 8 때 Map에 추가된 merge 메서드로 살펴보자.

Map.merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)

key가 없으면 value를 삽입하고, key가 이미 있으면 remappingFunction으로 기존 값과 새 값을 병합한다.

람다 사용

wordCounts.merge(word, 1, (oldVal, newVal) -> oldVal + newVal);

연산 로직을 코드 안에 명시한다.

메서드 참조를 사용할 때

wordCounts.merge(word, 1, Integer::sum);

람다 대신 이미 정의된 메서드 이름만 전달하기 때문에 더 간결하다.

언제 람다, 언제 메서드 참조?

메서드 참조를 추천하는 경우

  • 이미 잘 알려진 메서드를 단순히 호출만 할 때
  • 코드가 더 짧고 의도를 명확히 전달 가능할 때
  • ex: Integer::sum, String::concat, Objects::isNull

람다 추천

로직이 단순 메서드 호출이 아니거나 커스텀 연산이 필요할 때

(oldVal, newVal) -> oldVal > newVal ? oldVal : newVal

람다가 더 간결한 경우

람다가 메서드 참조보다 더 간결하거나 적합한 경우가 있다.

메서드와 람다가 같은 클래스에 있을 때

import java.util.*;

public class Example {
    public void run() {
        List<String> list = Arrays.asList("a", "bbb", "cc");

        // 같은 클래스의 메서드 process를 호출
        list.forEach(s -> process(s));  // 람다가 간결하고 자연스러움

        // 메서드 참조도 가능은 하지만 오히려 덜 직관적
        list.forEach(this::process);
    }

    private void process(String s) {
        System.out.println("Processing: " + s);
    }
}

람다를 사용하면 호출 흐름이 명확히 코드에 드러난다.

s -> process(s)

기능은 같지만 this:: 구문이 오히려 낯설거나 불필요할 수 있다.

list.forEach(this::process);

람다 쪽이 간결한 경우

람다가 단순히 "한 줄 호출/인자 변형"을 할 때 표현이 자연스럽다.

list.forEach(s -> log(process(s)));

하지만 "같은 클래스 메서드 호출" "인자 가공/결합/조건 추가"가 있으면, 메서드 참조보다 람다가 더 읽기 쉽고 직관적이다.

메서드 참조 유형

메서드 참조의 유형은 5가지가 있는데, 각각 간단한 예제와 함께 정리해보자.

1. 정적 메서드 참조

정적 메서드 참조 형식은 다음과 같다.

클래스명::static메서드명

람다와 비교하면 다음과 같다.

// 정적 메서드 참조 
Function<String, Integer> f1 = Integer::parseInt;

// 람다
Function<String, Integer> f2 = s -> Integer.parseInt(s);

2. 한정적 메서드 참조

한정적 메서드 참조 형식은 다음과 같다.

참조변수::인스턴스메서드명

람다와 비교하면 다음과 같다.

String str = "hello";

// 한정적 메서드 참조
Supplier<String> f1 = str::toUpperCase;

// 람다
Supplier<String> f2 = () -> str.toUpperCase();

한정적 참조는 정적 메서드 참조와 비슷하다.

한정적 참조는 함수 객체가 받는 인수와 참조되는 메서드의 인수가 그대로 일치한다.

정적 메서드 참조

Integer::sum  
// (x, y) -> Integer.sum(x, y)

한정적 참조

someString::toUpperCase  
// () -> someString.toUpperCase()
  • 수신 객체가 이미 한정되어 있다. (예: Integer, someString)
  • 함수 객체의 파라미터와 메서드 파라미터가 같다.

3. 비한정적 메서드 참조

비한정적 메서드 참조 형식은 다음과 같다.

클래스명::인스턴스메서드명

람다와 비교하면 다음과 같다.

// 비한정적 메서드 참조
Comparator<String> c1 = String::compareToIgnoreCase;

// 람다
Comparator<String> c2 = (s1, s2) -> s1.compareToIgnoreCase(s2);

함수 객체를 적용하는 시점에 수신 객체(this)를 전달한다.

String::compareToIgnoreCase  
// (s1, s2) -> s1.compareToIgnoreCase(s2)

String::compareToIgnoreCase 같은 비한정적 메서드 참조는
함수 객체를 호출할 때 첫 인자가 수신 객체(this)로 쓰이고, 나머지 인자는 메서드 파라미터로 전달된다.

4. 클래스 생성자 참조

클래스 생성자 참조 형식은 다음과 같다.

클래스명::new

람다와 비교하면 다음과 같다.

// 클래스 생성자 참조
Supplier<List<String>> f1 = ArrayList::new;

// 람다
Supplier<List<String>> f2 = () -> new ArrayList<>();

팩토리처럼 생성자로 객체를 만들어 준다.

import java.util.function.Supplier;
import java.util.function.Function;

class Person {
    String name;
    Person() { this.name = "Unknown"; }
    Person(String name) { this.name = name; }
}

// Supplier: 인자 없이 객체 생성
Supplier<Person> supplier = Person::new;
Person p1 = supplier.get();  // new Person()

// Function: String -> Person 생성
Function<String, Person> function = Person::new;
Person p2 = function.apply("Alice");  // new Person("Alice")

여기서 Supplier, Function은 팩토리 인터페이스처럼 동작한다.

  • Supplier<Person> supplier = Person::new : supplier.get() 호출하면 객체를 생성한다.
  • Function<String, Person> function = Person::new : function.apply("Alice") 호출하면 생성자에 인자를 넣어 생성한다.

이처럼 생성자 참조는 팩토리 메서드와 동일한 역할을 하는 함수 객체를 만들어 준다.

5. 배열 생성자 참조

배열 생성자 참조 형식은 다음과 같다.

타입[]::new

람다와 비교하면 다음과 같다.

// 배열 생성자 참조
IntFunction<int[]> f1 = int[]::new;

// 람다
IntFunction<int[]> f2 = size -> new int[size];

배열 생성자 참조도 “팩터리”처럼 동작한다.

배열 생성자 참조(타입[]::new)도 new 연산자를 함수형 인터페이스에 캡슐화하여 배열 팩터리처럼 동작한다.

int[] arr = f1.apply(5);  // new int[5]

여기서 f1은 int[]를 생성하는 함수 객체이다.

  • 생성 로직은 캡슐화
  • 호출부는 apply(5)만 하면 된다. (팩터리 메서드처럼 동작)

예를 들어, Stream API에서

String[] array = list.stream().toArray(String[]::new);
  • String[]::new이 배열 생성 팩터리 역할을 한다.
  • 스트림이 몇 개의 요소인지 미리 알 수 없지만, toArray는 String[]::new 팩터리에 크기만 넘기면 배열을 생성한다.

결론

메서드 참조 쪽이 짧고 가독성이 좋을 수 있다. (그렇지 않을 때만 람다를 사용하라.)

댓글