JAVA

모던 자바 인 액션 - CH03. 람다 표현식

사랑박 2023. 7. 30. 01:24

들어가기에 앞서 ...

이 장에서는 람다 표현식을 어떻게 만드는지, 어떻게 사용하는지, 어떻게 코드를 간결하게 만들 수 있는지 설명한다. 또한 자바 8 API에 추가된 중요한 인터페이스와 형식 추론 등의 기능도 확인한다. 마지막으로 람다 표현식과 함께 위력을 발휘하는 새로운 기능인 메서드 참조를 설명한다.


3.1 람다란 무엇인가?

람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다. 람다 표현식에는 이름이 없지만, 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외 리스트는 가질 수 있다.

람다의 특징

  • 익명 : 보통의 메서드와 달리 이름이 없다. 구현해야 할 코드에 대한 걱정거리가 줄어든다.
  • 함수 : 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다. 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 표함한다.
  • 전달 : 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
  • 간결성 : 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없다.

람다의 구성

(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
  • 파라미터 리스트
    • Comparator의 compare 메서드 파라미터
    • (Apple a1, Apple a2)
  • 화살표(→)
    • 화살표는 람다의 파라미터 리스트와 바디를 구분한다.
  • 람다 바디
    • 두 사과의 무게를 비교한다. 람다의 반환값에 해당하는 표현식이다.

람다 문법

  • 표현식 스타일 
    • (parameters) -> expression
    • 표현식 스타일에는 return이 함축되어 있으므로 return을 명시적으로 사용하지 않아도 됨
  • 블록 스타일
    • (parameters) -> { statements; }
    • 블록에는 여러 행의 문장을 포함하는 구문이 들어가며, 리턴 타입이 void가 아니라면 return을 명시적으로 사용해야한다.

람다 예제

사용 사례 람다 예제
불리언 표현식 (List<String> list) → list.isEmpty()
객체 생성 () → new Apple(10)
객체에서 소비 (Apple a) → { System.out.println(a.getWeight());} 
객체에서 선택/추출 (String s) → s.length()
두 값을 조합 (int a, int b) → a * b
두 객체 비교 (Apple a1, Apple a2) → a1.getWeight().compareTo(a2.getWeight())

3.2 어디에, 어떻게 람다를 사용할까?

함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다. 일단 함수형 인터페이스가 무엇인지 자세히 살펴보자

 

3.2.1 함수형 인터페이스

2장에서 만든 Preddicate<T>가 함수형 인터페이스이다. Predicate<T>는 오직 하나의 추상 메서드만 지정하기 때문이다.

간단히 말해 함수형 인터페이스는 정확이 하나의 추상 메서드를 지정하는 인터페이스이다. 지금까지 살펴본 자바 API의 함수형 인터페이스로 Comparator, Runnable 등이 있다.

 

람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다. 함수형 인터페이스보다는 덜 깔끔하지만 익명 내부 클래스로도 같은 기능을 구현할 수 있다.

다음 예제는 Runnable이 오직 하나의 추상 메서드 run을 정의하는 함수형 인터페이스이므로 올바른 코드다.

// 람다 사용 
Runnable r1 = () -> System.out.println("Hello world 1");

// 익명 클래스 사용
Runnable r2 = new Runnable() {
	public void run() {
		System.out.println("Hello World 2");
	}
}

public static void process(Runnable r) {
	r.run();
}
process(r1);
process(r2);
process(() -> System.out.println("Hello Wordl 3")); // 람다 표현식으로 직접 전달도 가능

 

3.2.2 함수 디스크립터

함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다. 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부른다. 예를 들어 Runnable 인터페이스의 유일한 추상 메서드 run은 인수화 반환값이 없으므로 (void 반환) Runnable 인터페이스는 인수와 반환값이 없는 시그니처로 생각할 수 있다.

EX)

() -> void : 파라미터가 없으며 void를 반환하는 함수

(Apple, Apple) -> int : 두 개의 Apple을 인수로 받아 int를 반환하는 함수

 

람다표현식은 `변수에 할당` 하거나 `함수형 인터페이스를 인수로 받는메서드로 전달`할 수 있으며, 함수형 인터페이스의 추상 메서드와 같은 시그니처를 갖는다.

 

@FunctionalInterface는 무엇인가?

@FunctionalInterface는 함수형 인터페이스임을 가리키는 어노테이션이다. @FunctionalInterface로 인터페이스를 선언했지만 실제로 함수형 인터페이스가 아니라면 컴파일러가 에러를 발생시킨다.


