메서드 레퍼런스(Method Reference)는 Java 8에서 도입된 기능으로, 람다 표현식을 더 간결하게 작성하는 방법이다. 클래스::메서드명 구문을 사용해 기존 메서드를 참조한다.


람다 vs 메서드 레퍼런스

// 람다 표현식
str -> str.toString()
str -> str.length()
(a, b) -> a.compareTo(b)
x -> System.out.println(x)

// 메서드 레퍼런스
Object::toString
String::length
Integer::compareTo
System.out::println

메서드 레퍼런스는 람다의 축약형이다. 람다가 단순히 기존 메서드를 호출하는 경우 사용할 수 있다.


메서드 레퍼런스 4가지 유형

유형 문법 예시
정적 메서드 Class::staticMethod Integer::parseInt
인스턴스 메서드 (특정 객체) instance::method System.out::println
인스턴스 메서드 (임의 객체) Class::instanceMethod String::length
생성자 Class::new ArrayList::new

1. 정적 메서드 참조

ClassName::staticMethodName

클래스의 정적 메서드를 참조한다.

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

// 메서드 레퍼런스
Function<String, Integer> parser2 = Integer::parseInt;

// 사용
List<String> numbers = List.of("1", "2", "3", "4", "5");
List<Integer> parsed = numbers.stream()
    .map(Integer::parseInt)
    .toList();

다양한 예시

// Math 클래스
Function<Double, Double> abs = Math::abs;
BinaryOperator<Integer> max = Math::max;
BinaryOperator<Integer> min = Math::min;

// Collections
Comparator<Integer> naturalOrder = Comparator::naturalOrder;

// 직접 정의한 정적 메서드
public class StringUtils {
    public static boolean isBlank(String s) {
        return s == null || s.trim().isEmpty();
    }
}

Predicate<String> blankCheck = StringUtils::isBlank;

2. 특정 객체의 인스턴스 메서드 참조

instance::instanceMethodName

이미 존재하는 특정 객체의 인스턴스 메서드를 참조한다.

// 람다
Consumer<String> printer1 = s -> System.out.println(s);

// 메서드 레퍼런스
Consumer<String> printer2 = System.out::println;

// 사용
List.of("A", "B", "C").forEach(System.out::println);

다양한 예시

// StringBuilder 인스턴스
StringBuilder sb = new StringBuilder();
Consumer<String> appender = sb::append;

List.of("Hello", " ", "World").forEach(sb::append);
System.out.println(sb);  // "Hello World"

// 특정 인스턴스의 메서드
String prefix = "Mr. ";
Function<String, String> addPrefix = prefix::concat;
System.out.println(addPrefix.apply("Kim"));  // "Mr. Kim"

// Comparator 사용
Collator collator = Collator.getInstance(Locale.KOREAN);
List<String> names = List.of("가", "나", "다");
names.stream()
    .sorted(collator::compare)
    .forEach(System.out::println);

3. 임의 객체의 인스턴스 메서드 참조

ClassName::instanceMethodName

특정 타입의 임의 객체에 대해 인스턴스 메서드를 참조한다. 첫 번째 파라미터가 메서드를 호출하는 객체가 된다.

// 람다
Function<String, Integer> len1 = s -> s.length();
BiFunction<String, String, Boolean> eq1 = (s1, s2) -> s1.equals(s2);

// 메서드 레퍼런스
Function<String, Integer> len2 = String::length;
BiPredicate<String, String> eq2 = String::equals;

// 사용
List<String> words = List.of("apple", "banana", "cherry");
List<Integer> lengths = words.stream()
    .map(String::length)
    .toList();  // [5, 6, 6]

람다로 변환하면?

// String::length 는 다음과 동일
(String s) -> s.length()

// String::compareTo 는 다음과 동일
(String s1, String s2) -> s1.compareTo(s2)

// String::equals 는 다음과 동일
(String s1, Object s2) -> s1.equals(s2)

