Java 메소드 레퍼런스(Method Reference) 정리
1. 개요
- 형태:
대상 :: 메소드명 또는 클래스명 :: new(생성자), 타입[] :: new(배열 생성자)
- 람다식과 동일한 의미를 더 간결히 작성하는 문법 설탕(syntactic sugar). 실제로는 람다와 같이
invokedynamic을 통해 SAM 구현이 생성됨.
- 중요: 정적 메소드만 가능한 것이 아님. 정적/인스턴스/생성자/배열 생성자 모두 가능.
- 사용 전제: 참조하려는 메소드 시그니처가 대상 함수형 인터페이스의 단일 추상 메소드(SAM) 시그니처와 호환되어야 함.
2. 종류와 형태
| 분류 | 형태 | 의미 | 예시 |
|---|
| 정적 메소드 참조 | ClassName::staticMethod | 클래스의 정적 메소드를 참조 | Function<String,Integer> f = Integer::parseInt; |
| 특정 객체의 인스턴스 메소드 참조 (bound) | instance::method | 이미 가진 객체의 메소드를 참조 | Consumer<String> c = System.out::println; |
| 특정 타입의 임의 객체 인스턴스 메소드 참조 (unbound) | ClassName::instanceMethod | 첫 번째 매개변수가 수신 객체가 됨 | BiPredicate<String,String> p = String::equals; |
| 생성자 참조 | ClassName::new | 생성자를 참조하여 새 객체 생성 | Supplier<ArrayList<String>> s = ArrayList::new; |
| 배열 생성자 참조 | Type[]::new | N 크기의 배열 생성자 참조 | IntFunction<String[]> a = String[]::new; |
unbound 형태 예: Function<String,Integer> f = String::length; 는 s -> s.length() 와 동일.
3. 예시 코드로 이해하기
3.1 정적 메소드 참조
System.out.println(Integer.parseInt("123"));
Function<String, Integer> func = Integer::parseInt;
System.out.println(func.apply("1234"));
3.2 인스턴스 메소드 참조
List<String> names = List.of("배두훈","강형호","조민규","배두훈");
names.stream().forEach(name -> System.out.println(name));
names.stream().forEach(System.out::println);
3.3 생성자 참조
names.stream().forEach(name -> new Member(name));
names.stream().forEach(Member::new);
List<Member> members = names.stream()
.map(Member::new)
.collect(Collectors.toList());
System.out.println(members);
보조 클래스:
class Member {
String name;
Member(String name){ this.name = name; }
}
4. 람다 → 메소드 레퍼런스 매핑 예
| 람다식 | 메소드 레퍼런스 |
|---|
s -> Integer.parseInt(s) | Integer::parseInt |
x -> System.out.println(x) | System.out::println |
(a,b) -> a.equals(b) | String::equals |
() -> new ArrayList<>() | ArrayList::new |
n -> new Member(n) | Member::new |
len -> new String[len] | String[]::new |
5. 오버로드/제네릭과 타입 추론
- 오버로드 주의: 동일한 이름의 메소드가 여러 개일 때, 대상 SAM의 시그니처로 하나로 귀결되어야 함. 불분명하면 형변환(cast) 로 명시.
Function<Integer,String> f = (Function<Integer,String>) String::valueOf;
- 제네릭: 컴파일러가 타입을 추론하지만, 복잡한 체이닝에서는 타입 인자를 명시하거나 중간 변수로 분리하면 오류를 줄일 수 있음.
6. Stream과 함께 쓰는 빈출 패턴
- 출력:
list.forEach(System.out::println)
- 변환:
map(String::trim), map(Object::toString)
- 정렬 키추출:
sorted(Comparator.comparing(String::length))
- 수집기 생성자:
collect(Collectors.toCollection(ArrayList::new))
- 맵 수집:
collect(Collectors.toMap(User::id, User::name, (a,b)->a, LinkedHashMap::new))
- 비교자:
Comparator<String> cmp = String::compareToIgnoreCase; → sorted(cmp)
7. 함수형 인터페이스와 시그니처 호환 표
| 인터페이스 | SAM 시그니처 | 가능한 예시 메소드 참조 |
|---|
Supplier<R> | R get() | ArrayList::new, Locale::getDefault |
Consumer<T> | void accept(T) | System.out::println, Logger::info |
Function<T,R> | R apply(T) | Integer::parseInt, String::trim |
BiFunction<T,U,R> | R apply(T,U) | Math::max, String::replace |
Predicate<T> | boolean test(T) | Objects::isNull, String::isEmpty |
BiPredicate<T,U> | boolean test(T,U) | String::equals, Pattern::matches |
UnaryOperator<T> | T apply(T) | String::toLowerCase |
BinaryOperator<T> | T apply(T,T) | BigInteger::add |
8. this/super 메소드 레퍼런스 (심화)
this::instanceMethod : 현재 인스턴스의 메소드 참조
super::instanceMethod : 상위 타입의 메소드 참조 (내부/익명 클래스 등에서 유용)
class Base { void greet() { System.out.println("base"); } }
class Child extends Base {
void greet() { System.out.println("child"); }
void run() {
Runnable r1 = this::greet;
Runnable r2 = super::greet;
r1.run(); r2.run();
}
}
9. 제약과 주의사항
- 부분 적용(partial application) 불가: 필요한 매개변수를 일부만 채워둘 수 없음.
- 체크 예외: 참조 메소드가 체크 예외를 던지면, 대상 인터페이스가 그 예외를 던질 수 있어야 함.
- 부작용 최소화: 메소드 레퍼런스 자체는 간결하지만, 참조 대상 메소드가 부작용을 가지면 Stream 파이프라인의 예측 가능성이 떨어짐.
- 가독성 우선: 모든 람다를 메소드 레퍼런스로 바꿔야 하는 것은 아님. 의미가 더 명확할 때에만 사용.
10. 요약
- 메소드 레퍼런스는 람다식을 더 간단하게 표현하는 문법.
- 정적/인스턴스/생성자/배열 생성자 참조를 지원.
- 핵심은 함수형 인터페이스의 시그니처와의 호환. 모호하면 형변환으로 명시.
- Stream과 함께 사용할 때 코드 가독성과 재사용성이 향상.