3.3 람다 활용 : 실행 어라운드 패턴

예제는 파일에서 한 행을 읽는 코드이다.

public String processFile() throws IOException {
	try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))){
		return br.readLine(); // 실제 필요한 작업을 하는 행
    }
}

 

3.3.1 1단계 : 동작 파라미터화를 기억하라

Q. 한 번에 두줄을 읽거나 가장 자주 사용되는 단어를 반환하려면??

A. 기존의 설정, 정리 과정은 재사용하고  processFile메서드만 다른 동작을 수행하도록 명령하는 방법 -> processFile의 동작을 파라미터화

processFile메서드가 BufferedReader를 이용해서 다른 동작을 수행할 수 있도록 processFile 메서드로 동작을 전달해야한다.

String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());

 

3.2.2 2단계 : 함수형 인터페이스를 이용해서 동작 전달

BufferedReader -> String 과 IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 한다.

@FunctionalInterface
public interface BufferedReaderProcessor {
	String process(BufferedReader b) throws IOException;
}

정의한 인터페이스를 processFile 메서드의 인수로 전달할 수 있다.

public String processFile(BufferedReaderProcessor p) throws IOException{
        ...
}

 

3.3.3 3단계 : 동작 실행

processFile 바디 내에서 BufferedReaderProcessor 객체의 process를 호출할 수 있다.

public String processFile(BufferedReaderProcessor p) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
		return p.process(br);
    }
}

 

3.3.4 4단계 : 람다 전달

이제 람다를 이용해서 다양한 동작을 procesFile 메서드로 전달할 수 있다.

한 행을 처리하는 코드

String oneLine = processFile((BufferedReader br) -> br.readLine());

두 행을 처리하는 코드

String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());

3.4 함수형 인터페이스 사용

자바 API는 Comparable, Runnable, Callable 등의 다양한 함수형 인터페이스를 포함하고 있다.

자바 8 라이브러리 설계자들은 java.util.function 패키지로 여러 가지 새로운 함수형 인터페이스를 제공한다.

이 절에서는 Predicate, Consumer, Function 인터페이스를 설명한다.

 

3.4.1 Predicate

java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 불리언을 반환한다.

다음 예제처럼 String 객체를 인수로 받는 람다를 정의할 수 있다.

@FunctionalInterface
public interface Predicate<T> {
	boolean test(T t);
}
public <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> results = new ArrayList<>();
    for (T t : list) {
        if (p.test(t))
          resuls.add(t);
    }
    return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listsOfStrings, nonEmptyStringPredicate);

Predicate 인터페이스의 자바독 명세를 보면 and나 or 같은 메서드도 있음을 알 수 있다.

 

3.4.2 Consumer

java.util.function.Consumer<T> 인터페이스는 제네릭 형식  T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의한다. T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있다.

 

다음은 forEach와 람다를 이용햐서 리스트의 모든 항목을 출력하는 예제다.

@FunctionalInterface
public interface Consumer<T> {
	void accept(T t);
}

public void <T> void forEach(List<T> list, Consumer<T> c) {
    for (T t : list) {
        c.accept(t);
    }
}

forEach(
  Arrays.asList(1,2,3,4,5),
  (Integer i) -> System.out.println(i); // <- Consumer의 accept 메서드를 구현하는 람다
);

 

3.4.3 Function

java.util.function.Function<T,R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의한다. 입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있다.

다음은 String 리스트를 인수로 받아 각 String의 길이를 포함하는 Integer 리스트로 반환하는 map 메서드를 정의하는 예제다.

@FunctionalInterface
public interface Function<T, R> {
	R apply(T t);
}

public <T, R> List<R> map(List<T> list, Function<T, R> f) {
    List<R> result = new ArrayList<>();
    for (T t : list) {
        result.add(f.apply(t));
    }
    return result;
}

List<Integer> l = map(
    Arrays.asList("lambdas", "in", "action"),
    (String s) -> s.length(); // <- Function의 apply 메서드를 구현하는 람다
);

기본형 특화

  • 박싱: 기본형을 참조형으로 변환
  • 언박싱 : 참조형을 기본형으로 변환
  • 오토박싱: 박싱과 언박싱이 자동으로 이루어지는 것

제네릭 파라미터에는 참조형만 사용할 수 있다. 다음은 int를 Integer로 박싱하는 코드이다.

