Skip to content

Latest commit

 

History

History
222 lines (145 loc) · 12.6 KB

File metadata and controls

222 lines (145 loc) · 12.6 KB

item 43 : 람다보다는 메서드 참조를 사용하라

1. 메서드 참조

1) 메서드 참조란?

람다가 익명 클래스보다 나은 점 중에서 가장 큰 특징은 간결함이다. 그런데 자바에는 함수 객체를 심지어 람다보다도 더 간결하게 만드는 방법이 있으니, 바로 메서드 참조(method reference)다.

메서드 참조를 사용하면 기존에 존재하는 메서드를 람다 표현식 대신 호출할 수 있어, 코드가 더 짧고 읽기 쉬워진다. 메서드 참조는 :: 기호를 사용해 작성된다.

2) Map.merge 메서드와 람다 표현식

자바 8에서는 Map.merge 메서드가 추가되어, 특정 키가 존재하지 않을 경우 해당 키에 대해 초기 값을 설정하고, 이미 존재하는 경우에는 값을 갱신하는 동작을 더 간결하게 표현할 수 있게 되었다.

예를 들어, 키가 맵에 없으면 {key, 1} 쌍을 저장하고, 이미 존재하면 해당 값에 1을 더한다. 람다 표현식을 활용하여 다음과 같이 작성할 수 있다

map.merge(key, 1, (count, incr) -> count + incr);

이 코드는 countincr 두 매개변수를 더하는 간단한 연산을 수행한다. 하지만 자바 8부터 Integer 클래스에 추가된 sum 메서드를 메서드 참조로 이용하면 더 간결하게 작성할 수 있다.

map.merge(key, 1, Integer::sum);

메서드 참조는 두 매개변수를 더하는 기능을 명확하게 표현하며, 코드가 훨씬 깔끔해진다. 이처럼 람다식이 지나치게 길거나 복잡할 때는 메서드 참조가 좋은 대안이 될 수 있다.

3) 메서드 참조와 람다 표현식의 장단점

  • 메서드 참조는 람다에 비해 코드가 짧고 명확해질 때 사용하면 좋다. IDE는 람다를 메서드 참조로 대체할 것을 권장하는 경우가 많지만, 메서드 참조가 꼭 더 간결하거나 명확하지 않은 경우에는 람다 표현식을 유지하는 것이 더 좋을 때도 니다.

  • 예를 들어, 클래스 내부에서 사용되는 코드가 있을 때 람다로 표현하는 것이 더 간결하고 명확할 수 있다.

    service.execute(() -> action());

    이 경우 메서드 참조로 표현하면 오히려 가독성이 떨어질 수 있다.

  • 또한, Function.identity() 같은 제네릭 팩터리 메서드보다 (x -> x)와 같은 짧은 람다 표현식이 더 명확할 수 있다.

2. 메서드 참조의 다섯 가지 유형

메서드 참조는 다섯 가지 유형이 있으며, 각 유형은 해당 기능을 수행하는 람다 표현식으로 쉽게 대체할 수 있다.

유형 예시 같은 기능의 람다 표현식
정적 메서드 참조 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]

메서드 참조에는 정적 메서드 참조, 한정적 인스턴스 메서드 참조, 비한정적 인스턴스 메서드 참조, 클래스 생성자 참조, 그리고 배열 생성자 참조의 다섯 가지 유형이 있다.

1) 정적 메서드 참조 (Static Method Reference)

정적 메서드는 특정 객체가 아닌 클래스 자체에 속한 메서드로, 객체를 생성하지 않고도 클래스 이름만으로 호출할 수 있다. 메서드 참조에서 정적 메서드를 참조하면, 메서드 이름을 람다 표현식처럼 사용할 수 있다.

Function<String, Integer> strToInt = Integer::parseInt;
// 같은 기능의 람다 표현식
Function<String, Integer> strToIntLambda = str -> Integer.parseInt(str);

// 사용 예시
Integer number = strToInt.apply("123"); // 결과: 123

설명:

  • Integer::parseIntparseInt라는 정적 메서드를 참조하는 메서드 참조
  • strToIntFunction<String, Integer> 타입으로 문자열을 받아 정수로 변환한다.
  • str -> Integer.parseInt(str) 람다 표현식과 동일하게 동작한다.

2) 한정적 인스턴스 메서드 참조 (Bound Instance Method Reference)

한정적 인스턴스 메서드 참조특정 객체에 바인딩된 인스턴스 메서드를 참조하는 방식이다. 즉, 참조 대상 객체(수신 객체)를 정해놓고 해당 객체의 메서드를 호출하는 방식이다.

{% hint style="info" %} 한정적 인스턴스 메서드 참조는 특정 객체의 메서드를 미리 참조하여, 이후에 그 객체의 메서드를 사용할 수 있도록 하는 방식이다. 말이 조금 복잡하게 들릴 수 있지만, 핵심은 특정한 객체를 지정한 상태에서 그 객체의 메서드를 참조하는 것이다. {% endhint %}

Instant now = Instant.now();
Predicate<Instant> isAfterNow = now::isAfter;

이 코드는 now라는 Instant 객체에 isAfter 메서드를 미리 연결해둔 것이다. 즉, isAfterNow항상 now라는 특정 객체의 isAfter 메서드를 실행한다.

