Contents
람다 표현식(2)
   Sep 3, 2022     17 min read

Chapter03-2

메서드 참조

inventory.sort((Apple a1, Apple a2) -> a1.getWeight().comparator.comparing

메서드 참조를 했을 때

inventory.sort(comparing(Apple::getWeight));

메소드 참조는 왜 중요할까 ?

메서드 참조는 특정 메서드만을 호출하는 람다의 축약형이라고 생각할 수 있다.

예를들어 ‘이 메서드를 직접 호출해’라고 명령한다면 메서드를 어떻게 호출해야 하는지 설명을 참조하기보다는 메서드명을 직접 참조하는 것이 편리하다.

실제로 메서드 참조를 이용하면 기존 메서드 구현으로 람다 표현식을 만들 수 있다.



  • 메서드 참조는 어떻게 활용할까 ?
클래스::메서드명

ex) Apple::getWeight

//메서드 참조는 람다 표현식 (Apple a) -> a.getWeight()를 축약한 것.


  • 람다와 메서드 참조 단축 표현 예제
람다메서드 참조 단축 표현
(Apple apple) → apple.getWeight()Apple::getWeight
() →Thread.currentThread()::dumpStack
Thread.currentThread().dumpStack 
(str, i) → str.subtring(i)String::substring
(String s) → System.out.println(s)System.out::println
(String s) → this.isValidName(s)this::isValidName


메서드 참조를 만드는 방법

  • 정적 메서드 참조
    • 예를 들어 Integer의 parseInt 메서드는 Integer::parseInt로 표현할 수 있다.
  • 다양한 형식의 인스턴스 메서드 참조
    • 예를 들어 String의 length 메서드는 String::length로 표현할 수 있다.
  • 기존 객체의 인스턴스 메서드 참조
    • 예를 들어 Transaction 객체를 할당받은 expensiveTransaction 지역 변수가 있고, Transaction 객체에는 getValue 메서드가 있다면, 이를 expensiveTransaction::getValue라고 표현할 수 있다.

두 번째(다양한 형식의 인스턴스 메서드 참조)라는 말이 와닿지 않을 것이다. String::length 같은 두 번째 유형의 메서드 참조를 이용해서 람다 표현식의 파라미터로 전달할 수 있다.

예를 들어 (String s) → s.toUpperCase()라는 람다 표현식을 String::toUpperCase로 줄여서 표현할 수 있다.

마찬가지로 세 번째(기존 객체의 인스턴스 메서드 참조)도 와닿지가 않을 것이다. 이 유형의 메서드 참조는 람다 표현식에서 현존하는 외부 객체의 메서드를 호출할 때 사용된다.

예를 들어 () → expensiveTransaction.getValue()라는 람다 표현식을 expensiveTransaction::getValue로 표현할 수 있다.

세 번째 유형의 메서드 참조는 비공개 헬퍼 메서드를 정의한 상황에서 유용하게 활용할 수 있다.


예를 들어 isValidName이라는 헬퍼 메서드를 정의했다고 가정해보자.

private boolean isValidName(String string){
	return Character.isUpperCase(string.charAt(0));
}

이제 Predicate를 필요로 하는 적당한 상황에서 메서드 참조를 사용할 수 있다.

filter(words, this::isValidName)

위와 같은 세 가지 경우 말고도 특별한 형식의 메서드 참조도 있다.

여기서 특별한 형식은 생성자, 배열 생성자, super 호출 등에 사용할 수 있는 형식을 말한다.

  • 예제 : List에 포함된 문자열을 대소문자를 구분하지 않고 정렬하는 프로그램 구현해 보자.

List의 sort 메서드는 인수로 Comparator를 기대한다. Comparator는 (T, T) → int 라는 함수 디스크립터를 갖는다.
String 클래스에 정의되어 있는 compareToIgnoreCase메서드로 람다 표현식을 정의할 수 있다.

List<String> str = Arrays.asList("a","b","A","B");
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
  • 세 가지 종류의 람다 표현식을 메서드 참조로 바꾸는 방법

Untitled

Comparator의 함수 디스크립터와 호환되는 람다 표현식 시그니처도 있다.
위에서 설명한 기법을 이용하면 람다 표현식을 메서드 참조를 사용해서 다음처럼 줄일 수 있다.

List<String> str = Arrays.asList("a","b","A","B");
str.sort(String::compareToIgnoreCase);

컴파일러는 람다 표현식의 형식을 검사하던 방식과 비슷한 과정으로 메서드 참조가 주어진 함수형 인터페이스와 호환하는지 확인한다. 즉 메서드 참조는 콘텍스트의 형식과 일치해야한다.