정렬 예시

List<String> names = Arrays.asList("Charlie", "Alice", "Bob");

// 람다 
names.sort((s1, s2) -> s1.compareTo(s2));

// 메서드 레퍼런스
names.sort(String::compareTo);

// 대소문자 무시
names.sort(String::compareToIgnoreCase);

4. 생성자 참조

ClassName::new

생성자를 참조한다. 함수형 인터페이스의 파라미터에 따라 적절한 생성자가 선택된다.

// 람다
Supplier<List<String>> supplier1 = () -> new ArrayList<>();
Function<Integer, List<String>> supplier2 = size -> new ArrayList<>(size);

// 메서드 레퍼런스
Supplier<List<String>> supplier3 = ArrayList::new;       // 기본 생성자
Function<Integer, List<String>> supplier4 = ArrayList::new;  // 파라미터 1개 생성자

객체 생성

@AllArgsConstructor
@Data
public class Person {
    private String name;
    private int age;
}

// 람다
BiFunction<String, Integer, Person> creator1 = (name, age) -> new Person(name, age);

// 메서드 레퍼런스
BiFunction<String, Integer, Person> creator2 = Person::new;

// Stream에서 활용
List<String> names = List.of("Kim", "Lee", "Park");
List<Person> persons = names.stream()
    .map(name -> new Person(name, 0))  // 람다 필요 (추가 로직)
    .toList();

배열 생성자 참조

// 람다
IntFunction<String[]> arrayCreator1 = size -> new String[size];

// 메서드 레퍼런스
IntFunction<String[]> arrayCreator2 = String[]::new;

// Stream을 배열로 변환
String[] array = Stream.of("a", "b", "c")
    .toArray(String[]::new);

Stream API와 메서드 레퍼런스

map

List<String> names = List.of("alice", "bob", "charlie");

// 대문자 변환
List<String> upper = names.stream()
    .map(String::toUpperCase)
    .toList();

// 길이 추출
List<Integer> lengths = names.stream()
    .map(String::length)
    .toList();

filter

List<String> items = List.of("", "apple", null, "banana", "  ");

// null이 아닌 것만
List<String> nonNull = items.stream()
    .filter(Objects::nonNull)
    .toList();

// 빈 문자열 제외
List<String> nonEmpty = items.stream()
    .filter(Objects::nonNull)
    .filter(Predicate.not(String::isEmpty))
    .toList();

sorted

List<String> words = List.of("Banana", "apple", "Cherry");

// 자연 정렬
words.stream().sorted(String::compareTo);

// 대소문자 무시 정렬
words.stream().sorted(String::compareToIgnoreCase);

// 객체 정렬
List<Person> people = /* ... */;
people.stream()
    .sorted(Comparator.comparing(Person::getName))
    .toList();

forEach

List<String> items = List.of("A", "B", "C");

items.forEach(System.out::println);

// Map의 forEach
Map<String, Integer> map = Map.of("a", 1, "b", 2);
map.forEach((k, v) -> System.out.println(k + "=" + v));  // BiConsumer라 람다 필요

reduce

List<Integer> numbers = List.of(1, 2, 3, 4, 5);

// 합계
int sum = numbers.stream()
    .reduce(0, Integer::sum);

// 최대값
Optional<Integer> max = numbers.stream()
    .reduce(Integer::max);

// 문자열 연결
List<String> words = List.of("Hello", "World");
String joined = words.stream()
    .reduce("", String::concat);

collect

// toList (Java 16+)
List<String> list = stream.toList();

// joining
String result = stream.collect(Collectors.joining(", "));

// groupingBy
Map<String, List<Person>> byCity = people.stream()
    .collect(Collectors.groupingBy(Person::getCity));

Optional과 메서드 레퍼런스

Optional<String> optional = Optional.of("hello");

// map
Optional<Integer> length = optional.map(String::length);

