12.7 메서드 참조 (Method Reference)
메서드 참조(Method Reference) 는 이미 존재하는 메서드를 람다식처럼 전달할 수 있는 Java 8의 기능입니다. :: 연산자를 사용하며, 람다식보다 더 간결하고 읽기 쉬운 코드를 만들어줍니다.
1. 왜 메서드 참조인가?
// 람다식으로 출력
List<String> names = List.of("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println(name));
// 메서드 참조로 (훨씬 간결!)
names.forEach(System.out::println);
람다식 name -> System.out.println(name)은 그냥 println을 호출할 뿐입니다. 이처럼 람다가 단순히 기존 메서드를 호출만 할 때 메서드 참조로 대체할 수 있습니다.
2. 메서드 참조의 4가지 유형
유형 1: 정적(static) 메서드 참조
클래스명::정적메서드명
// 람다
Function<String, Integer> parser1 = s -> Integer.parseInt(s);
// 메서드 참조
Function<String, Integer> parser2 = Integer::parseInt;
System.out.println(parser2.apply("42")); // 42
// 스트림에서 활용
List<String> numberStrings = List.of("1", "2", "3", "4", "5");
List<Integer> numbers = numberStrings.stream()
.map(Integer::parseInt) // Integer.parseInt(s) 대신
.collect(Collectors.toList());
System.out.println(numbers); // [1, 2, 3, 4, 5]
// 다른 예시
List<String> mixed = List.of("hello", "WORLD", "Java");
mixed.stream()
.map(String::valueOf) // 정적 메서드
.forEach(System.out::println);
유형 2: 특정 객체의 인스턴스 메서드 참조
인스턴스변수::인스턴스메서드명
String prefix = "Hello, ";
// 람다
Function<String, String> greeter1 = name -> prefix.concat(name);
// 메서드 참조 (특정 인스턴스 prefix의 concat 메서드)
Function<String, String> greeter2 = prefix::concat;
System.out.println(greeter2.apply("Java")); // Hello, Java
// 실전: 특정 객체로 필터링
List<String> fruits = List.of("apple", "banana", "apricot", "cherry");
String keyword = "ap";
long count = fruits.stream()
.filter(keyword::startsWith) // s -> keyword.startsWith(s) 아님!
// 주의: keyword::startsWith → s -> keyword.startsWith(s) (keyword가 receiver)
.count();
// 더 명확한 예
long countStartWith = fruits.stream()
.filter(f -> f.startsWith("a")) // 람다로 직접 쓰는 게 더 명확할 때도 있음
.count();
System.out.println(countStartWith); // 2 (apple, apricot)
유형 3: 임의 객체의 인스턴스 메서드 참조
클래스명::인스턴스메서드명
람다의 첫 번째 매개변수가 메서드를 호출하는 객체가 됩니다.
// 람다
Function<String, String> upper1 = s -> s.toUpperCase();
Function<String, Integer> len1 = s -> s.length();
Predicate<String> empty1 = s -> s.isEmpty();
// 메서드 참조 (s가 receiver)
Function<String, String> upper2 = String::toUpperCase;
Function<String, Integer> len2 = String::length;
Predicate<String> empty2 = String::isEmpty;
BiPredicate<String, String> starts = String::startsWith; // (s1, s2) -> s1.startsWith(s2)
System.out.println(upper2.apply("hello")); // HELLO
System.out.println(starts.test("hello", "he")); // true
// 스트림에서 가장 자주 쓰이는 형태
List<String> words = List.of("Hello", "World", "Java", "");
words.stream()
.filter(Predicate.not(String::isEmpty)) // isEmpty()가 false인 것
.map(String::toLowerCase)
.sorted(String::compareTo)
.forEach(System.out::println);
// hello, java, world
// Comparator로 정렬
List<String> names = new ArrayList<>(List.of("Charlie", "Alice", "Bob"));
names.sort(String::compareToIgnoreCase); // (a, b) -> a.compareToIgnoreCase(b)
System.out.println(names); // [Alice, Bob, Charlie]
유형 4: 생성자 참조
클래스명::new
// 람다
Supplier<ArrayList<String>> listMaker1 = () -> new ArrayList<>();
Function<String, StringBuilder> sbMaker1 = s -> new StringBuilder(s);
// 생성자 참조
Supplier<ArrayList<String>> listMaker2 = ArrayList::new;
Function<String, StringBuilder> sbMaker2 = StringBuilder::new;
List<String> newList = listMaker2.get();
StringBuilder sb = sbMaker2.apply("Hello");
// 스트림에서 toList 대신 특정 구현체로 수집
List<String> words = List.of("apple", "banana", "cherry");
ArrayList<String> arrayList = words.stream()
.collect(Collectors.toCollection(ArrayList::new));
LinkedList<String> linkedList = words.stream()
.collect(Collectors.toCollection(LinkedList::new));
// 배열 생성자 참조
Function<Integer, String[]> arrayMaker = String[]::new;
String[] arr = arrayMaker.apply(5); // new String[5]
System.out.println(arr.length); // 5
// Stream.toArray에서 활용
String[] result = words.stream()
.filter(s -> s.length() > 5)
.toArray(String[]::new); // new String[n] 자동 처리
System.out.println(Arrays.toString(result)); // [banana, cherry]
3. 종합 예제: 메서드 참조로 리팩토링
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
record Person(String name, int age, String city) {
static Person of(String name, int age, String city) {
return new Person(name, age, city);
}
boolean isAdult() { return age >= 18; }
String greeting() { return "안녕하세요, " + name + "입니다!"; }
}
public class MethodRefDemo {
public static void main(String[] args) {
List<Person> people = List.of(
Person.of("Alice", 25, "서울"),
Person.of("Bob", 16, "부산"),
Person.of("Charlie", 30, "서울"),
Person.of("Dave", 14, "대구"),
Person.of("Eve", 22, "서울")
);
// ① 성인만 필터링 (임의 인스턴스 메서드 참조)
people.stream()
.filter(Person::isAdult) // p -> p.isAdult()
.map(Person::greeting) // p -> p.greeting()
.forEach(System.out::println);
System.out.println("---");
// ② 이름만 추출하여 알파벳 순 정렬
people.stream()
.map(Person::name) // p -> p.name()
.sorted(String::compareTo) // (a,b) -> a.compareTo(b)
.forEach(System.out::println);
System.out.println("---");
// ③ 나이로 최솟값 찾기
people.stream()
.min(Comparator.comparingInt(Person::age)) // p -> p.age()
.ifPresent(p -> System.out.println("최연소: " + p.name()));
}
}
고수 팁
메서드 참조 vs 람다: 언제 무엇을 쓸까?
- 메서드 참조: 람다 본문이 기존 메서드 하나만 호출할 때 → 더 읽기 쉬움
- 람다: 인자를 변환하거나, 여러 작업을 하거나, 조건이 복잡할 때 → 람다가 더 명확
// 메서드 참조가 더 명확한 경우
.map(String::toUpperCase) // ✅ vs s -> s.toUpperCase()
.filter(Objects::nonNull) // ✅ vs s -> s != null
.sorted(Comparator.naturalOrder()) // ✅
// 람다가 더 명확한 경우
.filter(s -> s.length() > 3 && s.startsWith("A")) // 조건이 복잡
.map(s -> "[" + s + "]") // 변환 로직 포함