List<Integer> list = new ArrayList<>();
for(int i = 300; i < 400; i++) {
	list.add(i);
}

이런 변환 과정은 비용이 소모된다. 박싱한 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정이 필요하다. 따라서, 자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공한다.

 

예를 들어, 아래 예제에서 IntPredicate는 1000이라는 값을 박싱하지 않지만, Predicate<Integer>는 1000이라는 값을 Integer 객체로 박싱한다.

public interface IntPredicate {
	boolean test(int t);
}

IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000); // 참 (박싱 없음)

Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0;
oddNumbers.test(1000); // 거짓 (박싱)

일반적으로 특정 형식을 입력으로 받는 함수형 인터페이스의 이름 앞에는 DoublePredicate, IntConsumer, LongBinaryOperator, IntFunction처럼 형식명이 붙는다.

 

++ 예외, 람다, 함수형 인터페이스의 관계 (pg.107 참조)

함수형 인터페이스는 확인된 예외를 던지는 동작을 허용하지 않는다. 즉, 예외를 던지는 람다 표현식을 만들려면 확인된 예외를 선언하는 함수형 인터페이스를 직접 정의하거나 람다를 try/catch 블록으로 감싸야 한다.


3.5 형식 검사, 형식 추론, 제약

람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지의 정보가 포함되어 있지 않다. 따라서 람다 표현식을 더 제대로 이해하려면 람다의 실제 형식을 파악해야 한다.

 

3.5.1 형식 검사

람다가 사용되는 콘텍스트를 이용해서 람다의 형식을 추론할 수 있다. 어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 대상 형식이라고 부른다.

예제를 보자.

List<Apple> heavierThan150gram = 
    filter(inventory, (Apple apple) -> apple.getWeitht() > 150);
  1. filter 메서드 선언 확인
  2. 두번째 파라미터로 Predicate 형식 (대상 형식)을 기대
  3. Predicate은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스
  4. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터 묘사
  5. filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 한다.

 

3.5.2 같은 람다, 다른 함수형 인터페이스

대상 형식이라는 특징 때문에 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있다.

예를 들어 Callable과 PrivilegedAction 인터페이스는 인수를 받지 않고 제네릭 형식 T를 반환하는 함수를 정의한다. 따라서 두 할당문은 모두 유효하다.

Callable<Integer> c = () -> 42;
PrevillegedAction<Integer> p = () -> 42;

다른 예제도 있다.

