람다표현식 이란 동작파라미터화와 세트로 코드를 보다 간결하고 보기 좋게 만들어준다.
표현식은 익명 클래스와 유사하지만 훨씬 깔끔함
3.1 람다란 무엇인가?
람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것.
람다 표현식에는 이름은 없지만, 파라미터 리스트, 바디, 반환형식, 발생할 수 있는 예외 리스트를 가질 수 있다.
- 익명
- 보통의 메서드와 달리 이름이 없으므로 익명이라 표현한다. 구현해야 할 코드에 대한 걱정거리가 줄어든다.
- 함수
- 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다. 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함한다.
- 전달
- 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
- 간결성
- 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없다.
바로 Comparator 객체를 통해 예시를 들어보자.
Comparator<Apple> byWeight = new Comparator<Apple>() {
public int compare(Apple a, Apple b) {
return a.getWeight().compareTo(b.getWeight());
}
};
Comparator<Apple> appleComparator =
(Apple a, Apple b) -> a.getWeight().compareTo(b.getWeight());
위, 아래는 같은 작업을 처리하지만 누가봐도 아랫줄이 훨씬 더 보기 좋다.
람다 표현식을 이용하면 compare 메서드의 바디를 직접 전달하는 것처럼 코드를 전달할 수 있다.
- 파라미터 리스트
- Comparator의 compare 메서드 파리미터
- 화살표
- 화살표 (→)는 람다의 파라미터 리스트와 바디를 구분한다.
- 람다 바디
- 두 사과의 무게를 비교한다. 람다의 반환값에 해당하는 표현식이다.
java 8에서 지원하는 다섯가지 람다 표현식 예제
3.2 어디에, 어떻게 람다를 사용할까?
이전 예제에서는 Comparator<Apple> 형식의 변수에 람다를 할당했다. 2장에서 구현했던 필터 메서드에도 람다를 활용할 수 있었다.
List<Apple> apples = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));
함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다.
3.2.1 함수형 인터페이스
2장에서 만들 Predicate가 함수형 인터페이스다.
Predicate는 오직 하나의 추상 메서드만 지정하기 때문이다.
간단히 말해 함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스다. 지금까지 살펴본 자바 API의 함수형 인터페이스로 Comparator, Runnable 등이 있다.
함수형 인터페이스는 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다.
Runnable r1 = () -> System.out.println("Hello World 1");
Runnable r2 = new Runnable() {
public void run() {
System.out.println("Hello World 2");
}
};
process(r1); // Hello World 1
process(r2); // Hello World 2
process(() -> System.out.println("Hello World 3")); // Hello World 3
process라는 함수형 인터페이스가 있다면 다음과 같은 람다 표현식으로 직접 함수를 전달할 수 있다.
3.2.2 함수 디스크립터
람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터 라고 부른다. ex ( () → void )
() → void 표기는 파라미터 리스트가 없으며 void를 반환하는 함수를 의미한다.
쉽게 말해 Predicate의 함수 디스크립터는 test가 되고 시그니쳐는 (T) → boolean 이다.
3.3 람다 활용 : 실행 어라운드 패턴
위와 같은 형식의 코드를 실행 어라운드 패턴 이라고 부른다.
String processFile() throws IOException {
// try, with, resources
try (BufferedReader br =
new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}
이제 이 코드를 동작 파라미터화 시켜보자.
3.3.1 1단계 : 동작 파라미터화를 기억하라
현재 코드는 파일에서 한 번에 한 줄만 읽을 수 있다. 한번에 두 줄을 읽거나 가장 자주 사용되는 단어를 반환하려면 어떻게 해야할까? 기존의 설정, 정리 과정은 재사용하고 processFile 메서드만 다른 동작을 수행하도록 명령할 수 있다면 좋을 것이다.
processFile의 동작을 파라미터화 하는 것이다.
그럼 여기서 processFile 메서드가 한번에 두 행일 읅게 하려면 코드를 어떻게 고쳐야 할까?
우선 BufferedReader를 인수로 받아서 String을 반환하는 람다가 필요하다.
String result = processFile((BufferedReader br) ->
br.readLine() + br.readLine());
하지만 processFile을 변경해주지 않아 아직 시뻘건줄이 여기저기 뜬다.
3.3.2 2단계 : 함수형 인터페이스를 이용해서 동작 전달
함수형 인터페이스 자리에 람다를 사용할 수 있다.
따라서 BufferedReader → String과 IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 한다.
이 인터페이스를 BufferedReaderProcessor라고 정의하자.
void lambdaTest() throws IOException {
// 나 2줄 받아올래 (BufferedReader 인자로) -> br로 2줄 읽어올거임
String result = processFile((BufferedReader br) ->
br.readLine() + br.readLine());
}
// 그 함수형 인터페이스를 인자로 받아 오는것까진 성공!
public String processFile(BufferedReaderProcessor p) throws IOException {
// try, with, resources
try (BufferedReader br =
new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}
/* 해당 어노테이션을 정의하는 이유는 함수형 인터페이스를 명시적으로 나타내 코드의 가독성을 높이고
* 정확히 하나의 추상 메서드를 가지는지 검증해서 컴파일 오류를 발생시킨다. */
@FunctionalInterface
public interface BufferedReaderProcessor {
// BufferedReader 를 인자로 받고 IOException을 발생시키는 함수형 인터페이스
String process(BufferedReader br) throws IOException;
}
해당 함수형 인터페이스를 인자로 processFile의 메서드에 전달했다.
3.3.3 3단계 : 동작 실행
이제 마지막으로 우리가 람다식에 담아둔 동작을 실행시키면 되는데 람다의 코드가 processFile 내부에서 어떻게 실행될지 예상해보자.
람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으며 전달된 코드는 함수형 인터페이스의 인스턴스로 전달된 코드와 같은 방식으로 처리한다.
// 그 함수형 인터페이스를 인자로 받아 오는것까진 성공!
public String processFile(BufferedReaderProcessor p) throws IOException {
// try, with, resources
try (BufferedReader br =
new BufferedReader(new FileReader("data.txt"))) {
// 뚝딱
return p.process(br);
}
}
3.3.4 4단계 : 람다 전달
이제 람다를 이용해서 다양한 동작을 processFile 메서드로 전달할 수 있다.
void lambdaTest() throws IOException {
// 나 2줄 받아올래 (BufferedReader 인자로) -> br로 2줄 읽어올거임
String result = processFile((BufferedReader br) ->
br.readLine() + br.readLine());
String oneLine = processFile(BufferedReader::readLine);
String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());
}
/* 해당 어노테이션을 정의하는 이유는 함수형 인터페이스를 명시적으로 나타내 코드의 가독성을 높이고
* 정확히 하나의 추상 메서드를 가지는지 검증해서 컴파일 오류를 발생시킨다.
* 추가로 전달될 메소드의 시그니쳐는 맞춰줘야 하기 때문에 IOException을 throws*/
@FunctionalInterface
public interface BufferedReaderProcessor {
// BufferedReader 를 인자로 받고 IOException을 발생시키는 함수형 인터페이스
String process(BufferedReader br) throws IOException;
}
// 그 함수형 인터페이스를 인자로 받아 오는것까진 성공!
public String processFile(BufferedReaderProcessor p) throws IOException {
// try, with, resources
try (BufferedReader br =
new BufferedReader(new FileReader("data.txt"))) {
// 뚝딱
return p.process(br);
}
}
실행 어라운드 패턴을 적용하는 순서를 보면 다음과 같다.
- 동작하는 하나의 메서드를 작성
public String processFile() throws IOException {
// try, with, resources
try (BufferedReader br =
new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}
- 메서드에 제공할 동작 파라미터 작성
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader br) throws IOException;
}
public String processFile(BufferedReaderProcessor p) throws IOException {
...
}
- 동작 파라미터의 함수 디스크립터 실행
public String processFile(BufferedReaderProcessor p) throws IOException {
// try, with, resources
try (BufferedReader br =
new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
- 람다 표현식 적용
String oneLine = processFile(BufferedReader::readLine);
String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());
이건 사이드 플젝하면서 직접 한번 써봐야 감이 확실히 잡힐것같다.
3.4 함수형 인터페이스 사용
함수형 인터페이스의 추상 메서드 시그니처를 함수 디스크립터라고 한다.
다양한 람다 표현식을 사용하려면 공통의 함수 디스크립터를 기술하는 함수형 인터페이스 집합이 필요하다.
3.4.1 Predicate
Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 불리언을 반환한다.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
public <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> result = new ArrayList<>();
list.stream()
.filter(p::test)
.forEach(result::add);
return result;
}
Predicate 인터페이스의 자바독 명세를 보면 and나 or같은 메서드도 있음을 알 수 있다.
이건 나중에 알아보자.
3.4.2 Consumer
java.util.function.Consumer<T> 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의한다. T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있다.
public void forEachTest() {
forEach(
Arrays.asList(1,2,3,4,5),
System.out::println
);
}
@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);
}
}
3.4.3 Function
java.util.function.Function<T, R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의한다.
@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::length
);
기본형 특화
자바의 모든 형식을 참조형 아니면 기본형에 해당된다.
하지만 제네릭 파라미터에는 참조형만 사용할 수 있다.
이를 위해 자바에서는 기본형을 참조형으로 변환하는 기능을 제공한다.
이 기능을 박싱, 언박싱이라고 하며 비용이 소모된다.
이를 방지하기 위해 Predicate 앞에 Int, Double, Long 등등을 붙히면 박싱 없이 값을 사용할 수 있다.
3.5 형식 검사, 형식 추론, 제약
람다 표현식을 처음 설명할 때 람다로 함수형 인터페이스의 인스턴스를 만들 수 있다고 언급했다.
람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지의 정보가 포함되어 있지 않다.
따라서 람다 표현식을 더 제대로 이해하려면 람다의 실제 형식을 파악해야 한다.
3.5.1 형식 검사
람다가 사용되는 콘텍스트를 이용해서 람다의 형식을 추론할 수 있다.
어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 대상 형식 이라고 부른다.
public <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> result = new ArrayList<>();
list.stream()
.filter(p::test)
.forEach(result::add);
return result;
}
List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);
- filter 메서드의 선언을 확인
- filter 메서드는 두 번째 파라미터로 Predicate<T> 형식을 기대한다.
- Predicate는 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스다.
- test 메서드는 Apple(<T>)을 받아 boolean(<R>)을 반환하는 함수 디스크립터를 묘사한다.
- filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 한다.
쉽게 말하면 대상 형식 → 람다가 기대하는 함수 디스크립터
Predicate, Consumer, Function 등등
즉 함수 디스크립터는 Apple → boolean이므로 람다의 시그니처와 일치한다.
위 예제에서 람다 표현식은 Apple을 인수로 받아 boolean을 반환하므로 유효한 코드다.
람다 표현식이 예외를 던질 수 있다면 추상 메서드도 같은 예외를 던질 수 있도록 throws로 선언해야 한다.
3.5.2 같은 람다, 다른 함수형 인터페이스
대상 형식이라는 특징 때문에 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있다.
Callable<Integer> c = () -> 42;
PrivilegedAction<Integer> p = () -> 42;
위 코드에서 첫 번째 대상형식은 Callable<Integer>고, 두 번째 할당문의 대상 형식은 PrivilegedAction<Integer>다.
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()));
위 코드를 보면 List<Apple> 을 보고 대상형식을 유추할 수 있음
여러 파라미터를 포함하는 람다 표현식에서는 코드 가독성 향상이 두드러진다.
Comparator<Apple> test = (Apple a1, Apple a2) -> a1.getWeight()
.compareTo(a2.getWeight());
Comparator<Apple> test2 = (a1, a2) -> a1.getWeight()
.compareTo(a2.getWeight());
3.5.4 지역 변수 사용
익명 함수가 하는것처럼 자유 변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있다.
이와 같은 동작을 람다 캡처링 이라고 부른다.
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
하지만 자유 변수에도 약간의 제약이 있다.
람다는 인스턴스 변수와 정적 변수를 자유롭게 캡쳐할 수 있다.
하지만 그러려면 지역 변수는 명시적으로 final로 선언되어 있어야 하나 실질적이로 final로 선언된 변수와 똑같이 사용되어야 한다.
즉, 람다 표현식은 한 번만 할당할 수 있는 지역 변수를 캡쳐할 수 있다.
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
// 불가능!!
portNumber = 31337;
지역 변수의 제약
왜 이런 제약이 걸리는걸까?
이는 인스턴스 변수(클래스에 속한 변수)는 힙에 저장되는 반면 지역 변수(메서드에 선언된 변수)는 스택에 위치한다.
람다에서 지역 변수에 바로 접근할 수 있다는 가정 하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다.
따라서 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공한다.
따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴것이다.
3.6 메서드 참조
메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달 할 수 있다.
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
inventory.sort(comparing(Apple::getWeight));
이런식으로 정리할 수 있다.
3.6.1 요약
실제로 메서드 참조를 이용하면 기존 메서드 구현으로 람다 표현식을 만들 수 있다.
이때 명시적으로 메서드명을 참조함으로써 가독성을 높일 수 있다.
람다 메서드 참조 단축 표현
(Apple apple) → apple.getWeight() | Apple::getWeight |
() → Thread.currentThread().dumpStack() | Thread.currentThread()::dumpStack |
(str, i) → str.substring(i) | String::substring |
(String s) → System.out.println(s) | System.out::println |
(String s) → this.isValidName(s) | this::isValidName |
크게 어렵지 않은 모습이다.
매개 변수가 있는 경우는 다음과 같이 표현할 수 있다.
Function<Integer, String> func = "exampleString"::substring;
String result = func.apply(3);
System.out.println(result); // 출력: "mpleString"
메서드 참조는 그렇게 어렵지 않다.
쉽게 이해하려면 다음과 같이 이해하면 될것같음
ToIntFunction<String> stringToInt = (String s) -> Integer.parseInt(s);
ToIntFunction<String> stringToInt2 = Integer::parseInt;
BiPredicate<List<String>, String> contains = (list, element) -> list.contains(element);
BiPredicate<List<String>, String> contains2 = List::contains;
위 코드를 보면 Integer.perseInt와 같은 구조를 Integer::parseInt 와 같이 수정하는 모습을 확인할 수 있음
3.6.2 생성자 참조
Supplier<Apple> test1 = Apple::new; // 람다 표현식은 디폴트 생성자를 가진 Apple을 만든다.
Apple a1 = test1.get(); // <- Supplier의 get메서드를 호출해서 새로운 Apple 객체를 만들 수 있다.
Apple(Integer weight)라는 시그니처를 갖는 생성자는 Function 인터페이스와 시그니처와 같다.
따라서 다음과 같은 코드를 구현할 수 있다.
Function<Integer, Apple> test3 = Apple::new; // <- weight 생성자
Apple a2 = test3.apply(110); // <- Function의 apply 메서드에 무게를 인수로 호출해서 새로운 Apple 객체를 만들 수 있다.
어디에 쓰이는고 하니 다음과 같은 예제가 있다.
List<Integer> weights = Arrays.asList(1, 2, 3, 4, 5);
List<Apple> apples = map(weights, 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;
}
map에서 내부 동작을 서술하기 위해 다음과 같이 예시를 들었는데 Spring에서는 잘 쓰이진 않을것같은 문법이다.
BiFunction<Color, Integer, Apple> test4 = Apple::new;
Apple a3 = test4.apply(GREEN, 110);
이런식의 응용도 가능한듯 하다
그래도 뭔가 잘 쓰일것같진 않음.
Dto → Entity를 정의한다고 해도 생성자 참조를 사용하기 보다는 map을 사용해 Builder Pattern을 적용시켜 메서드 참조를 적용시키는게 코드의 가독성이 훨씬 올라갈듯함
3.7 람다, 메서드 참조 활용하기
3.7.1 1단계 : 코드 전달
sort 메서드에 정렬 전략을 전달해보자.
sort 메서드는 다음과 같은 시그니처를 갖는다.
default void sort(Comparator<? super E> c)
이 코드는 Comparator 객체를 인수로 받아 두 사과를 비교한다.
객체 안에 동작을 포함시키는 방식으로 다양한 전략을 전달할 수 있다.
이제 ‘sort의 동작은 파라미터화 되었다’ 라고 말할 수 있다.
1단계의 코드는 다음과 같이 완성할 수 있다.
public class AppleComparator implements Comparator<Apple> {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
}
void lambdaTest2() {
List<Integer> weights = Arrays.asList(100,200,300,500);
List<Apple> apples = map(weights, Apple::new);
apples.sort(new AppleComparator());
}
3.7.2 2단계 : 익명 클래스 사용
void lambdaTest2() {
List<Integer> weights = Arrays.asList(100,200,300,500);
List<Apple> apples = map(weights, Apple::new);
apples.sort(new AppleComparator() {
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
});
}
한번만 사용할 클래스는 아무래도 선언하는것보단 익명 클래스를 사용하는게 좋을것이다.
하지만 여기서 한번 더 최적화를 할 수 있는데
3.7.3 3단계 : 람다 표현식 사용
함수형 인터페이스를 기대하는 곳 어디에서나 람다 표현식을 사용할 수 있다.
함수형 인터페이스란 오직 하나의 추상 메서드를 정의하는 인터페이스다.
추상 메서드의 시그니처(함수 디스크립터)는 람다 표현식의 시그니처를 정의한다.
Comparator의 함수 디스크립터는 (T, T) → int
void lambdaTest2() {
List<Integer> weights = Arrays.asList(100,200,300,500);
List<Apple> apples = map(weights, Apple::new);
apples.sort((Apple a1, Apple a2) -> a1.getWeight()
.compareTo(a2.getWeight()));
}
여기서 자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 활용해서 람다의 파라미터 형식을 추론할 수 있다.
따라서 한번 더 줄일 수 있음
void lambdaTest2() {
List<Integer> weights = Arrays.asList(100,200,300,500);
List<Apple> apples = map(weights, Apple::new);
apples.sort((a1, a2) -> a1.getWeight()
.compareTo(a2.getWeight()));
}
3.7.4 4단계 : 메서드 참조 사용
apples.sort(comparing(Apple::getWeight));
최종 단계
‘Apple을 weight 별로 비교해서 inventory를 sort하라’는 의미를 전달할 수 있다.
3.8 람다 표현식을 조합할 수 있는 유용한 메서드
Comparator, Function, predicate같은 함수형 인터ㅠ페이스는 람다 표현식을 조합할 수 있다록 유틸리티 메서드를 제공한다.
간단한 여러개의 람다 표현식을 조합해서 복잡한 람다 표현식을 만들 수 있다는 뜻.
함수형 인터페이스는 시그니처를 제외한 메서드를 추가로 제공하는게 불가능하다.
이 한계를 극복하기 위해 디폴트 메서드를 사용한다.
3.8.1 Comparator 조합
이전에도 보았듯 Comparator.comparing을 이용해서 비교에 사용할 키를 추출하는 Function 기반의 Comparator를 반환할 수 있다.
역정렬
사과의 무게를 내림차순으로 정렬하려면 인터페이스 자체에서 주어진 비교자의 순서를 뒤바꾸는 reverse라는 디폴트 메서드를사용하면 된다.
Comparator 연결
비교 결과를 더 다듬을 수 있는 두 번째 Comparator를 만들 수 있다.
thenComparing 메서드로 두 번째 비교자를 만들 수 있다.
apples.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getColor));
3.8.2 Predicate 조합
negate, and, or 세가지 메서드를 제공한다.
예를들어 ‘빨간색이 아닌 사과’ 처럼 특정 프레디케이트를 반전시킬 때 negate 메서드를 사용할 수 있다.
Predicate<Apple> redApple = (apple) -> apple.getColor().equals(RED);
Predicate<Apple> notRedApple = redApple.negate();
또한 and 메서드를 이용해서 빨간색이면서 무거운 사과를 선택하도록 두 람다를 조합할 수 있다.
Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);
그뿐만 아니라 or을 이용할 수도 있음
Predicate<Apple> redAndHeavyApple =
redApple.and(apple -> apple.getWeight() > 150)
.or(apple -> GREEN.equals(apple.getColor()));
3.8.3 Function 조합
Function도 조합이 가능!
Function 인터페이스는 andThen, compose 두가지 디폴트 메서드를 제공한다.
andThen 메서드는 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환한다.
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g); // f의 결과를 g의 매개변수로 전달
int result = h.apply(1); // 4을 반환
Function<Integer, Integer> i = f.compose(g); // g의 결과를 f의 매개변수로 전달
int result2 = h.apply(1); // 3을 반환
andThen과 compose의 차이는 서순
표현식은 익명 클래스와 유사하지만 훨씬 깔끔함
'코딩딩 > Java' 카테고리의 다른 글
Synchronized를 이용한 동시성 실습 (0) | 2024.09.01 |
---|---|
Synchronized와 Reentrantlock (1) | 2024.08.31 |
모던 자바 인 액션 Chapter 2 (1) | 2024.08.16 |
모던 자바 인 액션 Chapter 1 (0) | 2024.08.15 |
List에 담긴 Entity를 DTO로 변환하며 정렬하는 방법 (0) | 2024.01.11 |