1. 소개
Method reference(메서드 참조)는 Java에서 기존에 존재하는 메서드(또는 생성자)를 함수형 인터페이스 구현으로 전달할 수 있게 해 주는 특별한 문법입니다. 시그니처가 일치한다면, 메서드를 람다식이 기대되는 곳에 “전달”할 수 있습니다.
문법:
Klass::metod
obekt::metod
Klass::new
람다가 “즉석에서 만든 작은 함수”라면, method reference는 “이미 있는 메서드를 그대로 전달”하는 것입니다. 마치 레시피를 다시 쓰는 대신 레시피 페이지 링크를 건네는 것과 비슷합니다.
간단한 예
다음과 같이 쓰는 대신:
list.forEach(s -> System.out.println(s));
이렇게 할 수 있습니다:
list.forEach(System.out::println);
더 간결하죠?
2. method reference의 종류
메서드 참조에는 네 가지 기본 형태가 있습니다. 이것만 알면 충분합니다.
정적 메서드 참조
문법: Klass::staticheskiyMetod
예:
Function<Integer, String> intToString = String::valueOf;
System.out.println(intToString.apply(123)); // "123"
람다로 작성하면 동일합니다:
Function<Integer, String> intToString = i -> String.valueOf(i);
객체의 인스턴스 메서드 참조
문법: obekt::metod
예:
PrintStream printer = System.out;
Consumer<String> consumer = printer::println;
consumer.accept("안녕, 세상!");
람다와 동등합니다:
Consumer<String> consumer = s -> printer.println(s);
클래스의 인스턴스 메서드 참조
문법: Klass::metod
여기서는 함수형 인터페이스의 첫 번째 매개변수가 메서드를 호출할 대상 객체가 됩니다.
예:
Function<String, Integer> stringLength = String::length;
System.out.println(stringLength.apply("Java")); // 4
여기서 String::length는 다음과 같은 함수로 변환됩니다: (String s) -> s.length()
생성자 참조
문법: Klass::new
예:
Supplier<ArrayList<String>> listSupplier = ArrayList::new;
ArrayList<String> list = listSupplier.get();
람다와 동등합니다:
Supplier<ArrayList<String>> listSupplier = () -> new ArrayList<>();
3. method reference를 언제 사용하나요?
람다가 추가 로직 없이 기존 메서드를 그대로 호출할 때 method reference를 쓰면 코드가 더 짧고 읽기 쉬워집니다.
예: 리스트 정렬
다음과 같이 쓰는 대신:
List<String> names = Arrays.asList("이반", "표트르", "안나");
names.sort((a, b) -> a.compareToIgnoreCase(b));
이렇게 할 수 있습니다:
names.sort(String::compareToIgnoreCase);
예: 컬렉션 처리
다음과 같이 쓰는 대신:
list.forEach(s -> System.out.println(s));
아주 간단하게:
list.forEach(System.out::println);
예: 요소 변환
다음과 같이 쓰는 대신:
List<String> numbers = Arrays.asList("1", "2", "3");
List<Integer> ints = numbers.stream()
.map(s -> Integer.parseInt(s))
.collect(Collectors.toList());
다음과 같이 할 수 있습니다:
List<Integer> ints = numbers.stream()
.map(Integer::parseInt)
.collect(Collectors.toList());
Stream API와 유틸리티 클래스 Collectors에 대해서는 레벨 30에서 자세히 배우게 됩니다 :P
4. method reference와 람다식 비교
동등성
method reference와 람다식은 자주 상호 대체 가능합니다. 시그니처가 일치하면 둘 다 함수형 인터페이스를 구현합니다.
예:
Consumer<String> c1 = s -> System.out.println(s);
Consumer<String> c2 = System.out::println;
method reference를 쓰기 좋은 때
- 람다가 추가 로직 없이 기존 메서드를 그대로 호출할 때.
- 특히 긴 메서드 체인에서 가독성을 높이고 싶을 때.
- “여기는 단순히 메서드 호출임”을 명시적으로 보여 주고 싶을 때.
method reference가 맞지 않을 때
- 추가 로직(검증, 조건, 오류 처리 등)이 필요할 때.
- 메서드를 호출하기 전에 매개변수를 변환해야 할 때.
예:
list.forEach(s -> {
if (s != null) System.out.println(s);
});
// 여기서는 method reference가 맞지 않으므로 람다만 가능합니다.
5. 실습: 람다식을 method reference로 바꿔 보기
예 1: 동물 이름 출력
List<String> animals = Arrays.asList("고양이", "개", "앵무새");
animals.forEach(animal -> System.out.println(animal));
다음과 같이 바뀝니다:
animals.forEach(System.out::println);
예 2: 문자열을 숫자로 변환
List<String> numbers = Arrays.asList("10", "20", "30");
List<Integer> ints = numbers.stream()
.map(s -> Integer.parseInt(s))
.collect(Collectors.toList());
다음과 같이 바뀝니다:
List<Integer> ints = numbers.stream()
.map(Integer::parseInt)
.collect(Collectors.toList());
예 3: 이름으로 객체 정렬
List<Animal> animalList = ...;
animalList.sort((a, b) -> a.getName().compareTo(b.getName()));
다음과 같이 바뀝니다:
animalList.sort(Comparator.comparing(Animal::getName));
여기서 Animal::getName은 클래스의 인스턴스 메서드에 대한 참조입니다.
예 4: 생성자를 통한 객체 생성
Supplier<Dog> dogFactory = () -> new Dog();
Dog dog = dogFactory.get();
다음과 같이 바뀝습니다:
Supplier<Dog> dogFactory = Dog::new;
Dog dog = dogFactory.get();
6. 시그니처 매핑은 어떻게 동작하나
method reference는 메서드의 시그니처가 함수형 인터페이스의 추상 메서드와 일치할 때에만 사용할 수 있습니다.
@FunctionalInterface
interface IntToString {
String convert(int value);
}
public class Demo {
public static String intToHex(int value) {
return Integer.toHexString(value);
}
public static void main(String[] args) {
IntToString converter = Demo::intToHex;
System.out.println(converter.convert(255)); // ff
}
}
여기서 Demo::intToHex가 맞는 이유는 int를 받고 String을 반환하기 때문입니다.
7. 매개변수가 있는 생성자와 method reference
생성자가 매개변수를 받더라도, 시그니처가 일치하면 method reference를 사용할 수 있습니다.
@FunctionalInterface
interface AnimalFactory {
Animal create(String name);
}
class Animal {
private String name;
public Animal(String name) { this.name = name; }
public String getName() { return name; }
}
AnimalFactory factory = Animal::new;
Animal cat = factory.create("바르식");
System.out.println(cat.getName()); // 바르식
8. method reference 사용 시 흔한 실수
오류 1: 시그니처 불일치.
메서드 시그니처가 인터페이스의 추상 메서드와 일치하지 않으면 컴파일 오류가 발생합니다. 예를 들어 인터페이스는 두 개의 매개변수를 기대하는데, 참조가 한 개의 매개변수를 받는 메서드를 가리키는 경우입니다.
오류 2: 객체 없이 클래스의 인스턴스 메서드 참조 사용.
Klass::metod 형태를 사용할 때, 인터페이스의 첫 번째 매개변수는 호출 대상 객체가 됩니다. 매개변수의 개수나 순서를 혼동하면 매핑 오류가 발생합니다.
오류 3: 추가 로직이 필요한 곳에서 method reference 사용.
조건, 로깅, 예외 처리 등이 필요하다면 메서드 참조가 아니라 람다를 사용하세요.
오류 4: 오버로드된 메서드에 대한 참조.
클래스에 동일한 이름의 메서드가 여러 개 있으면, 컴파일러가 어떤 메서드를 선택해야 할지 모를 수 있습니다. 이럴 때는 함수형 인터페이스의 타입을 명시하는 것이 도움이 되기도 합니다.
오류 5: 객체 없이 인스턴스 메서드 참조 사용.
예를 들어 String::toUpperCase는 map에서 올바르게 동작합니다. 첫 번째 매개변수가 String 객체 자신이기 때문입니다. 그러나 적절한 컨텍스트 없이(정적 메서드를 기대하는 자리에서) 사용하면 오류가 발생합니다.
GO TO FULL VERSION