위 예제의 동작 방식을 하나하나 풀어보면 다음과 같다.

  1. Instant now = Instant.now();
    이 코드로 now라는 특정 시점(현재 시간)을 가지는 Instant 객체를 생성한다.
  2. Predicate<Instant> isAfterNow = now::isAfter;
    이 줄에서는 now 객체의 isAfter 메서드를 참조하는 isAfterNow라는 Predicate를 만든다.
  3. isAfterNownow 객체의 isAfter 메서드를 참조하므로, 호출할 때마다 항상 now라는 객체와 비교하게 된다.

따라서 이 isAfterNownow보다 미래의 시점인지 여부를 확인하는 역할을 한다. isAfterNow.test(Instant)와 같이 isAfterNowInstant 객체를 전달하면, 그 시점이 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보다 미래이기 때문)

여기서 isAfterNownow::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::isAfternow 객체의 isAfter 인스턴스 메서드를 참조하는 메서드 참조
  • isAfterNowPredicate<Instant> 타입으로, 특정 Instant 값이 now보다 이후인지 검사하는 역할을 한다.
  • t -> now.isAfter(t) 람다 표현식과 동일하게 동작한다.

3) 비한정적 인스턴스 메서드 참조 (Unbound Instance Method Reference)

비한정적 인스턴스 메서드 참조수신 객체가 특정되지 않은 상태로 인스턴스 메서드를 참조하는 방식이다. 참조된 메서드는 사용 시점에 객체를 전달받아 그 객체의 메서드를 호출하게 된다.

Function<String, String> toLowerCase = String::toLowerCase;
// 같은 기능의 람다 표현식
Function<String, String> toLowerCaseLambda = str -> str.toLowerCase();

// 사용 예시
String result = toLowerCase.apply("HELLO"); // 결과: "hello"

설명:

  • String::toLowerCase는 비한정적 메서드 참조로, 실제 호출 시 인스턴스(여기선 String)가 제공
  • toLowerCaseFunction<String, String> 타입으로, 문자열을 소문자로 변환
  • str -> str.toLowerCase() 람다 표현식과 동일하게 동작한다.

4) 클래스 생성자 참조 (Class Constructor Reference)

클래스 생성자 참조클래스의 생성자를 참조하여 객체를 생성할 수 있게 해주는 방식이다. 주로 팩터리 메서드에서 사용할 수 있다.

예제:

Supplier<List<String>> listSupplier = ArrayList::new;
// 같은 기능의 람다 표현식
Supplier<List<String>> listSupplierLambda = () -> new ArrayList<>();

// 사용 예시
List<String> list = listSupplier.get(); // 결과: 빈 ArrayList 생성

설명:

  • ArrayList::newArrayList 클래스의 생성자를 참조하여 빈 ArrayList를 생성하는 메서드 참조
  • listSupplierSupplier<List<String>> 타입으로, 새로운 ArrayList 인스턴스를 제공
  • () -> new ArrayList<>() 람다 표현식과 동일하게 동작한다.

5) 배열 생성자 참조 (Array Constructor Reference)

배열 생성자 참조배열을 생성할 수 있도록 배열 생성자를 참조하는 방식이다. 전달받은 크기만큼의 배열을 생성할 수 있다.

예제:

IntFunction<int[]> arrayCreator = int[]::new;
// 같은 기능의 람다 표현식
IntFunction<int[]> arrayCreatorLambda = len -> new int[len];

// 사용 예시
int[] array = arrayCreator.apply(5); // 결과: 길이 5의 int 배열 생성

설명:

  • int[]::newint 배열의 생성자를 참조하여 지정된 길이의 배열을 생성한다.
  • arrayCreatorIntFunction<int[]> 타입으로, 배열의 크기를 받아 그 크기만큼의 새로운 배열을 생성한다.
  • len -> new int[len] 람다 표현식과 동일하게 동작

요약

유형 예시 설명
정적 메서드 참조 Integer::parseInt 특정 클래스의 정적 메서드를 참조. 객체가 필요 없으며 클래스 이름으로 호출 가능.
한정적 인스턴스 메서드 참조 now::isAfter 특정 객체에 바인딩된 메서드를 참조. 객체가 이미 존재하며 해당 객체의 메서드를 참조함.
비한정적 인스턴스 메서드 참조 String::toLowerCase 수신 객체가 특정되지 않은 상태로 인스턴스 메서드를 참조. 호출 시점에 객체를 전달받음.
클래스 생성자 참조 ArrayList::new 클래스의 생성자를 참조하여 객체를 생성할 수 있음. 주로 팩터리 메서드로 사용됨.
배열 생성자 참조 int[]::new 배열 생성자를 참조하여 지정된 길이의 배열을 생성. 함수형 인터페이스로 배열 생성 가능.

3. 람다로 할 수 없는 일

람다로는 불가능하나 메서드 참조로는 가능한 유일한 예외제네릭 함수 타입(generic function type)입다. 자바는 제네릭 람다식을 지원하지 않기 때문에 제네릭 함수형 인터페이스의 구현은 메서드 참조로 표현할 수 있지만 람다로는 표현할 수 없다.

4. 핵심 정리

  • 메서드 참조는 람다의 간단한 대안으로 사용할 수 있으며, 코드가 더 짧고 명확해질 때 유용하다.
  • 그러나 메서드 참조가 오히려 가독성을 떨어뜨릴 수 있는 경우가 있으므로 상황에 따라 람다를 유지하는 것이 좋다.
  • 메서드 참조와 람다 표현식은 코드의 간결성과 가독성을 높이기 위한 수단으로, 상황에 맞게 적절하게 선택하여 사용하는 것이 중요하다.

람다와 메서드 참조는 자바의 코드 가독성을 높이고, 함수형 프로그래밍을 효과적으로 지원하는 기능이다.