클래스의 생성자를 이용하는 방법 - 생성자 참조

ClassName::new처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다. 이것은 정적 메서드의 참조를 만드는 방법과 비슷하다.

예를 들어 인수가 없는 생성자, 즉 Supplier() → Apple과 같은 시그니처를 갖는 생성자가 있다고 가정하자.

Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get(); // <-- Supplier의 get 메서드를 호출해서 새로운 Apple 객체를 만들 수 있다.
Supplier<Apple> c1 = () -> new Apple(); //<-- 람다 표현식은 디폴트 생성자를 가진 Apple을 만든다.
Apple a1 = c1.get(); //<-- Supplier의 get 메서드를 호출해서 새로운 Apple 객체를 만들 수 있다.

Apple(Integer weight)라는 시그니처를 갖는 생성자는 Function 인터페이스의 시그니처와 같다.

따라서 다음과 같은 코드를 구현할 수 있다.

Function<Integer, Apple> c2 = Apple::new; //<-- Apple(Integer weight)의 생성자 참조
Apple a2 = c2.apply(110); // <-- Function의 Apple 메서드에 무게를 인수로 호출해서 새로운 Apple 객체를 만들 수 있다.

이 코드는 다음과 같다.

Function<Integer, Apple> c2 = (weight) -> new Apple(weight); //<-- 특정 무게의 사과를 만드는 람다 표현식
Apple a2 = c2.apply(110); //<-- Function의 apply 메서드에 무게를 인수로 호출해서 새로운 Apple객체를 만들 수 있다.

다음 코드에서 Integer를 포함하는 리스트의 각 요소를 우리가 정의했던 map 같은 메서드를 이용해서 Apple 생성자로 전달한다.
결과적으로 다양한 무게를 포함하는 사과 리스트가 만들어 진다.

List<Integer> weights = Arrays.asList(7, 3, 4, 10);
List<Apple> apples = map(weights, Apple::new); //<-- map 메서드로 생성자 참조 전달

public List<Apple> map(List<Integer> list, Funtcion<Integer, Apple> f){
	List<Apple> result =new ArrayList<>();
	for(Intger i : list) {
		result.add(f.apply(i));
	}
	return result;
}

Apple(String color, Integer weight) 처럼 두 인수를 갖는 생성자는 BiFunction 인터페이스와 같은 시그니처를 가지므로 다음처럼 할 수 있다.

BiFunction<Color, Integer, Apple> c3 = Apple::new //<-- Apple(String color, Integer weight)의 생성자 참조
Apple a3 = c3.apply(GREEN, 110); // <-- BiFunction의 apply 메서드의 색과 무게를인수로 제공해서 새로운 Apple 객체를 만들 수 있다.

이 코드는 다음과 같다.

BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, wieght); //<-- 특정 색과 무게를 가진 사과를 만드는 람다 표현식
Apple a3 = c3.apply(GREEN, 110); //<-- BiFunction의 apply메서드에 색과 무게를 인수로 제공해서 새로운 Apple 객체를 만들 수 있다.

인스턴스화 하지 않고도 생성자에 접근할 수 있는 기능을 다양한 상황에 응요할 수 있다.

예를 들어 Map으로 생성자와 문자열값을 관련시킬수 있다. 그리고 String과 Integer가 주어졌을 때 다양한 무게를 갖는 여러 종류의 과일을 만드는 giveMeFruit라는 메서드를 만들 수 있다.

static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
static {
	map.put("apple", Apple::new);
	map.put("orange", Orange::new);
	// 등등...
}
public static Fruit giveMeFruit(String fruit, Integer weight){
	return map.get(fruit.toLowerCase()) //<-- map에서 Function<Integer, Fruit>를 얻었다.
					  .apply(weight); //<-- Function의 apply 메서드에 정수 무게 파라미터를 제공해서 Fruit를 만들 수 있다.
}

람다, 메서드 참조 활용하기

  • 1단계 : 코드 전달
    • sort 메서드에 정렬 전략을 전달할 수 있을까? sort 메서드는 다음과 같은 시그니처를 갖는다.

      void sort(Comparator<? super E> c)
      

이 코드는 Comparator 객체를 인수로 받아 두 사과를 비교한다.
객체 안에 동작을 포함시키는 방식으로 다양한 전략을 전달할 수 있다. 이제 ‘sort의 동작파라미터화 되었다.’ 라고 말할 수 있다. 즉 sort에 전달된 정렬 전략에 따라 sort의 동작이 달라질 것이다.


  • 1단계 코드
public class AppleComparator implements Comparator<Apple> {
	public int compare(Apple a1, Apple a2) {
		return a1.getWeight().compareTo(a2.getWeight());
	}
}