Comparator<Apple> c1 =
        (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple> c2 =
        (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple> c3 =
        (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

즉, 하나의 람다 표현식을 다양한 함수형 인터페이스에 사용할수 있다.

 

++ 같은 함수형 디스크립터를 가진 두 함수형 인터페이스를 갖는 메소드를 오버로딩할 때 , 어떤 메소드 시그니처가 사용되어야 하는지를 명시적으로 구분하도록 람다를 캐스트할 수 있다.

 

3.5.3 형식 추론

대상 형식을 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론할 수 있다. 결과적으로 컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있으므로 람다 문법에서 이를 생략할 수 있다.

List<Apple> greenApples =
        filter(inventory, apple -> GREEN.equals(apple.getColor())); // 파라미터 apple에 형식을 지정하지 않았다.

 

상황에 따라 명시적으로 형식을 표현하는 것이 좋을 때도 있고 형식을 배제하는 것이 가독성을 향상시킬 때도 있다.

Comparator<Apple> c =
        (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Comparator<Apple> c =
        (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

3.5.4 지역 변수 사용

람다 캡처링 : 람다 표현식에서 익명 함수가 하는 것처럼 자유 변수(파라미터로 넘겨진 변수가 아닌 외부 변수)를 활용할 수 있다.

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);

하지만, 지역 변수는 final로 선언되어 있어야 하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야한다.

다음 예제는 portNumber에 값을 두 번 할당하므로 컴파일할 수 없는 코드다.

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;

 

지역 변수의 제약

Q. 왜 지역 변수에 이런 제약이 필요한가?

A. 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공한다. 따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴 것이다.


3.6 메서드 참조

메소드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다. 때로는 람다 표현식보다 메서드 참조를 사용하는 것이 더 가독성이 좋으며 자연스러울 수 있다.

기존코드

// 기존의 람다 표현식
inventory.sort((Apple a1, Apple a2) 
	-> a1.getWeight().compareTo(a2.getWeight()));

// 메서드 참조와 java.util.Comparator.comparing을 활용한 코드
inventory.sort(comparing(Apple::getWeight));

 

3.6.1 요약

  • 메서드 참조는 특정 메서드만을 호출하는 람다의 축양형이다.
  • 메서드 참조를 이용하면 기존 메서드 구현으로 람다 표현식을 만들 수 있다. 이때 명시적으로 메서드명을 참조함으로써 가독성을 높일 수 있다.
  • 메서드 참조는 메서드명 앞에 구분자(::)를 붙이는 방식으로 활용할 수 있다. 예를들면 (Apple a) -> a.getWeight()를 축약하면 Apple::getWeight 이다.
  • 메서드 참조는 새로운 기능이 아니라 하나의 메서드를 참조하는 람다를 편리하게 표현할 수 있는 문법으로 간주할 수 있다.

메서드 참조 세 가지 유형

1. 정적 메서드 참조

ex) Integer.parseInt() → Integer::parseInt

2. 다양한 형식의 인스턴스 메서드 참조

ex) String의 length() → 람다로 표현하면 (String s) -> s.length() → 메서드 참조로 표현하면 String::length

3. 기존 객체의 인스턴스 메서드 참조

ex) (Transaction) expensiveTransaction.getValue() → 람다로 표현하면 () -> expensiveTransaction.getValue() → 메서드 참조로 표현하면 expensiveTransaction::getValue

 

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

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

// 헬퍼 메소드
private boolean isValidName(String string) {
	return Character.isUpperCase(string.charAt(0));
}

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

// 메서드 참조
filter(list, this::isValidName);

 

생성자, 배열 생성자, super 호출 등에 사용할 수 있는 특별한 형식의 메서드 참조도 있다. 예제를 통해 메서드 참조 활용법을 확인하자. List에 포함된 문자열을 대소문자 구분하지 않고 정렬하는 프로그램을 구현하려 한다. List의 sort 메서드는 인수로 Comparator를 기대한다. Comparator는 (T, T) -> int 라는 함수 디스크립터를 갖는다.

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

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

 

3.6.2 생성자 참조

ClassName::new 처럼 클래스명과 new 키워드 이용해서 기존 생성자의 참조를 만들 수 있다. 이것은 정적 메서드의 참조를 만드는 방법과 비슷하다. 예를 들어 인수가 없는 생성자, 즉 Supplier () -> Apple과 같은 시그니처를 갖는 생성자도 있다고 가정하자.

// Supplier.java
@FunctionalInterface
public interface Supplier<T> {
    T get(); // 매개변수를 받지 않고, T를 반환하는 추상 메서드. () -> T
}

// Supplier의 get 메서드를 호출해서 새로운 Apple 객체를 만들 수 있다.
Supplier<Apple> c1 = () -> new Apple();
Apple a1 = c1.get();

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

Apple (Integer weight)라는 시그니처를 갖는 생성자는 Function 인터페이스의 시그니처와 같다. 따라서 다음과 같은 코드를 구현할 수 있다.

// Function.java
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t); // (T) -> R
}

// 람다 표현식
Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Apple a2 = c2.apply(110);

// 생성자 참조
Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);

 

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

List<Integer> weights = Arrays.asList(7, 3, 4, 10);
List<Apple> apples = map(weight, Apple::new);

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

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

BiFunction<Color, Integer, Apple> c3= Apple::new;
Apple a3 = c3.apply(GREEN, 110);

// 이 코드는 다음과 같다.
BiFunction<Color, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight);
Apple a3 = c3.apply(GREEN, 110);

 

 

인스턴스화하지 않고도 생성자에 접근할 수 있는 기능을 다양한 상황에 응용할 수 있다. 예를 들어 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())
            .apply(weight);
}

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

처음에 다룬 사과 리스트를 다양한 정렬 기법으로 정렬하는 문제로 다시 돌아가서 이 문제를 더 세련되고 간결하게 해결하는 방법을 보여주면서 3장에서 배운 람다를 마무리한다. 사과 리스트 정렬 문제를 해결하면서 지금까지 배운 동작 파라미터화, 익명 클래스, 람다 표현식, 메서드 잠조 등을 총동원한다.

 

3.7.1 1단계 : 코드 전달

자바 8의 List API에서 sort 메서드를 제공하므로 정렬 메서드를 직접 구현할 필요는 없다. sort 메서드는 다음과 같은 시그니처를 갖는다.

