✏️[모던 자바 인 액션, 전문가를 위한 자바 8, 9, 10 기법 가이드] 스터디 관련 책 내용을 정리한 글입니다.
📌 이 장의 내용
- 람다란 무엇인가?
- 어디에, 어떻게 람다를 사용하는가?
- 실행 어라운드 패턴
- 함수형 인터페이스 사용
3.1 람다란 무엇인가?
람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것입니다.
람다의 특징은 다음과 같습니다.
- 익명 : 보통의 메서드와 달리 이름이 없으므로 익명이라 표현합니다.
- 함수 : 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부릅니다.
- 전달 : 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있습니다.
- 간결성 : 익명 클래스처럼 자질구레한 코드를 구현할 필요가 없으므로 람다 표현식은 늘 간결하고 유연합니다.
람다 표현식을 사용하면 동작 파라미터 형식의 코드를 더 쉽게 구현할 수 있으며, 이를 통해 코드가 더 간결하고 유연해집니다.
또한, 스트림 API를 사용해 람다 표현식을 이용하면 코드를 더 간결하게 구현할 수 있습니다.
예를 들어, 커스텀 Comparator 객체를 기존 코드보다 람다를 이용해서 간단하게 구현할 수 있습니다.
// 기존 코드
Comparator<Apple> byWeight = new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
};
// 람다 표현식을 이용하면 compare 메서드의 바디를 직접 전달하는 것처럼 코드를 전달할 수 있습니다.
Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
람다 표현식은 파라미터, 화살표, 바디로 이루어집니다.
- 람다 파라미터(파라미터 리스트) : Comparator의 compare 메서드 파라미터 (사과 두 개)
- 화살표 : 화살표(->)는 람다 파라미터 리스트와 바디를 구분
- 람다 바디 : 람다의 반환값에 해당하는 표현식
3.2 어디에, 어떻게 람다를 사용하는가?
다음은 이전 장에서 구현했던 필터 메서드에서 람다를 활용한 예제입니다.
List<Apple> greenApples = filter(inventory, (Apple a) -> GREEN.equals(a.getColor()));
함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있습니다.
위 예제에서는 함수형 인터페이스 Predicate <T>를 기대하는 filter 메서드의 두 번째 인수로 람다 표현식을 전달했습니다.
3.2.1 함수형 인터페이스
2장에서 만든 Predicate <T>가 함수형 인터페이스입니다. Predicate <T>는 오직 하나의 추상 메서드만 지정하기 때문입니다.
public interface Predicate<T> {
boolean teat(T t);
}
함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스로, 예를 들어 Comparator, Runnable 등이 있습니다.
public interface Comparator<T> { // java.util.Comparator
int compare(T o1, T o2);
}
public interface Runnable { // java.lang.Runnable
void run();
}
람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있습니다. 함수형 인터페이스보다 덜 깔끔하지만 익명 내부 클래스로도 같은 기능을 구현할 수 있습니다.
다음 예제는 Runnable이 오직 하나의 추상 메서드 run을 정의하는 함수형 인터페이스이므로 올바른 코드입니다.
Runnable r1 = () -> System.out.println("Hello World 1"); // 람다 사용
Runnable r2 = new Runnable() { // 익명 클래스 사용
public void run() {
System.out.println("Hello World2");
}
};
public static void process(Runnable r) {
r.run();
}
process(r1); // "Hello World 1" 출력
process(r2); // "Hello World 2" 출력
process() -> System.out.println("Hello World 3")) // 직접 전달된 람다 표현식으로 "Hello World 3" 출력
3.2.2 함수 디스크립터
함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킵니다. 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부릅니다. 예를 들어, Runnable 인터페이스의 유일한 추상 메서드 run은 인수와 반환값이 없으므로, Runnable 인터페이스는 인수와 반환값이 없는 시그니처로 생각할 수 있습니다. 이러한 함수 디스크립터를 이용하여 람다 표현식의 시그니처를 확인하고 함수형 인터페이스와 연결하여 사용할 수 있습니다.
람다와 함수형 인터페이스를 가리키는 특별한 표기법은 ( ) -> void와 (Apple, Apple) -> int와 같이 사용됩니다.
( ) -> void 표기는 파라미터 리스트가 없으며 void를 반환하는 함수를 의미합니다. 이러한 함수형 인터페이스는 Runnable과 같은 인터페이스와 연결될 수 있습니다.
(Apple, Apple) -> int는 두 개의 Apple을 인수로 받아 int를 반환하는 함수를 가리킵니다. 이러한 함수형 인터페이스는 Comparator와 같은 인터페이스와 연결될 수 있습니다.
즉, 람다와 함수형 인터페이스를 연결하기 위해서는 람다 표현식이 구현하고자 하는 함수형 인터페이스의 시그니처를 ( ) -> void나 (Apple, Apple) -> int와 같은 특별한 표기법으로 표현할 수 있어야 합니다.
예를 들어 이전 예제에서는 다음처럼 process 메서드에 직접 람다 표현식을 전달했습니다.
public void process(Runnable r) {
r.run();
}
process(() -> System.out.println("This is awesome!!"));
( ) -> System.out.println("This is awesome!!")은 인수가 없으며 void를 반환하는 람다 표현식입니다.
이는 Runnable 인터페이스의 run 메서드 시그니처와 같습니다.
람다와 메서드 호출
다음은 정상적인 람다 표현식입니다.
process(() -> System.out.println("This is awesome"));
위 코드에서는 중괄호를 사용하지 않고 System.out.println은 void를 반환하므로 완벽한 표현식은 아닙니다. 중괄호로 감싸면 다음과 같이 구현됩니다.
process(() -> { System.out.println("This is awesome"); });
결론적으로는 자바 언어 명세에서 void를 반환하는 메서드 호출과 관련한 특별한 규칙을 정하고 있기 때문에 중괄호는 필요가 없습니다. 즉, 한 개의 void 메서드 호출은 중괄호로 감쌀 필요가 없습니다.
@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단계 : 동작 파라미터화를 기억하라
현재 코드에서는 파일에서 한 줄만 읽을 수 있습니다. 그러나 두 줄을 읽거나 가장 자주 사용되는 단어를 반환하려면, 기존의 설정, 정리 과정은 재사용하고 processFile 메서드만 다른 동작을 수행하도록 명령할 수 있어야 합니다. 이때, processFile 메서드의 동작을 파라미터 화하며 BufferedReader를 이용해서 다른 동작을 수행할 수 있도록 해야 합니다. 이를 위해 람다를 사용하여 동작을 전달할 수 있습니다. 만약 processFile 메서드가 한 번에 두 줄을 읽게 하려면, BufferedReader를 인수로 받아서 String을 반환하는 람다가 필요합니다.
다음은 BufferedReader에서 두 행을 출력하는 코드입니다.
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
3.3.2 2단계 : 함수형 인터페이스를 이용해서 동작 전달
함수형 인터페이스 자리에 람다를 사용할 수 있습니다. 따라서 BufferedReader -> String과 IOException은 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 합니다.
BufferedReaderProcessor라는 인터페이스를 정의하면 다음과 같습니다.
@FunctionalInteface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
// 정의한 인터페이스를 processFile 메서드의 인수로 전달할 수 있습니다.
public String processFile(BufferedReaderProcessor p) throws IOException {
...
}
3.3.3 3단계 : 동작 실행
BufferedReaderProcessor 클래스에 process 메서드의 시그니처와 일치하는 람다를 processFile 메서드에 전달할 수 있습니다. 이때 전달된 람다의 코드는 processFile 메서드 내부에서 실행됩니다.
람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으며, 전달된 코드는 함수형 인터페이스의 인스턴스로 전달된 코드와 같은 방식으로 처리합니다. 따라서 processFile 바디 내에서 BufferedReaderProcessor 객체의 process를 호출할 수 있습니다.
public String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br); // BufferedReader 객체 처리
}
}
3.3.4 4단계 : 람다 전달
이제 람다를 이용해서 다양한 동작을 processFile 메서드로 전달할 수 있습니다.
// 다음은 한 행을 처리하는 코드입니다.
String oneLine = processFile((BufferedReader br) -> br.readLine());
// 다음은 두 행을 처리하는 코드입니다.
String oneLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());
3.4 함수형 인터페이스 사용
함수형 인터페이스는 오직 하나의 추상 메서드를 지정합니다. 함수형 인터페이스의 추상 메서드는 람다 표현식의 시그니처를 묘사합니다.
함수형 인터페이스의 추상 메서드 시그니처를 함수 디스크립터라고 합니다. 다양한 람다 표현식을 사용하려면 공통의 함수 디스크립터를 기술하는 함수형 인터페이스의 집합이 필요합니다.
3.4.1 Predicate
java.util.function.Predicate <T> 인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 불리언을 반환합니다. 따로 정의할 필요 없이 바로 사용할 수 있는 인터페이스입니다. T 형식의 객체를 사용하는 불리언 표현식이 필요한 상황에서 Predicate 인터페이스를 사용할 수 있습니다.
다음은 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)) {
results.add(t);
}
}
return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
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 <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 메서드를 정의하는 예제입니다.
@FunctionalInteface
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;
}
// [7, 2, 6]
List<Integer> l = map (
Arrays.asList("lambdas", "in", "action"),
(String s) -> s.length() // Function의 apply 메서드를 구현하는 람다
);
기본형 특화
자바의 모든 형식은 참조형 아니면 기본형에 해당합니다. 하지만, 제네릭 파라미터는 참조형만 사용할 수 있습니다. 자바에서는 기본형을 참조형으로 변환하는 기능을 제공하는데, 이 기능을 박싱이라고 합니다. 참조형을 기본형으로 변환하는 반대 동작은 언박싱이라고 합니다.
이 박싱과 언박싱을 자동으로 이루어지는 기능인 오토박싱이라는 기능도 제공합니다.
자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공합니다.
'Study > Modern Java in Action[2회독]' 카테고리의 다른 글
[모던 자바 인 액션] 3장 동작 람다 표현식(2) (1) | 2023.12.04 |
---|---|
[모던 자바 인 액션] 2장 동작 파라미터화 코드 전달하기 (0) | 2023.12.02 |
[모던 자바 인 액션] 1장 Java 8, 9, 10, 11 : 무슨 일이 일어나고 있는가? (0) | 2023.11.24 |
[모던 자바 인 액션] 20장 OOP와 FP의 조화 : 자바와 스칼라 비교 (0) | 2023.04.02 |
[모던 자바 인 액션] 19장 함수형 프로그래밍 기법 (0) | 2023.04.02 |