// filter
Optional<String> filtered = optional.filter(String::isEmpty);

// ifPresent
optional.ifPresent(System.out::println);

// orElseGet
String value = optional.orElseGet(String::new);

// or (Java 9+)
Optional<String> result = optional.or(() -> Optional.of("default"));

Comparator와 메서드 레퍼런스

@Data
public class Employee {
    private String name;
    private String department;
    private int salary;
}

List<Employee> employees = /* ... */;

// 이름순 정렬
employees.sort(Comparator.comparing(Employee::getName));

// 급여 역순 정렬
employees.sort(Comparator.comparing(Employee::getSalary).reversed());

// 부서 → 급여순 복합 정렬
employees.sort(
    Comparator.comparing(Employee::getDepartment)
              .thenComparing(Employee::getSalary)
);

// null 처리
employees.sort(
    Comparator.comparing(Employee::getName,
        Comparator.nullsLast(Comparator.naturalOrder()))
);

메서드 레퍼런스 사용 불가 케이스

1. 추가 로직이 필요한 경우

// 람다 필요 - 추가 연산
list.stream()
    .map(s -> s.length() + 1)  // 메서드 레퍼런스 불가
    .toList();

// 람다 필요 - 조건부 로직
list.stream()
    .filter(s -> s != null && s.length() > 3)
    .toList();

2. 파라미터 순서가 다른 경우

// 람다 필요
BiFunction<String, String, String> concat = (a, b) -> b.concat(a);
// String::concat은 (a, b) -> a.concat(b)

3. 인스턴스 생성 시 추가 설정

// 람다 필요
Supplier<List<String>> supplier = () -> {
    List<String> list = new ArrayList<>();
    list.add("default");
    return list;
};

this와 super 메서드 레퍼런스

public class Parent {
    public void greet() {
        System.out.println("Hello from Parent");
    }
}

public class Child extends Parent {

    @Override
    public void greet() {
        System.out.println("Hello from Child");
    }

    public void demo() {
        Runnable r1 = this::greet;   // Child의 greet
        Runnable r2 = super::greet;  // Parent의 greet

        r1.run();  // "Hello from Child"
        r2.run();  // "Hello from Parent"
    }
}

실전 예제

DTO 변환

@Data
@AllArgsConstructor
public class UserDto {
    private String name;
    private String email;

    public static UserDto from(User user) {
        return new UserDto(user.getName(), user.getEmail());
    }
}

// 엔티티 → DTO 변환
List<UserDto> dtos = users.stream()
    .map(UserDto::from)
    .toList();

Validator

public class Validators {
    public static boolean isValidEmail(String email) {
        return email != null && email.contains("@");
    }

    public static boolean isNotBlank(String s) {
        return s != null && !s.trim().isEmpty();
    }
}

// 유효성 검사
boolean allValid = users.stream()
    .map(User::getEmail)
    .allMatch(Validators::isValidEmail);

Builder 패턴과 함께

public interface EntityBuilder<T> {
    T build();
}

public <T> List<T> buildAll(List<EntityBuilder<T>> builders) {
    return builders.stream()
        .map(EntityBuilder::build)
        .toList();
}

정리

상황 람다 메서드 레퍼런스
기존 메서드 단순 호출 s -> s.length() String::length
정적 메서드 호출 s -> Integer.parseInt(s) Integer::parseInt
특정 객체 메서드 s -> out.println(s) System.out::println
생성자 호출 () -> new ArrayList<>() ArrayList::new
추가 로직 필요 s -> s.length() + 1 불가

선택 기준

1. 람다가 기존 메서드를 그대로 호출 → 메서드 레퍼런스
2. 추가 연산이나 조건이 필요 → 람다
3. 가독성이 더 좋은 쪽 선택

메서드 레퍼런스는 코드를 간결하게 만들지만, 때로는 람다가 더 명확할 수 있다. 팀의 코드 스타일과 가독성을 고려해 선택하자.