람다가 익명 클래스보다 나은 점 중에서 가장 큰 특징은 간결함이다. 그런데 자바에는 함수 객체를 심지어 람다보다도 더 간결하게 만드는 방법이 있으니, 바로 메서드 참조(method reference)
다.
메서드 참조를 사용하면 기존에 존재하는 메서드를 람다 표현식 대신 호출할 수 있어, 코드가 더 짧고 읽기 쉬워진다. 메서드 참조는 ::
기호를 사용해 작성된다.
자바 8에서는 Map.merge
메서드가 추가되어, 특정 키가 존재하지 않을 경우 해당 키에 대해 초기 값을 설정하고, 이미 존재하는 경우에는 값을 갱신하는 동작을 더 간결하게 표현할 수 있게 되었다.
예를 들어, 키가 맵에 없으면 {key, 1}
쌍을 저장하고, 이미 존재하면 해당 값에 1을 더한다. 람다 표현식을 활용하여 다음과 같이 작성할 수 있다
map.merge(key, 1, (count, incr) -> count + incr);
이 코드는 count
와 incr
두 매개변수를 더하는 간단한 연산을 수행한다. 하지만 자바 8부터 Integer
클래스에 추가된 sum
메서드를 메서드 참조로 이용하면 더 간결하게 작성할 수 있다.
map.merge(key, 1, Integer::sum);
메서드 참조는 두 매개변수를 더하는 기능을 명확하게 표현하며, 코드가 훨씬 깔끔해진다. 이처럼 람다식이 지나치게 길거나 복잡할 때는 메서드 참조가 좋은 대안이 될 수 있다.
-
메서드 참조는 람다에 비해 코드가 짧고 명확해질 때 사용하면 좋다. IDE는 람다를 메서드 참조로 대체할 것을 권장하는 경우가 많지만, 메서드 참조가 꼭 더 간결하거나 명확하지 않은 경우에는 람다 표현식을 유지하는 것이 더 좋을 때도 니다.
-
예를 들어, 클래스 내부에서 사용되는 코드가 있을 때 람다로 표현하는 것이 더 간결하고 명확할 수 있다.
service.execute(() -> action());
이 경우 메서드 참조로 표현하면 오히려 가독성이 떨어질 수 있다.
-
또한,
Function.identity()
같은 제네릭 팩터리 메서드보다(x -> x)
와 같은 짧은 람다 표현식이 더 명확할 수 있다.
메서드 참조는 다섯 가지 유형이 있으며, 각 유형은 해당 기능을 수행하는 람다 표현식으로 쉽게 대체할 수 있다.
유형 | 예시 | 같은 기능의 람다 표현식 |
---|---|---|
정적 메서드 참조 | Integer::parseInt |
str -> Integer.parseInt(str) |
한정적 인스턴스 메서드 참조 | Instant.now()::isAfter |
t -> Instant.now().isAfter(t) |
비한정적 인스턴스 메서드 참조 | String::toLowerCase |
str -> str.toLowerCase() |
클래스 생성자 참조 | TreeMap<K,V>::new |
() -> new TreeMap<K,V>() |
배열 생성자 참조 | int[]::new |
len -> new int[len] |
메서드 참조에는 정적 메서드 참조, 한정적 인스턴스 메서드 참조, 비한정적 인스턴스 메서드 참조, 클래스 생성자 참조, 그리고 배열 생성자 참조의 다섯 가지 유형이 있다.
정적 메서드는 특정 객체가 아닌 클래스 자체에 속한 메서드로, 객체를 생성하지 않고도 클래스 이름만으로 호출할 수 있다. 메서드 참조에서 정적 메서드를 참조하면, 메서드 이름을 람다 표현식처럼 사용할 수 있다.
Function<String, Integer> strToInt = Integer::parseInt;
// 같은 기능의 람다 표현식
Function<String, Integer> strToIntLambda = str -> Integer.parseInt(str);
// 사용 예시
Integer number = strToInt.apply("123"); // 결과: 123
설명:
Integer::parseInt
는parseInt
라는 정적 메서드를 참조하는 메서드 참조strToInt
는Function<String, Integer>
타입으로 문자열을 받아 정수로 변환한다.str -> Integer.parseInt(str)
람다 표현식과 동일하게 동작한다.
한정적 인스턴스 메서드 참조는 특정 객체에 바인딩된 인스턴스 메서드를 참조하는 방식이다. 즉, 참조 대상 객체(수신 객체)를 정해놓고 해당 객체의 메서드를 호출하는 방식이다.
{% hint style="info" %} 한정적 인스턴스 메서드 참조는 특정 객체의 메서드를 미리 참조하여, 이후에 그 객체의 메서드를 사용할 수 있도록 하는 방식이다. 말이 조금 복잡하게 들릴 수 있지만, 핵심은 특정한 객체를 지정한 상태에서 그 객체의 메서드를 참조하는 것이다. {% endhint %}
Instant now = Instant.now();
Predicate<Instant> isAfterNow = now::isAfter;
이 코드는 now
라는 Instant
객체에 isAfter
메서드를 미리 연결해둔 것이다. 즉, isAfterNow
는 항상 now
라는 특정 객체의 isAfter
메서드를 실행한다.
위 예제의 동작 방식을 하나하나 풀어보면 다음과 같다.
Instant now = Instant.now();
이 코드로now
라는 특정 시점(현재 시간)을 가지는Instant
객체를 생성한다.Predicate<Instant> isAfterNow = now::isAfter;
이 줄에서는now
객체의isAfter
메서드를 참조하는isAfterNow
라는 Predicate를 만든다.isAfterNow
는now
객체의isAfter
메서드를 참조하므로, 호출할 때마다 항상now
라는 객체와 비교하게 된다.
따라서 이
isAfterNow
는now
보다 미래의 시점인지 여부를 확인하는 역할을 한다.isAfterNow.test(Instant)
와 같이isAfterNow
에Instant
객체를 전달하면, 그 시점이now
이후인지 확인하는 것이다.
한정적 인스턴스 메서드 참조의 실제 동작 예시
Instant now = Instant.now(); // 현재 시간을 저장한 Instant 객체
// isAfterNow라는 Predicate를 생성합니다.
Predicate<Instant> isAfterNow = now::isAfter;
// 이후에 다른 시점을 비교할 때 사용
Instant oneMinuteLater = now.plusSeconds(60);
System.out.println(isAfterNow.test(oneMinuteLater)); // 결과: false (60초 뒤는 now보다 미래이기 때문)
여기서 isAfterNow
는 now::isAfter
로 지정된 한정적 메서드 참조로, isAfterNow.test(...)
를 호출할 때마다 now
가 정해진 기준 시간이다. now
보다 나중의 시간을 비교하는 로직이 필요한 곳에서 언제든지 isAfterNow
를 사용하여 now
시점과 비교할 수 있게 만들어준 것이다.
이런 방식으로 특정 객체와 메서드를 한 번에 묶어서 참조하는 것이 한정적 인스턴스 메서드 참조이다.
예제:
Instant now = Instant.now();
Predicate<Instant> isAfterNow = now::isAfter;
// 같은 기능의 람다 표현식
Predicate<Instant> isAfterNowLambda = t -> now.isAfter(t);
// 사용 예시
Instant futureTime = now.plusSeconds(60);
boolean result = isAfterNow.test(futureTime); // 결과: false (현재 시간보다 미래인지 확인)
설명:
now::isAfter
는now
객체의isAfter
인스턴스 메서드를 참조하는 메서드 참조isAfterNow
는Predicate<Instant>
타입으로, 특정Instant
값이now
보다 이후인지 검사하는 역할을 한다.t -> now.isAfter(t)
람다 표현식과 동일하게 동작한다.
비한정적 인스턴스 메서드 참조는 수신 객체가 특정되지 않은 상태로 인스턴스 메서드를 참조하는 방식이다. 참조된 메서드는 사용 시점에 객체를 전달받아 그 객체의 메서드를 호출하게 된다.
Function<String, String> toLowerCase = String::toLowerCase;
// 같은 기능의 람다 표현식
Function<String, String> toLowerCaseLambda = str -> str.toLowerCase();
// 사용 예시
String result = toLowerCase.apply("HELLO"); // 결과: "hello"
설명:
String::toLowerCase
는 비한정적 메서드 참조로, 실제 호출 시 인스턴스(여기선String
)가 제공toLowerCase
는Function<String, String>
타입으로, 문자열을 소문자로 변환str -> str.toLowerCase()
람다 표현식과 동일하게 동작한다.
클래스 생성자 참조는 클래스의 생성자를 참조하여 객체를 생성할 수 있게 해주는 방식이다. 주로 팩터리 메서드에서 사용할 수 있다.
예제:
Supplier<List<String>> listSupplier = ArrayList::new;
// 같은 기능의 람다 표현식
Supplier<List<String>> listSupplierLambda = () -> new ArrayList<>();
// 사용 예시
List<String> list = listSupplier.get(); // 결과: 빈 ArrayList 생성
설명:
ArrayList::new
는ArrayList
클래스의 생성자를 참조하여 빈ArrayList
를 생성하는 메서드 참조listSupplier
는Supplier<List<String>>
타입으로, 새로운ArrayList
인스턴스를 제공() -> new ArrayList<>()
람다 표현식과 동일하게 동작한다.
배열 생성자 참조는 배열을 생성할 수 있도록 배열 생성자를 참조하는 방식이다. 전달받은 크기만큼의 배열을 생성할 수 있다.
예제:
IntFunction<int[]> arrayCreator = int[]::new;
// 같은 기능의 람다 표현식
IntFunction<int[]> arrayCreatorLambda = len -> new int[len];
// 사용 예시
int[] array = arrayCreator.apply(5); // 결과: 길이 5의 int 배열 생성
설명:
int[]::new
는int
배열의 생성자를 참조하여 지정된 길이의 배열을 생성한다.arrayCreator
는IntFunction<int[]>
타입으로, 배열의 크기를 받아 그 크기만큼의 새로운 배열을 생성한다.len -> new int[len]
람다 표현식과 동일하게 동작
유형 | 예시 | 설명 |
---|---|---|
정적 메서드 참조 | Integer::parseInt |
특정 클래스의 정적 메서드를 참조. 객체가 필요 없으며 클래스 이름으로 호출 가능. |
한정적 인스턴스 메서드 참조 | now::isAfter |
특정 객체에 바인딩된 메서드를 참조. 객체가 이미 존재하며 해당 객체의 메서드를 참조함. |
비한정적 인스턴스 메서드 참조 | String::toLowerCase |
수신 객체가 특정되지 않은 상태로 인스턴스 메서드를 참조. 호출 시점에 객체를 전달받음. |
클래스 생성자 참조 | ArrayList::new |
클래스의 생성자를 참조하여 객체를 생성할 수 있음. 주로 팩터리 메서드로 사용됨. |
배열 생성자 참조 | int[]::new |
배열 생성자를 참조하여 지정된 길이의 배열을 생성. 함수형 인터페이스로 배열 생성 가능. |
람다로는 불가능하나 메서드 참조로는 가능한 유일한 예외는 제네릭 함수 타입(generic function type)입다. 자바는 제네릭 람다식을 지원하지 않기 때문에 제네릭 함수형 인터페이스의 구현은 메서드 참조로 표현할 수 있지만 람다로는 표현할 수 없다.
- 메서드 참조는 람다의 간단한 대안으로 사용할 수 있으며, 코드가 더 짧고 명확해질 때 유용하다.
- 그러나 메서드 참조가 오히려 가독성을 떨어뜨릴 수 있는 경우가 있으므로 상황에 따라 람다를 유지하는 것이 좋다.
- 메서드 참조와 람다 표현식은 코드의 간결성과 가독성을 높이기 위한 수단으로, 상황에 맞게 적절하게 선택하여 사용하는 것이 중요하다.
람다와 메서드 참조는 자바의 코드 가독성을 높이고, 함수형 프로그래밍을 효과적으로 지원하는 기능이다.