inventory.sort(new AppleComparator());
  • 2단계 : 익명 클래스 사용
    • 한 번만 사용할 Comparator를 위 코드처럼 구현하는 것보다는 익명클래스 를 이용하는 것이 좋다.

      inventory.sort(new Comparator<Apple>() {
      	public itn compare(Apple a1, Apple a2){
      		return a1.getWeight().compareTo(a2.getWeight());
      	}
      });
      
  • 3단계 : 람다 표현식 사용
    • 익명 클래스만 코드를 보면 아직 코드가 너무 길다.. 자바 8에서 나온 람다 표현식을 사용해서 경량화된 문법을 사용해 코드를 전달해보자.
    • 함수형 인터페이스 를 기대하는 곳 어디에서나 람다 표현식을 사용할 수 있다.
    • 함수형 인터페이스란 오직 하나의 추상 메서드르 정의하는 인터페이스
    • 추상 메서드의 시그니처(함수 디스크립터)는 람다 표현식의 시그니처를 정의한다.
    • Comparator의 함수 디스크팁터 → (T, T) → int
    • 사과코드를 적용했을 경우 → (Apple, Apple) → int

      inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
      
    • 자바 컴파일러는 람다 표현식이 사용된 콘택스트를 활용해서 람다의 파라미터 형식을 추론한다.

    따라서 아래의 코드 처럼 줄일 수 있다.

      inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
    

    위의 코드의 가독성을 더 향상 시킬 수 없을까 ?

    Comparator는 Comparable 키를 추출해서 Comparator 객체로 만드는 Function 함수를 인수로 받는 정적 메서드 comparing을 포함한다.
    다음 처럼 comparing 메서드를 사용할 수 있다.( 람다 표현식은 사과를 비교하는데 사용할 키를 어떻게 추출할 것인지 지정하는 한 개의 인수만 포함한다.)

      Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());
    

이제 코드를 간소화 해보자.

import static java.util.Comparator.comparing;
inventory.sort(comparing(apple -> apple.getWeight()));
  • 4단계 : 메서드 참조 사용
    • 메서드 참조 사용하면 람다 표현식의 인수를 더 깔끔하게 전달할 수 있다.

      import static java.util.Comparator.comparing;
      iventory.sort(comparing(Apple::getWeight));
      

드디어 최적의 코드를 만들었다. 자바 8 이전의 코드에 비해 어ㅈ떤 점이 개선되었을까?

코드의 간결성이 좋아졌고 코드의 의미도 명확해졌다. 코드 자체로 ‘Apple을 Weight별로 비교해서 inventory를 sort하라’는 의미를 전달할 수 있다.

람다 표현식을 조합할 수 있는 유용한 메서드

Comparator조합

정적 메서드 Comparator.comparing을 이용해서 비교에 사용할 키를 추출하는 Funtion 기반의 Comparator를 반환할 수 있다.

Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
  • 역정렬

사과의 무게를 내림차순으로 하고 싶다면 어떻게 해야할까 ?

다른 Comparator 인스턴스를 만들 필요가 없다. 인터페이스 자체에서 주어진 비교자의 순서를 뒤바꾸는 reverse라는 디폴트 메서드를 제공하기 때문이다.
따라서 처음 비교자 구현을 그대로 재사용해서 사과의 무게를 기준으로 역정렬할 수 있다.

➡️코드


System.out.println("---------역정렬---------");
inventory.sort(comparing(Apple::getWeight).reversed()); //<-- 무게를 내림차순으로 정렬

➡️출력 결과

---------역정렬---------
[Apple{color=GREEN, weight=150}, Apple{color=RED, weight=120}, Apple{color=GREEN, weight=80}]

➡️코드

System.out.println("---------정렬---------");
inventory.sort(comparing(Apple::getWeight)); // 디폴트로 오름차순으로 되어있다.!
System.out.println(inventory);

➡️출력 결과

---------정렬---------
[Apple{color=GREEN, weight=80}, Apple{color=RED, weight=120}, Apple{color=GREEN, weight=150}]
  • Comparator 연결

위 코드를 하면서 의문점이 하나 들었다. 사과의 무게가 같은 것들이 있는 것은 어떻게 처리할까 ? 정렬된 리스트에서 어떤 사과를 먼저 나열해야할까 ?

비교 결과를 더 다듬을 수 있는 두 번째 Comparator를 만들 수 있다.