void sort(Comparator<? super E> c)

이 코드는 Comparator 객체를 인수로 받아 두 사과를 비교한다. `sort의 동작은 파라미터화 되었다`라고 말할 수 있다.

public class AppleComparator implements Comparator<Apple> {
	public int compare(Apple a1, Apple a2) {
		return a1.getWeight().compareTo(a2.getWeight());
	}
}

inventory.sort(new AppleComparator());

 

3.7.2 2단계 : 익명 클래스 사용

한 번만 사용할 Comparator를 위 코드처럼 구현하는 것보다 익명 클래스를 이용하는 것이 좋다.

inventory.sort(new Comparator<Apple>() {
	public int compare(Apple a1, Apple a2) {
		return a1.getWeight().compareTo(a2.getWeight());
	}
});

 

3.7.3 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 메서드를 사용할 수 있다.

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor
)

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

이제 코드를 다음처럼 간소화할 수 있다.

import static java.util.Compartor.comparing;
inventory.sort(comparing(apple -> apple.getWeight());

 

3.7.4 4단계 : 메서드 참조 사용

메서드 참조를 이용해서 코드를조금 간소화할 수 있다.

import static java.util.Comparator.comparing;

inventory.sort(comparing(apple -> apple.getWeight()));

코드 자체로 `Apple을 weight별로 비교해서 inventory를 sort하라`는 의미를 전달할 수 있다.

 


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

자바 8의 API의 몇몇 함수형 인터페이스는 다양한 유틸리티 메서드를 포함한다. 예를 들어 Comparator, Function, Predicate 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 메서드를 제공한다.

=> 간단한 여러 개의 람다 표현식을 조합해서 복잡한 람다 표현식을 만들 수 있다.

여기서 등장한느 것이 바로 `디폴트 메서드`이다.

 

3.8.1 Comparator 조합

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

Comparator<Apple> = Comparator.comparing(Apple::getWeight);

 

역정렬

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

default Comparator<T> reversed() {
	return Collections.reverseOrder(this);
}

inventory.sort(comapring(Apple::getWegiht).reversed());

 

Comperator 연결

thenComparing 메서드로 두 번째 비교자를 만들 수 있다. thenComparing은 함수를 인수로 받아 첫 번재 비교자를 이용해서 두 객체가 같다고 판단되면 두 번째 비교자에 객체를 전달한다. 즉, 다음처럼 문제를 해결할 수 있다.

default <U extends Comparable<? super U>> Comparator<T> thenComparing(Function<? super T, ? extends U> keyExtractor);

inventory.sort(comparing(Apple::getWeight)
				 .reversed()
				 .thenComparing(Apple::getCountry));

 

3.8.2 Predicate 조합

Predicate 인터페이스는 복잡한 프레디케이트를 만들 수 있도록 negate, and, or 세 가지 메서드를 제공한다. 예를 들어 `빨간색이 아닌 사과`처럼 특정 프레디케이트를 반전시킬 때 negate 메서드를 사용할 수 있다.

default Predicate<T> negate() {
	return (t) -> !test(t);
}

// 빨간색인 사과 → 빨간색이 아닌 사과
Predicate<Apple> notRedApple = redApple.negate();

또한 and 메서드를 이용해서 빨간색이면서 무거운 사과를 선택하도록 두 람다를 조합할 수 있다. 그뿐만 아니라 or을 이용해서 `빨간색이면서 무거운(150그램 이상) 사과 또는 그냥 녹색 사과`등 다양한 조건을 만들 수 있다.

default Predicate<T> and(Predicate<? super T> other) {
	Objects.requireNonNull(other);
	return (t) -> test(t) && other.test(t);
}

default Predicate<T> or(Predicate<? super T> other) {
	Objects.requireNonNull(other);
	return (t) -> test(t) || other.test(t);
}

// 빨간색인 사과 -> 빨간색이면서 무거운 사과
Predicate<Apple> redAndHeavyApple = 
		redApple.and(apple -> apple.getWeight() > 150);

// 빨간색이면서 무거운 사과 또는 그냥 녹색 사과
Predicate<Apple> redAndHeavyApple = 
		redApple.and(apple -> apple.getWeight() > 150)
						.or(apple -> GREEN.equals(a.getColor()));

 

 

3.8.3 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); // h(x) = g(f(x))
int result = h.apply(1) // result = 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); // h(x) = f(g(x))
int result = h.apply(1) // result = 3