Originally posted by **MyeoungDev** May 30, 2023
# 📕 Modern Java In Action Chapter_3
# Chapter3 람다 표현식
# 3.1 람다란 무엇인가?
**람다 표현식이란?**
→ 메서드로 전달할 수 있는 익명함수를 단순화 한 것
람다 표현식의 특징
- 익명
- 보통의 메서드와 달리 **이름이 없으므로** **익명**이라 표현한다.
- 함수
- 람다는 메서들처럼 **특정 클래스에 종속되지 않으므로 함수**라고 부른다.
- 전달
- 람다 표현식을 **메서드 인수로 전달하거나 변수로 저장할 수 있다.**
- 간결성
- 익명 클래스처럼 많은 자질구레한 코들르 구현할 필요가 없다.
즉, 람다를 사용하게 되면, 코드가 간결하고 유연해진다.
```java
/* 익명 클래스를 이용한 Comparaotr 구현 */
Comparator byWeight = new Comparator() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
```
```java
/* 람다 표현식을 이용한 코드 */
Comparator byWeight =
(Apple a1, Apple a2) -> a1.getWeight.compareTo(a2.getWeight());
```
람다 표현식은 파라미터, 화살표, 바디로 이루어진다.
![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/9c6368c9-7eac-46de-9c26-df52cb735384)
- 파라미터 리스트
- Comparator의 compare **메서드 파라미터**(Apple a1, Applea2)
- 화살표
- 화살표(→)는 **람다의 파라미터 리스트와 바디를 구분**한다.
- 람다 바디
- 두 사과의 무게를 비교한다. **람다의 반환값**에 해당하는 표현식이다.
```java
/* Java 8의 유요한 람다 표현식 */
// String 형식의 파라미터 하나를 가지며 int를 반환,
// 람다 표현식에는 return이 함축되어 있으므로 명시적으로 사용하지 않아도 된다.
(String s) -> s.length();
// Apple 형식의 파라미터 하나를 가지며 boolean을 반환한다.
(Apple a) -> a.getWeight() > 150
// int 형식의 파라미터 두 개를 가지며 리턴값이 없다. return type void
// 람다 표현식은 여러 행의 문장을 포함할 수 있다.
(int x, int y) -> {
System.out.println("Result: ");
System.out.println(x + y);
}
// Apple 형식의 파라미터 두개를 가지며 int를 반환한다.
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
```
그렇다면, 어디에서 람다를 사용할 수 있는건가?
정답은 `함수형 인터페에스(Functional Interface)` 라는 문맥에서 람다 표현식을 사용할 수 있다.
그럼 `Functinal Interface` 가 뭔지 알아보자.
# 3.2.1 함수형 인터페이스(FunctionalInterface)
`함수형 인터페이스(FunctionalInterface)`란?
→ **정확히 하나의 추상 메서드를 지정하는 인터페이스**
```java
/* 자바의 함수형 인터페이스인 것들 예시 */
// java.util.Comparator
public interface Comparator {
...
int compare(T o1, T o2);
...
}
// java.lang.Runnable
public interface Runnable {
...
void run();
...
}
// java.awt.event.ActionListener
public interface ActionListener extends EventListener {
...
void actionPerformed(ActionEvent e):
...
}
// java.util.concurrent.Callable
public interface Callable {
...
V call() throws Exception;
...
}
```
Java 8 부터 `default method` 를 통해 하나의 인터페이스에 많은 디폴트 메서드가 존재할 수 있다.
그러나, **많은 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스 이다.**
그렇다면, 함수형 인터페이스로 뭘 할 수 있을까?
→ 람다 표현식으로 **함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있다.**
→ **전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다.**
이를통해, 익명 클래스를 사용하는 방식보다 더 깔끔하고 간결한 코드를 작성할 수 있다.
```java
public static void process(Runnable r) {
r.run();
}
// 람다 표현식 사용
Runnable r1 = () -> System.out.println("Hello World Lamda");
// 익명 클래스 사용
Runnable r2 = new Runnable() {
public void run() {
System.out.println("Hello World Anonymous Class");
}
}
process(r1);
process(r2);
// 직접 전달된 람다표현식으로 출력
process(() -> System.out.println("Hello World Direct Transfer Lamda Expression");
```
그렇다면 우리가 한번씩 봤던 `@FunctinalInterface` 얘는 뭐하는 걸까?
→ `@FunctionalInterface` 는 함수형 인터페이스임을 가르키는 어노테이션이다.
→ 만약 해당 어노테이션을 선언 하였지만, 함수형 인터페이스가 아닌 경우에는 에러를 발생시킨다.
→ 예를 들어, 추상 메서드가 한 개 이상이라면 `Multiple nonoverriding abstract mehods found is interface `
## 함수 디스크립터(function descriptor)
함수형 인터페이스의 추상 메서드 시그니처(signature: 메서드 이름과 메서드 매게변수 리스트를 나타내는 것) 는 람다 표현식의 시그니처를 서술하는 메서드를 **함수 디스크립터(function descriptor) 라고 부른다.**
`Predicate` , `Consumer`, `Function` , `Supplier` 등이 자바에 존재하는 함수 디스크립터이다.
이에 대해서는 뒤에 더 자세히 설명하겠다.
여기까지는, 람다 표현식은 **변수에 할당하거나 함수형 인터페이스를 인수로 받는 메서드로 전달할 수 있으며,** **함수형 인터페이스의 추상 메서드와 같은 시그니처를 갖는다**는 사실을 기억하고 있으면 된다.
# 3.3 람다 활용 : 실행 어라운드 패턴
**실행 어라운드 패턴(Execute Around Pattern)** 이란?
→ 설정(SetUp) 과 정리(CleanUp) 두 과정이 둘러싸는 형태를 갖는 것을 실행 어라운드 패턴이라고 한다.
![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/2e9fb93e-d267-4dc7-bc20-02c4db96f686)
그렇다면 위와 같이 람다를 활용하여 실행 어라운드 패턴으로 아래 코드를 재구성 해보자!
> try-with-resources 구문을 사용하여 데이터 파일 읽어들이는 코드
>
```java
public String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}
/*
만약 여기서 한줄이 아닌 두줄을 읽고 싶다면?
두줄로 수정 한 후 두줄이 아닌 세줄이라면??
계속해서 읽어들이는 요구사항이 변한다면???
*/
```
> 함수형 인터페이스를 이용해서 동작 파라미터화 하기 위한 인터페이스 선언
>
```java
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
public String processFile(BufferedReaderProcessor p) throws IOException {
...
}
```
> 실행을 담당할 메서드 정의
>
```java
public String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
```
> 람다를 이용한 다양한 동작 전달해서 사용
>
```java
String oneLine = processFile((BufferedReader br) -> br.readLine());
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
```
위의 4가지 단계를 통해 실행 어라운드 패턴을 적용할 수 있다.
이는 람다와 동적 파라미터화로 유연하고 간결한 코드를 구현하는 데 도움을 주는 예제이다.
# 3.4 함수형 인터페이스 사용
Java 8 에서는 `java.util.function` 이라는 패키지로 여러 가지 새로운 함수형 인터페이스를 제공한다.
이는
- **Predicate**
- 제네릭 형식의 T의 객체를 인수로 받아 `test()` 라는 메서드를 통해 boolean을 반환한다.
```java
Predicate startsWithSeoul = (str) -> str.startsWith("Seoul");
System.out.println(startsWithSeoul.test("SeoulCity")); // true 출력
```
```java
/* Predicate를 이용한 필터링 하는 예제 */
public List filter(List list, Predicate p) {
List results = new ArrayList<>();
for(T t : list) {
if(p.test(t)) {
results.add(t);
}
}
return results;
}
Predicate nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
```
```java
Predicate redAndHeavyAppleOrGreen =
redApple.and(apple -> apple.getWeight() > 150)
.or(apple -> GREEN.equals(a.getColor()));
```
- **Consumer**
- 제네릭 형식 T의 객체를 인수로 받아 void를 반환하는 `accept()` 라는 추상 메서드를 정의한다.
```java
Consumer printCity = (city) -> System.out.println(city);
printCity.accept("Seoul");
```
```java
public void forEach(List list, Consumer c) {
for(T t : list) {
c.accept(t);
}
}
forEach(
Arrays.asList(1, 2, 3, 4, 5),
(Integer i) -> System.out.println(i)
}
```
- ****************************Function****************************
- 제네릭 형식 T의 객체를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 `apply()`를 정의한다.
```java
Function plus10 = (number) -> number + 10;
System.out.println(plus10.apply(10)); // 20 출력
```
```java
public List map(List list, Function f) {
List result = new ArrayList<>();
for(T t : list) {
result.add(f.apply(t));
}
}
List l = map (
Arrays.asList("lambdas", "in", "action"),
(String s) -> s.length()
);
```
- **Supplier**
- 매게변수를 받지 않고, 단순히 무언가를 반환하는 추상 메서드 `get()`이 존재한다.
```java
Supplier helloSupplier = () -> "Hello ";
System.out.println(helloSupplier.get() + "World");
// Hello World
```
---
아래의 코드는 해당 함수형 인터페이스를 이용하여 구현된 코드이다.
```java
/* java.util.Stream 라이브러리에 IntPipeline 에 구현되어있는 filter() 메서드 */
public final IntStream filter(final IntPredicate predicate) {
Objects.requireNonNull(predicate);
return new StatelessOp(this, StreamShape.INT_VALUE, StreamOpFlag.NOT_SIZED) {
Sink opWrapSink(int flags, Sink sink) {
return new Sink.ChainedInt(sink) {
public void begin(long size) {
this.downstream.begin(-1L);
}
public void accept(int t) {
if (predicate.test(t)) {
this.downstream.accept(t);
}
}
};
}
};
}
```
위의 코드를 보면 `IntPredicate` 형의 `Predicate` 타입의 매개변수를 받고 `test()` 메서드를 수행한 것을 볼 수있다.
또한, `downstream`변수의 경우 따라 올라가서 보게되면
```java
protected final Sink super E_OUT> downstream;
```
위와 같이 선언되어 있고 `Sink` 인터페이스의 경우
```java
interface Sink extends Consumer
```
위와 같이 선언되어 있다.
### 그런데 람다에서 예외처리는 어떻게?
**함수형 인터페이스는 확인된 예외를 던지는 동작을 허용하지 않는다.**
즉, 예외를 던지는 람다 표현식을 만들려면 확인된 **예외를 선언하는 함수형 인터페이스를 직접 정의**하거나,
람다를 `try/catch` 블럭으로 감싸야 한다.
```java
Function f = (BufferedReader b) -> {
try {
return b.readLine();
} catch(IOException e) {
throw new RuntimeException(e);
}
};
```
# 3.5 형식 검사, 형식 추론, 제약
**람다 표현식 자체에는** 람다가 **어떤 함수형 인터페이스를 구현하는지의 정보가 포함되어 있지 않다.**
그렇다면, 컴파일러는 어떻게 람다의 형식을 확인할까?
![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/43707444-4520-4eec-a5b0-7115d260fd6f)
위의 사진은 람다 표현식의 형식 검사 과정을 나타내는 것이다.
1. `filter` 메서드의 선언을 확인한다.
2. `filter` 메서드는 두 번째로 파라미터로 `Predicate` 형식을 기대한다.
3. `Predicate`은 `test`라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스이다.
4. `test` 메서드는 Appl을 바아 boolean을 반환하는 함수 디스크립터를 묘사한다.
5. `filter` 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 한다.
이와 같이 **대상 형식(Traget Typing)**이라는 특징 때문에 **같은 람다 표현식이라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있다.**
```java
/* Callable 과 PrivilegedAction 모두 인수를 받지않고 제네릭 형식 T를 반환하는 함수형 인터페이스*/
Callable c = () -> 42;
PrivilegedAction p = () -> 42;
```
그렇다면 아래의 경우는 어떻게 될까?
```java
Object o = () -> {System.out.println("Tricky example");
```
이 경우는 **컴파일 에러**가 나게 된다.
그 이유는 해당 람다식의 경우 **어떠한 함수형 인터페이스를 참조해야 할지 명확하지 않기 때문**이다.
이와 같은 경우는 아래와 같이 **어떤 메서드의 시그니처가 사용되어야 하는지를 명시적으로 구분하도록 람다를 캐스트 하여 사용**하면 된다.
```java
Object o = (Runnable) () -> {System.out.println("Tricky example")};
```
이렇듯 자바 컴파일러는 람다 표현식과 관련된 함수형 인터페이스를 추론한다.
함수형 인터페이스의 추론이 올바르게 되었다면, **파라미터 형식도 추론이 가능하게 된다.**
즉, 파라미터의 형식을 명시적으로 작성할 수도 있고, 생략할 수도 있다.
```java
/* 파라미터 형식을 추론하지 않음*/
Comparator c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
/* 파라미터 형식을 추론*/
Comparator c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
```
어느 코드가 더 좋은가? 그건 잘 모르겠다.
정해진 규칙은 없고, 개발자 스스로 어떤 코드가 가독성을 향상시킬 수 있는지 경정해야 한다.
### 지역 변수의 제약
람다 표현식에서는 익명 함수가 하는 것처럼 **자유 변수(free variable: 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)**를 사용할 수 있다.
이와 같은 동작을 **람다 캡처링(capturing lambda)** 라고 부른다.
```java
int portNumber = 1234;
Runnable r = () -> System.out.println(portNumber);
```
하지만, 자유 변수에도 제약이 존재한다.
람다 표현식은 한 번만 할당할 수 있는 지역 변수를 캡처할 수 있다.
```java
/* 할당 되었던 portNumber가 재할당 되면서 컴파일 에러가 발생한다. */
int portNumber = 1234;
Runnable r = () -> System.out.println(portNumber);
int portNumber = 1111;
```
이를 제약사항을 걸어둔 이유는, 다른 스레드에서 변수 할당이 해제되었는데, 해당 변수를 다른 스레드에서 접근하려 할 수 있다.
따라서, 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴 것이다.
# 3.6 메서드 참조
메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다.
```java
/* 기존 코드 */
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
/* 메서드 참조 코드*/
inventory.sort(comparing(Apple::getWeight));
```
그렇다면 이 메서드 참조가 왜 중요한가?
- 메서드 호출에서 어떻게 메서드를 호출해야하는지 설명보다 **메서드명을 직접 참조하는 것이 편리하다.**
- **기존 메서드 구현으로 람다 표현식을 만들 수 있다.**
- **명시적으로 메서드명을 참조함으로써 가독성을 높일 수 있다.**
컴파일러는 람다 표현식의 형식을 검사하던 방식과 비슷한 과정으로 메서드 참조가 주어진 함수형 인터페이스와 호환하는지 확인한다.
즉, 메서드 참조는 콘텍스트의 형식과 일치해야 한다.
### 생성자 참조
`ClassName::new` 처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다.
```java
/* Supplier를 이용한 방식*/
Supplier c1 = Apple::new;
Apple a1 = c1.get();
/* Function을 이용한 방식*/
Function c2 = Apple::new;
Apple a2 = c2.apply(110);
/* 위를 응용하여 다양한 무게를 갖는 사과 리스트를 만드는 방식 */
List weights = Arrays.asList(100, 120, 140, 150);
List apples = map(weights, Apple::new);
public List map(List list, Function f) {
List result = new ArrayList<>();
for(Integer i : list) {
result.add(f.apply(i));
}
return result;
}
```
# 3.10 요약, 마침
- **람다 표현식은 익명 함수의 일종이다**. 이름은 없지만, 파라미터 리스트, 바디, 반환 형식을 가지며 예외를 던질 수 있다.
- 람다 표현식으로 간결한 코드를 구현할 수 있다.
- **함수형 인터페이스는 하나의 추상 메서드만을 정의하는 인터페이스이다.**
- 함수형 인터페이스를 기대하는 곳에서만 람다 표현식을 사용할 수 있다.
- **람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급된다.**
- java.util.function 패키지에는 자주 사용하는 다양한 함수형 인터페이스를 제공한다.
- 실행 어라운드 패턴을 람다와 활용하면 유연성과 재사용성을 추가로 얻을 수 있다.
- 메서드 참조를 이용하면 기존의 메서드 구현을 재사용하고 직접 전달할 수 있다.
- `Comparator`, `Predicate` , `Function` 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있는 다양한 디폴트 메서드를 제공한다.
Discussed in https://github.com/FeGwan-Training/FeGwan/discussions/10