> For the complete documentation index, see [llms.txt](https://develop-footprint.gitbook.io/o/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://develop-footprint.gitbook.io/o/java/modern-java-in-action/3.md).

# 3장: 람다

## 람다

* 메서드로 전달할 수 있는 익명 함수를 단순화한 것
* 특징
  * 익명: 일반적인 메서드와 달리 이름이 없어, 구현해야 할 코드에 대한 걱정거리가 줄어든다.
  * 함수: 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다. 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 발생 가능한 예외 리스트를 포함한다.
  * 전달: 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
  * 간결성: 익명 클래스처럼 불필요한 선언 등의 코드를 작성할 필요가 없다.
* 익명 클래스에서 구현했던 메서드의 바디만 직접 전달하는 것과 비슷하다.

### 람다의 구성

<figure><img src="/files/8adExe8EOyindMTKQBah" alt=""><figcaption><p><a href="https://www.geeksforgeeks.org/java-lambda-expressions-parameters/">https://www.geeksforgeeks.org/java-lambda-expressions-parameters/</a></p></figcaption></figure>

* 파라미터 리스트
  * 람다 바디에서 사용되는 파라미터를 입력받는다.
* 화살표(→)
  * 화살표는 람다의 파라미터 리스트와 바디를 구분한다.
* 람다 바디
  * 람다의 반환값에 해당하는 표현식이다.

### 스타일

* 일반 표현식 스타일
  * `(parameters) -> expression`
  * 람다 표현식에는 return이 함축되어 있으므로 return을 명시적으로 사용하지 않아도 된다.
  * `(String s) -> "Iron Man"` 형태는 파라미터를 입력받아 "Iron Man"을 return하는 람다 식이다.
* 블록 스타일
  * (parameters) -> { statements; }
  * 블록에는 여러 행의 문장을 포함하는 구문이 들어가며, 리턴 타입이 void가 아니라면 return을 명시적으로 사용해야한다.
  * `() -> {}` 형태는 파라미터가 없는 void 람다 식이다.
  * `(Integer i) -> {return "Alan" + i;}` 형태는 파라미터를 입력받아 특정 문자열을 반환하는 람다 식이다.

## 람다 사용하기

* [함수형 인터페이스](#undefined-5)를 입력인자로 받는 경우 람다를 사용할 수 있다.

### 실행 어라운드 패턴

* DB 등의 자원을 처리할 때 자원을 초기화/준비 -> 작업 처리 -> 자원을 정리/마무리 하는 순서로 이뤄진다.
* 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 실행 어라운드 패턴이라고 부른다.
* 아래는 try-with-resources 구문을 사용해 파일에서 한 행을 읽는 코드이다.

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

* 이제 함수형 인터페이스를 이용해 동작을 전달해보자. 정의한 `BufferedReaderProcessor` 인터페이스를 processFile 메서드의 인수로 전달할 수 있다.

```java
@FuntionalInterface
public interface BufferedReaderProcessor {
	String process(BuffredReader b) throws IOException;
}
```

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

* 다양한 동작들을 람다식 형태로 processFile 메서드에 전달할 수 있다.

```java
// 한 행을 처리하는 코드
String oneLine = processFile((BufferedReader br) -> br.readLine());

// 두 행을 처리하는 코드
Strine twoLines = processFile((BuffredReader br) -> br.readLine() + br.readLine());
```

## 함수형 인터페이스

### 개념

* 정확히 **하나의 추상 메서드를 지정**하는 인터페이스
* 여러 디폴트 메서드가 있어도 추상 메서드가 하나뿐이면 함수형 인터페이스다.
* 다음은 함수형 인터페이스의 예시이다.

```java
public interface Comparator<T> {
    int compare(T o1, T o2);
}

public interface Runnable {
    void run();
}

public interface Callable<V> {
    V call() throws Exception;
}
```

* 람다 표현식으로 이러한 함수형 인터페이스의 추상 메서드를 구현해 직접 전달할 수 있다.

```java
// 람다로 Runnable의 구현체 생성 가능
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);
// Runnable 구현체를 미리 생성해두지 않고 바로 만들어 사용 가능
process(() -> System.out.println("Hello Wordl 3"));
```

### 함수형 인터페이스 종류

#### Predicate\<T>

* test라는 추상 메서드를 정의한다.
* test 메서드는 제네릭 형식 T의 객체를 인수로 받아 boolean 값을 반환한다.
* 다음 코드는 Predicate 인터페이스를 구현해 String 객체가 Empty가 아닌 경우를 필터링한다.

```java
public <T> List<T> filter(List<T> list, Predicate<T> p) {
	List<T> results = new ArrayList<>();
	for(T t: list) {
		if(p.test(t)) {
			results.add(t);
		}
	}
	return results;
}

// T 객체를 입력받아 boolean을 반환하도록 한다.
Predicate<String> nonEmptyStringPredicate = (String) -> !s.isEmpty();

// String 리스트 중 Empty가 아닌 String만 필터링한다.
List<String> nonEmpty= filter(listOfStrings, nonEmptyStringPredicate);
```

#### Consumer\<T>

* 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의한다.
* T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있다.
* 다음은 Consumer와 람다를 이용해서 리스트의 모든 항목을 출력하는 예제다.

```java
public <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 메서드를 람다로 구현
);
```

#### Function\<T, R>

* 제네릭 형식 T 객체를 인수로 받아 제네릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의한다.
* 입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있다.
  * ex) 사과를 입력받아 사과의 무게 정보만 추출, 문자열을 입력받아 문자열의 길이만 추출
* 다음 코드는 String 리스트를 인수로 받아 각 String의 길이를 포함하는 Integer 리스트로 반환한다.

```java
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()
);
```

#### primitive type을 위한 함수형 인터페이스

* 제네릭 파라미터에는 reference type(참조형)만 입력받을 수 있다.
* 따라서 기본형을 입출력으로 사용할 때 자동으로 박싱 타입으로 변환(오토박싱)하는 과정을 피할 수 있도록 각 primitive type을 위한 함수형 인터페이스를 제공한다.
* 특정 형식을 입력받는 함수형 인터페이스의 경우 IntPredicate, DoublePredicate, LongBinaryOperator , IntFunction 처럼 타입명이 함께 붙는다.

```java
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0;
addNumbers.test(1000);  // int -> Integer로 박싱 과정이 발생

IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000); // int -> int 그대로 사용하므로 박싱 과정이 발생 x
```

#### 다양한 함수형 인터페이스 정리

<figure><img src="/files/VlZsboLxGx8YKDg4ZmZp" alt=""><figcaption><p><a href="https://javaconceptoftheday.com/java-8-functional-interfaces/">https://javaconceptoftheday.com/java-8-functional-interfaces/</a></p></figcaption></figure>

### 함수 디스크립터

* 함수 디스트립터(function descriptor): 람다 표현식의 시그니처를 서술하는 메서드
* 함수형 인터페이스의 추상 **메서드 시그니처**(입력 인자, 반환값 등 메서드의 특성)는 람다 표현식의 시그니처와 동일해야 한다.
* 아래와 같이 함수형 인터페이스의 추상 메서드 시그니처와 람다의 시그니처가 다르다면 유효하지 않은 람다식이 된다. (Predicate는 boolean을 반환해야 하는데, 람다 식에서는 int를 반환하고 있어 메서드 시그니처 불일치!)

```java
Predicate<Apple> p = (Apple a) -> a.getWeight();
```

### @FunctionalInterface

* 함수형 인터페이스임을 가리키는 어노테이션
* 함수형 인터페이스가 아니라면 컴파일 에러가 발생한다.

### 예외 처리

* 아래와 같이 Checked Exception인 IOException을 명시적으로 선언하는 함수형 인터페이스를 만들었다면,  예외를 던질 수 있는 람다를 사용해도 된다.

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

BufferedReaderProcessor p = (BufferedReader br) -> br.readline();
```

* 하지만 자바에서 제공하는 함수형 인터페이스의 경우 Checked Exception을 던지는 동작을 허용하지 않는다.
* 따라서 아래와 같이 람다식 내부에서 try/catch 블록으로 Checked Exception을 처리해주어야 한다.

```java
Function<BufferedReader, String> f = (BufferedReader b) -> { 
    try { 
        return b.readLine();
    } 
    catch (IOException e) {
        throw new RuntimeException(e);
    }
};
```

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

* 람다 표현식 자체에 어떤 함수형 인터페이스를 구현하는지 정보가 포함되어 있지 않으므로 람다의 형식을 파악해야 한다. 람다의 형식을 파악하기 위해 다음 세 가지의 과정을 거친다.

### 형식 검사

* 람다가 사용되는 콘텍스트(context)를 이용해서 람다의 형식을 추론할 수 있다.
* 어떤 콘텍스트(ex. 람다가 전달될 메서드 파라미터나 람다가 할당되는 변수 등)에서 기대되는 람다 표현의 형식을 **대상 형식**(target type)이라고 부른다.

```java
List<Apple> heavierThan150g = 
	filter(inventory, (Apple apple) -> apple.getWeight() > 150);
```

* 형식 검사 과정은 다음과 같다.

1. 람다가 사용된 콘텍스트를 파악하기 위해 람다를 입력 인자로 받는 메서드의 선언을 먼저 확인한다.

   메서드는 파라미터로 Predicate\<Apple> 형식(**대상 형식**)을 기대하며, Predicate 함수형 인터페이스는 test라는 한 개의 추상 메서드를 정의한다.
2. test 메서드의 시그니처를 통해 Apple 객체를 입력받아 boolean을 반환하는 함수 디스크립터를 확인할 수 있다.
3. 함수 디스크립터와 람다의 시그니처(Apple 객체를 입력받아 boolean을 반환)를 비교하여 일치하면 형식 검사를 완료한다.

* 하나의 람다 표현식은 다양한 함수형 인터페이스에 할당될 수 있다.
* **대상 형식**이 다르더라도 람다 표현식과 호환되는 추상 메서드를 가진다면 아래처럼 정상적으로 할당된다.

```java
Callable<Integer> c = () -> 42;
PriviligedAction<Integer> p = () -> 42;
```

> 람다의 바디에 [일반 표현식](#undefined-2)이 있다면 void를 반환하지 않더라도 void 반환하는 함수 디스크립터와 호환된다.
>
> 예를들어 list.add(s) 메서드는 boolean을 반환하지만, 다음과 같이 작성하면 유효하게 할당되며 반환되는 값은 무시된다.
>
> `Consumer<String> cs = s -> list.add(s);`

> 아래처럼 같은 함수형 디스크립터를 가진 두 함수형 인터페이스를 갖는 메서드를 오버로딩할 때 명시적으로 함수형 인터페이스로 캐스팅하여 분명하게 할 수 있다.
>
> public void **execute**(Runnable runnable) {
>
> &#x20;   runnable.run();
>
> }
>
> public void **execute**(Action\<T> action) {
>
> &#x20;   action.act();
>
> }
>
> execute((Action) () -> {});

### 형식 추론

* 자바 컴파일러는 람다 표현식이 사용된 콘텍스트(대상 형식)를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다.
* 즉, 대상 형식을 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론할 수 있다.
* 결과적으로 컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있으므로 람다 문법에서 이를 생략할 수 있다.
* 자바 컴파일러는 다음처럼 람다 파라미터 형식을 추론할 수 있다.

```java
// 형식을 지정해주어 형식 추론이 필요 없음
List<Apple> greenApples =
	filter(inventory, Apple a -> GREEN.equals(a.getColor()));

// 형식을 지정하지 않아 형식 추론이 필요함
List<Apple> greenApples =
	filter(inventory, a -> GREEN.equals(a.getColor()));
```

### 제약

* 람다 캡처링(capturing lambda): 익명 함수처럼 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 **자유 변수**(free variable)를 활용할 수 있다
* 람다는 인스턴스 변수와 정적 변수 모두 자유롭게 사용할 수 있지만, 한번만 할당할 수 있도록 final로 선언되거나 final 변수와 동일한 상태여야만 한다.
* 그렇지 않다면 `Variable used in lambda expression should be final or effectively final` 컴파일 오류가 발생한다.

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

* 이러한 제약이 있는 이유는 람다가 스레드에서 실행될 때, 변수를 할당한 스레드가 사라져 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있어 복사본의 값이 바뀌지 않도록 해야 하기 때문이다.
* 람다에 값을 복사해서 사용하는 이유는 인스턴스 변수는 힙에 저장되고 지역 변수는 스택에 위치하는데, 스택은 스레드마다 고유한 영역이므로 다른 스레드에서 접근할 수 없다. 따라서 다른 스레드의 람다 표현식에서도 해당 값에 접근할 수 있도록 복사해서 사용한다.

> **클로저**
>
> 함수의 비지역 변수를 자유롭게 참조할 수 있는 함수의 인스턴스를 가리킨다.
>
> 예를 들어 클로저를 다른 함수의 인수로 전달할 수 있다. 클로저 외부에 정의된 변수의 값에 접근하고, 값을 바꿀 수 있다.
>
> 자바8의 람다와 익명 클래스는 클로저와 비슷한 동작을 수행하지만, 람다와 익명 클래스는 람다가 정의된 메서드의 지역 변수의 값은 바꿀 수 없다. 람다가 정의된 메서드의 지역 변수 값은 final 변수여야 한다.
>
> 지역 변수 값은 스택에 존재하므로 자신을 정의한 스레드와 생존을 같이 해야하며, 따라서 지역 변수는 final이어야 한다.
>
> 가변 지역 변수를 새로운 스레드에서 캡처할 수 있다면 안전하지 않은 동작을 수행할 가능성이 생긴다.

## 메서드 참조

* 특정 메서드만을 호출하는 람다를 축약해서 표현하는 방식
* 메서드명 앞에 구분자 '::' 를 붙여 메서드 참조를 활용 가능하다.
* 기존의 메서드 정의를 재활용해 람다처럼 전달 가능하다.
* 메서드 참조와 주어진 함수형 인터페이스의 디스크립터와 호환되어야 한다.
* 컴파일러는 람다 표현식의 형식을 검사하던 방식과 비슷하게 **메서드 참조가 주어진 함수형 인터페이스와 호환되는지 확인**한다.

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

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

### 유형

1. 정적 메서드 참조
   * `클래스이름::정적메서드이름` 형태로 참조할 수 있다.
   * Intege의 정적 메서드 parseInt()를 람다로 표현하면 `Integer.parseInt()`
   * 메서드 참조로 표현하면 `Integer::parseInt`
2. 다양한 형식의 인스턴스 메서드 참조
   * `클래스이름::인스턴스메서드이름` 형태로 참조할 수 있다.
   * String의 length()를 람다로 표현하면 `(String s) -> s.length()`
   * 메서드 참조로 표현하면 `String::length`
3. 기존 객체의 인스턴스 메서드 참조
   * `객체이름::인스턴스메서드이름` 형태로 참조할 수 있다.
   * expensiveTransaction.getValue()를 람다로 표현하면 `() -> expensiveTransaction.getValue()`
   * 메서드 참조로 표현하면 `expensiveTransaction::getValue`
   * 공통적으로 반복되는 작업을 수행하도록 만든 **비공개 헬퍼 메서드**를 유용하게 사용할 수 있다.

```java
// 비공개 헬퍼 메서드 예시
private boolean isValidName(String string) {
	return Character.isUpperCase(string.charAt(0));
}

filter(list, this::isValidName); // filter(list, (String s) -> isValidName(s)); 와 같은 의미
```

#### 생성자 참조

* new 키워드를 사용해 생성자의 참조를 만들 수 있다.
* 인수가 없는 생성자의 경우 Supplier 함수형 인터페이스의 시그니쳐와 같다.

```java
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();
```

* 인수가 하나인 생성자의 경우 Function 함수형 인터페이스의 시그니처와 같다.

```java
Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);
```

* 인수가 두개인 생성자의 경우 BiFunction 함수형 인터페이스의 시그니처와 같다.

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

* 인수를 세 개 이상 받고 싶은 경우, 직접 시그니처를 갖는 함수형 인터페이스를 만들어 사용해야 한다.&#x20;

```java
public interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
}

TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new;
```

* 다음과 같이 giveMeFruit 메서드를 호출할 때 Fruit의 이름과 무게를 넣어 실제 객체가 생성되도록 만들 수도 있다.

```java
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);
}
```

## 마무리

* 함수형 인터페이스 구현체로 시작해 람다, 메서드 참조를 활용하는 것까지의 진화과정을 살펴보자.
* 함수형 인터페이스를 직접 구현한 클래스를 넘긴다.

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

inventory.sort(new AppleComparator());
```

* 익명 클래스를 넘긴다.

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

* 람다 표현식을 넘긴다.

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

* 정적 메서드인 comparing()을 사용해 간결화한다.

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

* 정적 메서드를 메서드 참조하여 간결화한다.

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

## 람다 표현식 조합을 위한 유용한 메서드

* 몇몇 함수형 인터페이스는 람다 표현식 조합을 위해 유용한 메서드들을 제공한다.
* default method를 사용해 제공하여 함수형 인터페이스의 정의를 벗어나지 않는다.
* 단순 람다 표현식을 조합해 복잡한 람다 표현식을 만들 수 있으며, 가독성도 해치지 않는다.

### Comparator

#### reversed()

* 주어진 비교자의 순서를 뒤바꾸는 default 메서드를 제공하여 역정렬할 수 있다.

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