예를들면 - 두 사과를 비교한 다음에 무게가 같다면 원산지 국가별로 사과를 정렬할 수 있다. thenComparing 메서드로 두 번째 비교자를 만들 수 있다. thenComparing은 (comparing 메서드처럼) 함수를 인수로 받아 첫 번째 비교자를 이용해서 두 객체가 같다고 판단되면 두 번째 비교자에 객체를 전달한다.

Untitled 1

백문이 불여일타 ! 코드를 통해서 보자!

inventory.sort(comparing(Apple::getWeight)
				 .reversed() // <-- 무게를 내림차순으로 정렬
				 .thenComparing(Apple::getCountry)); //<-- 두 사과의 무게가 같으면 국가별로 정렬

➡️출력 결과

--------같은 무게가 존재한다면 ? ---------
[Apple{color=RED, weight=150, country=KOREA},
 Apple{color=GREEN, weight=150, country=USA},
Apple{color=RED, weight=120, country=JAPAN},
Apple{color=GREEN, weight=80, country=KOREA}]

Predicate 조합

Predicate 인터페이스는 복잡한 프레디케이트를 만들 수 있도록 negate, and, or 세 가지 메서드를 제공한다.

예를 들어 ‘빨간색이 아닌 사과’처럼 특정 프레디케이트를 반전시킬 때 negate메서드를 사용할 수 있다.

//기존 프레디케이트 객체 redApple의 결과를 반전시킨 객체를 만든다.
Predicate<Apple> notRedApple = redApple.negate();
  • and 메서드를 이용해서 ‘빨간색이면 무거운 사과를 선택’하도록 두 람다를 조합할 수 있다.
// 두 프레디케이트를 연결해서 새로운 프레디케이트 객체를 만든다.
Predicate<Apple> redAppleHeavyApple = redApple.and(apple  apple.getWeight() > 150);
  • or 메서드를 이용해서 ‘빨간색이면 무거운(150g 이상) 사과 또는 그냥 녹색 사과’ 등 다양한 조건을 만들 수 있다.
//프레디케이트 메서드를 연결해서 더 복잡한 프레디케이트 객체를 만든다.
Predicate<Apple> redAndHeavyAppleOrGreen =
			redApple.and(apple -> apple.getWeight() > 150)
							.or(apple -> GREEN.equals(a.getColor()));

  • 이것이 대단한 일인 이유가 뭘까 ?

단순한 람다 표현식을 조합해서 더 복잡한 람다 표현식을 만들 수 있기 때문이다.
심지어 람다 표현식을 조합해도 코드 자체가 문제를 잘 설명한다는 점은 변치 않는다.

여기서 소개한 and, or 등은 왼쪽에서 오른쪽으로 연결되었다. 즉, a.or(b).and(c)(a || b) && c와 같다.


Function 조합

Function 인터페이스에서 제공하는 람다 표현식도 조합할 수 있다.
Function인터페이스는 Function 인스턴스를 반환하는 andThen, compose 두 가지 디폴트 메서드를 제공한다.

  • andThen 메서드는 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환한다.

예를 들어 숫자를 증가(x → x + 1)시키는 f라는 함수가 있고, 숫자에 2를 곱하는 g라는 함수가 있다고 가정하자.

f와 g를 조합해서 숫자를 증가시킨 뒤 결과에 2를 곱하는 h라는 함수를 만들 수 있다.

Function<Integer, Integer> f = x ->x + 1;
Function<Integer, Integer> g = x ->x * 2;
Function<Integer, Integer> h = f.andThen(g); // <-- 수학으로는 write g(f(x)) 또는 (g o f)(x)라고 표현
int result = h.apply(1); // <- 4를 반환
  • compose 메서드는 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공한다. 즉, f.andThen(g)에서 andThen 대신에 compose를 사용하면 g(f(x))가 아니라 f(g(x))라는 수식이 된다.
Function<Integer, Integer> f = x ->x + 1;
Function<Integer, Integer> g = x ->x * 2;
Function<Integer, Integer> h = f.compose(g); //<-- 수학으로는 f(g(x))또는 (f o g)(x)라고 표현
int result = h.apply(1); //<-- 3을 반환
  • andThen 과 compose의 차이

Untitled 2

Untitled 3

이해가 안된다!… 다른 예제를 통해서 한번 더 해보자.

예를 들어 문자열로 구성된 편지 내용을 변환하느 다양한 유틸리티 메서드가 있다고 가정하자.

public class Letter{
	public static String addHeader(String text){
		return "From Raoul, Mario and Alan: " + text;
	}

	public static String addFooter(String text){
		return text + " Kind regards";
	}

	public static String checkSpelling(String text){
		return text.replaceAll("labda", "lambda");
	}
}

