✏️[모던 자바 인 액션, 전문가를 위한 자바 8, 9, 10 기법 가이드] 스터디 관련 책 내용을 정리한 글입니다.
📌 이 장의 내용
- 형식 검사, 형식 추론, 제약
- 메서드 참조
- 람다, 메서드 참조 활용하기
- 람다 표현식을 조합할 수 있는 유용한 메서드
- 비슷한 수학적 개념
3.5 형식 검사, 형식 추론, 제약
람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지의 정보가 포함되어 있지 않습니다. 따라서 람다 표현식을 제대로 이해하려면 람다의 실제 형식을 파악해야 합니다.
3.5.1 형식 검사
람다가 사용되는 컨텍스트를 이용해서 람다의 형식을 추론할 수 있습니다. 어떤 컨텍스트에서 기대되는 람다 표현식의 형식을 대상 형식이라고 부릅니다.
람다 표현식을 사용할 때 실제 어떤 일이 일어나는지 보여주는 다음과 같은 예제가 있습니다.
List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);
다음과 같은 순서로 위 코드의 형식 확인 과정이 진행됩니다.
- filter 메서드의 선언을 확인합니다.
- filter 메서드는 두 번째 파라미터로 Predicate<Apple> 형식(대상 형식)을 기대합니다.
- Predicate <Apple>은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스입니다.
- test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사합니다.
- filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 합니다.
위 예제에서 람다 표현식은 Apple을 인수로 받아 boolean을 반환하므로 유효한 코드입니다. 람다 표현식이 예외를 던질 수 있다면 추상 메서드로 같은 예외를 던질 수 있도록 throws로 선언해야 합니다.
3.5.2 같은 람다, 다른 함수형 인터페이스
대상 형식이라는 특징 때문에 같은 람다 표현식이더라도 호환되는 추상 메서드가 가진 다른 함수형 인터페이스로 사용될 수 있습니다.
Comparator<Apple> c1 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
ToIntBiFunction<Apple, Apple> c2 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
BiFunction<Apple, Apple, Integer> c3 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
3.5.3 형식 추론
자바 컴파일러는 람다 표현식이 사용된 컨텍스트 즉, 대상 형식을 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론합니다. 즉, 대상 형식을 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론할 수 있습니다.
결과적으로 컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있으므로 람다 문법에서 이를 생략할 수 있습니다. 즉, 자바 컴파일러는 다음처럼 람다 파라미터 형식을 추론할 수 있습니다.
List<Apple> greenApples = filter(inventory, apple -> GREEN.equals(apple.getColor()));
// 파라미터 a에는 형식을 명시적으로 지정하지 않았습니다.
여러 파라미터를 포함하는 람다 표현식에서 코드 가독성 향상이 더 두드러집니다.
예를 들어, 다음은 Comparator 객체를 만드는 코드입니다.
// 형식을 추론하지 않음
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 지역 변수 사용
지금까지 살펴본 모든 람다 표현식은 인수를 자신의 바디 안에서만 사용했습니다. 하지만, 람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수를 활용할 수 있습니다. 이와 같은 동작을 람다 캡처링이라고 부릅니다.
다음은 portNumber 변수를 캡처하는 람다 예제입니다.
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
하지만 자유 변수에도 약간의 제약이 있습니다. 람다는 인스턴스 변수의 정적 변수를 자유롭게 캡처할 수 있습니다. 하지만 그러려면 지역 변수는 명시적으로 final로 선언되어 있어야 하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야 합니다. 즉, 람다 표현식은 한 번만 할당할 수 있는 지역 변수를 캡처할 수 있습니다. 참고로 인스턴스 변수 캡처는 final 지역 변수 this를 캡처하는 것과 마찬가지입니다.
예를 들어, 다음 예제는 portNumber에 값을 두 번 할당하므로 컴파일할 수 없는 코드입니다.
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber); // 에러 : 람다에서 참조하는 지역 변수는 final로 선언되거나 실질적으로 final처럼 취급되어야 한다.
portNumber = 31337;
지역 변수의 제약
내부적으로 인스턴스 변수와 지역 변수는 태생부터 다릅니다. 인스턴스 변수는 힙에 저장되는 반면에 지역 변수는 스택에 저장됩니다.
람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서 해당 변수를 접근하려 할 수 있습니다.
자바 구현에서 원래의 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공합니다. 따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴 것입니다.
또한, 지역 변수의 제약 때문에 외부 변수를 변화시키는 일반적인 명령형 프로그래밍 패턴(병렬화를 방해하는 요소)에 제동을 걸 수 있습니다.
클로저
클로저란 함수의 비지역 변수를 자유롭게 참조할 수 있는 함수의 인스턴스를 가리킵니다. 예를 들어, 클로저를 다른 함수의 인수로 전달할 수 있습니다. 클로저는 클로저 외부에 정의된 변수의 값에 접근하고 값을 바꿀 수 있습니다. 자바 8의 람다와 익명 클래스는 클로저와 비슷한 동작을 수행합니다. 람다와 익명 클래스 모두 메서드의 인수로 전달될 수 있으며 자신의 외부 영역의 변수에 접근할 수 있습니다. 다만 람다와 익명 클래스는 람다가 정의된 메서드의 지역 변수의 값을 바꿀 수 없습니다. 람다가 정의된 메서드의 지역 변수값은 스택에 존재해야 하므로 자신을 정의한 스레드와 생존을 같이 해야 하며 따라서 지역 변수는 final이어야 합니다. 가변 지역 변수를 새로운 스레드에서 캡처할 수 있다면 안전하지 않은 동작을 수행할 가능성이 생깁니다. 인스턴스 변수는 스레드가 공유하는 힙에 존재하므로 특별한 제약이 없습니다.
3.6 메서드 참조
메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있습니다.
다음은 메서드 참조와 새로운 자바 8 API를 활용한 정렬 예제입니다.
// 기존 코드
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
// 메서드 참조와 java.util.Comparator.comparing을 활용
inventory.sort(comparing(Apple::getWeight));
3.6.1 요약
메서드 참조는 특정 메서드만 호출하는 람다의 축약형이라고 생각할 수 있습니다. 실제로 메서드 참조를 이용하면 기존 메서드 구현으로 람다 표현식을 만들 수 있습니다. 이때 명시적으로 메서드명을 참조함으로써 가독성을 높일 수 있습니다. 메서드명 앞에 구분자(::)를 붙이는 방식으로 메서드 참조를 활용할 수 있습니다.
예를 들어, Apple::getWeight는 Apple 클래스에 정의된 getWeight의 메서드 참조입니다. 결과적으로 메서드 참조는 람다 표현식 (Apple a) -> a.getWeight()를 축약한 것입니다.
메서드 참조를 새로운 기능이 아니라 하나의 메서드를 참조하는 람다를 편리하게 표현할 수 있는 문법으로 간주할 수 있습니다.
메서드 참조를 이용하면 다음처럼 같은 기능을 더 간결하게 구현할 수 있습니다.
- 메서드 참조를 만드는 세 가지 방법
- 정적 메서드 참조
- 예를 들어 Integer의 parseInt 메서드는 Integer::parseInt로 표현할 수 있습니다.
- 다양한 형식의 인스턴스 메서드 참조
- 예를 들어 String의 length 메서드는 String::length로 표현할 수 있습니다.
- 기존 객체의 인스턴스 메서드 참조
- 예를 들어 Transaction 객체를 할당받은 expensiveTransaction 지역 변수가 있고, Transaction 객체에는 getValue 메서드가 있다면, 이를 expensiveTransaction::getValue라고 표현할 수 있습니다.
- 정적 메서드 참조
3.6.2 생성자 참조
ClassName::new처럼 클래스명과 new 키워드를 이용해서 기존 생성자와 참조를 만들 수 있다. 정적 메서드의 참조를 만드는 방법과 비슷하다. 예를 들어 인수가 없는 생성자, 즉 Supplier의 ( ) → Apple과 같은 시그니처를 갖는 생성자가 있다고 가정하면 다음과 같습니다.
Supplier<Apple> c1 = Apple::new;
Apple c1 = c1.get(); // Supplier의 get 메서드를 호출해서 새로운 Apple 객체를 만들 수 있다.
위 예제는 다음의 코드와 같습니다.
Supplier<Apple> c1 = () -> new Apple(); // 람다 표현식은 디폴트 생성자를 가진 Apple을 만든다.
Apple c1 = c1.get(); // Supplier의 get 메서드를 호출해서 새로운 Apple 객체를 만들 수 있다.
Apple(Integer weight)라는 시그니처를 갖는 생성자는 Function 인터페이스의 시그니처와 같습니다.
따라서 다음과 같은 코드를 구현할 수 있습니다.
Function<Integer, Apple> c2 = Apple::new // Apple(Integer weight)의 생성자 참조
Apple a2 = c2.apply(110); // Function의 apply 메서드에 무게를 인수로 호출해서 새로운 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, Function<Integer, Apple> f) {
List<Apple> result = new ArrayList<>();
for (Integer i : list) {
result.add(f.apply(i));
}
return result;
}
3.7 람다, 메서드 참조 활용하기
3.7.1 1단계 : 코드 전달
자바 8의 List API에서 sort 메서드를 제공하므로 정렬 메서드를 직접 구현할 필요는 없습니다.
sort 메서드는 다음과 같은 시그니처를 갖습니다.
void sort(Comparator<? super E> c)
객체 안에 동작을 포함시키는 방식으로 다양한 전략을 전달할 수 있습니다.
1단계의 코드는 다음과 같이 완성됩니다.
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에서는 람다 표현식이라는 경량화된 문법을 이용해서 코드를 전달할 수 있습니다. 함수형 인터페이스를 기대하는 곳에서는 람다 표현식을 사용합니다. 함수형 인터페이스란, 오직 하나의 추상 메서드를 정의하는 인터페이스입니다. 추상 메서드의 시그니처(함수 디스크립터)는 람다 표현식의 시그니처를 정의합니다.
다음처럼 코드를 개선할 수 있습니다.
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()));
3.7.4 4단계 : 메서드 참조 사용
java.util.Comparator.comparing은 정적으로 임포트 했다고 가정하고 메서드 참조를 이용해서 코드를 좀 더 간소화할 수 있다.
inventory.sort(comparing(Apple::getWeight));
코드가 간결해지고 의미도 명확해졌습니다. Apple을 weight별로 비교해서 inventory를 sort 하라는 의미를 전달할 수 있습니다.
3.8 람다 표현식을 조합할 수 있는 유용한 메서드
자바 8 API의 몇몇 함수형 인터페이스는 다양한 유틸리티 메서드를 포함합니다. 예를 들어 Comparator, Function, Predicate 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 메서드를 제공합니다. 즉, 간단한 여러 개의 람다 표현식을 조합해서 복잡한 람다 표현식을 만들 수 있습니다. 예를 들어, 두 프레디케이트를 조합해서 두 프레디케이트 or 연산을 수행하는 커다란 프레디케이트를 만들 수 있습니다. 또한, 한 함수의 결과가 다른 함수의 입력이 되도록 두 함수를 조합할 수 있습니다.
3.8.1 Comparator 조합
정적 메서드 Comparator.comparing을 이용해서 비교에 사용할 키를 추출하는 Function 기반의 Comparator를 반환할 수 있습니다.
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
역정렬
사과의 무게를 내림차순으로 정렬하고 싶을 때 다른 Comparator 인스턴스를 만들 필요가 없습니다. 인터페이스 자체에서 주어진 비교자의 순서를 뒤바꾸는 reversed라는 디폴트 메서드를 제공하기 때문입니다. 따라서 다음 코드처럼 비교자 구현을 그대로 재사용해서 사과의 무게를 기준으로 역정렬할 수 있습니다.
inventory.sort(comparing(Apple::getWeight).reversed()); // 무게를 내림차순으로 정렬
Comparator 연결
무게가 같은 두 사과가 존재할 경우 정렬 리스트에서 비교를 더 다듬을 수 있는 두 번째 Comparator를 만들 수 있습니다. thenComparing 메서드로 두 번째 비교자를 만들 수 있다. thenComparing은 함수를 인수로 받아 첫 번째 비교자를 이용해서 두 객체가 같다고 판단되면 두 번째 비교자에 객체를 전달합니다.
즉, 다음처럼 문제를 해결할 수 있습니다.
inventory.sort(comparing(Apple::getWeight) .reversed() .thenComparing(Apple::getCountry)); // 두 사과의 무게가 같다면 국가별로 정렬
3.8.2 Predicate 조합
Predicate 인터페이스는 복잡한 프레디케이트를 만들 수 있도록 negate, and, or 세 가지 메서드를 제공합니다.
단순한 람다 표현식을 조합해서 더 복잡한 람다 표현식을 만들 수 있습니다.
3.8.3 Function 조합
Function 인터페이스에서 제공하는 람다 표현식도 조합할 수 있습니다. Function 인터페이스는 Function 인스턴스를 반환하는 andThen, compose 두 가지 디폴트 메서드를 제공합니다. andThen 메서드는 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환합니다. compose 메서드는 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공합니다.
'📚 서적 및 학습자료 > 모던 자바 인 액션[2회독]' 카테고리의 다른 글
[모던 자바 인 액션] 3장 동작 람다 표현식(1) (1) | 2023.12.03 |
---|---|
[모던 자바 인 액션] 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 |