inventory.sort(comparing(Apple::getWegiht).reversed());
```

#### thenComparing()

* Comparator를 여러 개 나열해서 처음 비교한 값이 같다면 다음 비교자 기준으로 비교하도록 할 수 있다.

```java
default Comparator<T> thenComparing(Comparator<? super T> other) {
    Objects.requireNonNull(other);
    return (Comparator<T> & Serializable) (c1, c2) -> {
        int res = compare(c1, c2);
        return (res != 0) ? res : other.compare(c1, c2);
    };
}

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

### Predicate

#### negate()

* 특정  Predicate를 반전시키는 메서드이다.
* 아래처럼 사과 색이 빨강이면 true를 반환하는 람다를 negate 메서드를 통해 반전시킬 수 있다.

```java
Predicate<Apple> notRedApple = redApple.negate();
```

#### and(), or()

* and 메서드와 or 메서드를 이용해 "빨갛고 150g 이상인 사과 또는 녹색 사과일 때 true를 반환"하도록 할 수 있다.

```java
Predicate<Apple> redAndHeavyApple = 
		redApple.and(apple -> apple.getWeight() > 150)
			.or(apple -> GREEN.equals(a.getColor()));
```

### Function

#### andThen()

* 주어진 함수 적용 결과를 다른 함수의 입력으로 전달한다.
* 다음 예제의 경우 f 함수 먼저 수행한 후 g 함수를 수행하게 된다.

```java
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()

* 입력 인자로 전달된 함수를 먼저 실행한 후 결과를 외부의 함수에 전달한다.
* 아래 예제의 경우 g 함수를 먼저 수행한 후 f 함수를 수행한다.

```java
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
```

&#x20;