여러 유틸리티 메서드를 조합해서 다양한 변환 파이프라인을 만들 수 있다.
헤더를 추가(addHeader)한 다음에, 철자검사(checkSpelling)를 하고 마지막에 푸터를 추가(addFooter)할 수 있다.

Function<String, String> addHeader = Letter::addHeader;
Function<String, String> transformationPipeline =
				addHeader.andThen(Letter::checkSpelling)
							   .andThen(Letter::addFooter);

Untitled 4

응용 : 철자 검사빼고 헤더와 푸터만 추가하는 파이프라인

Function<String, String> addHeader = Letter::addHeader;
Function<String, String> transformationPipeline =
				addHeader.andThen(Letter::addFooter);

비슷한 수학적 개념

적분

다음과 같은 함수 (자바의 함수가 아닌 수학의 함수) f 가 있다고 가정하자.

$f(x) = x + 10$

공학에서는 함수가 차지하는 영역을 묻는 질문이 자주 등장한다.(일반적으로 x축이 기준선이 됨.)

$\textstyle \int_3^7 f(x)dx$ 또는 $\textstyle \int_3^7 (x + 10)dx$

  • x가 3에서 7까지의 범위일 때 함수 $f(x) = x + 10$ 의 영역

Untitled 5

이 예제에서 함수 f는 직선이므로 사다리꼴 기법(기본적으로 삼각형으로 구성되는)으로 정답을 찾는다.

$\frac12 \ast ((3 + 10) + (7 + 10)) \ast (7 - 3) = 60$

이 공식을 자바코드로 표현해보자. 우선 dy / dx 기호를 어떻게 처리할 것인가 ?

우선은 f와 한계값(여기서 3.0과 7.0)을 인수로 받는 integrate라는 함수를 만들어야한다.
즉, 다음처럼 함수 f를 인수로 받는 함수를 자바로 구현할 수 있다.

integrate(f, 3, 7)

그러나 다음처럼 간단히 구현할 수 없다.

integrate(x + 10, 3, 7)

우선 이 식에서 x의 범위가 불분명하다. 그리고 f를 전달하는 것이 아니라 x + 10 이라는 값을 전달하게 되기 때문에 잘못된 식이다.

수학에서 dx는 ‘x를 인수로 받아 x + 10의 결과를 만드는 함수' 로 정리할 수 있다.

자바 8 람다로 연결 !

자바 8에서는 (double x) → x + 10 같은 람다 표현식을 사용할 수 있다.

따라서 함수 f를 다음처럼 구현할 수 있다.

integrate((double x) -> x + 10, 3, 7)

또는

integrate((double x) -> f(x), 3, 7)

C가 정적 메서드 f를 포함하는 클래스라 가정하면 메서드 참조를 사용해서 코드를 더 간단하게 만들 수 있다.

f의 코드를 integrate 메서드로 전달하는 것이 이 예제의 핵심.

integrate(C::f, 3, 7)

이제 integrate 메서드를 구현해보자. f를 선형 함수(직선)이라고 가정하자.

//틀린 자바 코드 ! 자바코드를 수학 함수처럼 구현할 수 없다.
public double integrate((double -> double) f, double a, double b) {
	return (f(a) + f(b)) * (b - a) / 2.0
}

하지만 함수형 인터페이스(여기서는 DoubleFunction)를 기대하는 콘텍스트에서만 람다 표현식을 사용할 수 있으므로 다음처럼 코드를 구현해야 한다.

public double integrate(DoubleFunction<Double>) f, double a, double b){
	return (f.apply(a) + f.apply(b)) * (b - a) / 2.0;
}

또는 DoubleUnaryOperator를 이용해도 결과를 박싱할 필요가 없다.

public double integrate(DoubleUnaryOperator f, double a, double b){
	return (f.applyAsDouble(a) + f.applyAsDouble(b)) * (b - a) / 2.0;
}

참고로 수학처럼 f(a)라는 표현할 수없고 f.apply(a)라고 구현했는데 이는 자바가 진정으로 함수를 허용하지 않고 모든 것을 객체로 여기는 것을 포기할 수 없기 때문이다.

Untitled 6

회고록

람다 표현식(2) - 메소드 레퍼런드, 람다 표현식을 조합할 수 있는 유용한 메서드, Comparator, Predicate, Function , 비슷한 수학적 개념 등 을 배웠다.
이론은 이해는 한걸까? 모르는 사람에게 이해되게 설명해서 체크해봐야겠다. 역시나 어려운 적용 및 응용 하는 …. 계속 예제해보고 내가 만든 것들을 계속해보자.
천천히 나아가 보자. 배우는 건 상대적으로 늦지만 꾸준히하는 것을 누구보다 잘 할 자신이 있다.

giphy