0%

(학습) Java8 In Action 3장 정리

Java8 in Action 책을 보고 학습한 내용을 정리한 포스팅이다.
직접 실행해보거나 작성해본 코드는 Github에 작성하였다.
1장 포스팅
2장 포스팅


3장 람다 표현식


람다의 특징

  • 익명 : 보통의 메서드와 달리 이름이 없으므로 익명이라 표현이 된다.
  • 함수 : 람다는 메서드처럼 특정 클래스에 종속되어 있지 않으므로 함수라 부른다.
  • 전달 : 람다 표현식을 메서드 인수로 전달하거나 변수로 저장이 가능하다.

람다 사용 예제

  • 기존의 정렬 예제코드

    1
    2
    3
    4
    5
    6
    7
    8
    public void exam01() {
    Comparator<Apple> byWeight = new Comparator<Apple>() {
    @Override
    public int compare(Apple o1, Apple o2) {
    return o1.getWeight().compareTo(o2.getWeight());
    }
    };
    }
  • 위 예제에서 Apple의 경우 weight의 경우 Integer를 반환한다.

  • 아래는 람다를 사용한 예제 코드

    1
    Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getPrice().compareTo(a2.getWeight());

람다 표현식

  • 형식은 다음과 같다.
    • (Apple a1, Apple a2) : 파라미터 리스트
    • -> : 화살표
    • a1.getPrice().compareTo(a2.getWeight()); : 람다의 바디
  • 파라미터 리스트 : 전달인자 부분
  • 화살표 : 파라미터 리스트와 바디를 구분하는 구분자
  • 람다의 바디 : 람다의 반환값에 해당하는 표현식
  • 람다의 기본 문법은 다음과 같다.
    • (parameters) -> expression
    • (parameters) -> { statements; }

자바의 람다 표현식 (예제)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//1
(String s) -> s.length();

//2
(Apple a) -> a.getWeight() > 150

//3
(int x, int y) -> {
System.out.println("Result : ");
System.out.println(x+y);
}

//4
() -> 42

//5
(Apple a1, Apple a2) -> a1.getPrice().compareTo(a2.getWeight());

람다의 사용조건

  • 람다식은 아무곳에서 사용이 안되고, 함수형 인터페이스 문맥에서 사용이 가능하다.

함수형 인터페이스

  • 함수형 인터페이스는 하나의 추상 메서드를 지정하는 인터페이스다.
  • 함수형 인터페이스의 예제는 아래의 코드를 참고하자.
1
2
3
4
5
6
7
public interface Comparator<T> {
int compare(T o1, T o2);
}

public interface Runnable {
void run();
}
  • 람다 표현식으로는 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다.
  • 아래는 익명 내부 클래스로 구현한 것과 람다로 구현한 것을 예제로 하는 코드이다.
1
2
3
4
5
6
7
Runnable r1 = new Runnable() {
public void run() {
System.out.printlb("Hello World");
}
}

Runnable r2 = () -> System.out.println("Hello World");

함수 디스크립터

  • 함수형 인터페이스의 추상 메서드 시그니쳐는 람다 표현식의 시그니처를 가리킨다.
  • 메서드 시그니처 는 메서드의 특성과 이름, 전달인자, 반환값의 데이터 타입을 표현한 형태를 가리킨다.
  • 함수 디스크립터 란 람다 표현식의 시그니처를 서술하는 메서드를 가리킨다.
  • 즉 함수 디스크립터는 메서드 시그니처를 람다 표현식 형태로 표현한 것을 가리킨다.
    • boolean test(T t)가 메서드 시그니처라면 함수 디스크립터로 표현을 하면 T->boolean 이라 할 수 있다.

@FunctionalInterface

  • 이 어노테이션은 해당 인터페이스가 함수형 인터페이스라는 것을 나타낸다.
  • 위에서 확인한것 처럼 함수형 인터페이스는 하나의 추상 메서드만을 소유할 수 있다. (단 Default Method 예외)
  • 아래 코드는 예제이다.
1
2
3
4
5
6
7
@FunctionalInterface
public interface FunctionalInterfaceTest {
public void show();
default public void display() {
System.out.println("Default method call : Interface can have body");
}
}
  • 즉 쉽게 기존에 인터페이스에 단 하나의 추상 메서드를 가진 인터페이스를 함수형 인터페이스라고 하는 것이다.
  • 이 어노테이션은 아래와 같은 상황에서 유용하게 사용할 수 있다.
    1. 추상 메서드가 한 개인 인터페이스에서 개발자 A는 다른 곳에서 람다식으로 구현을 하였다.
    2. 시간이 지나서 다른 개발자 B는 1의 인터페이스를 수정할 일이 생겼고, 추가로 추상 메서드를 추가하려 하였다.
    3. 인터페이스에는 추상 메서드의 개수를 제한하는 방법은 없으므로 다른 곳에 람다식이 있는 것을 모르고 추가하였을 때 문제가 발생한다.
    4. 개발자 B는 다른 곳의 람다식을 모른체 추상 메서드를 추가하였는데 다른 곳에서 에러가 발생하게 된다면 이것으로 인해 문제가 생길 수 있다.
    5. 그래서 이런 것을 막기 위해 해당 어노테이션이 존재한다.

람다의 활용 : 실행 어라운드(Excute Around Pattern)

  • 실행 어라운드 패턴은 자원처리(DB의 파일처리 등)에 사용하는 순환 패턴(Recurrent Pattern)은 자원을 열고, 처리한 다음에 자원을 닫는 순서로 이뤄지는데, 설정과 정리 과정 사이에 실제 작업이 들어가 있는 형태를 가리킨다.
  • 아래는 예제코드이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@FunctionalInterface
public interface BufferedReaderProcessor {
public String process(BufferedReader b) throws IOException;
}

public class Example03 {
public static void main(String [] args) throws IOException {
String result = processFileLimited();
System.out.println(result);

System.out.println("---");

String oneLine = processFile((BufferedReader b) -> b.readLine());
System.out.println(oneLine);

String twoLines = processFile((BufferedReader b) -> b.readLine() + b.readLine());
System.out.println(twoLines);
}

//Non use Lambda
public static String processFileLimited() throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader("StudyJava8/src/lambdasinaction/chap3/data.txt")))
{ return br.readLine(); }
}

//Use Lambda
public static String processFile(BufferedReaderProcessor p) throws IOException {
try(BufferedReader br =
new BufferedReader(new FileReader("StudyJava8/src/lambdasinaction/chap3/data.txt")))
{ return p.process(br); }
}

}

함수형 인터페이스 예제(기본형)

1. Predicate

  • Predicate는 참 거짓을 반환하는 함수형 인터페이스로써 Java8의 java.util.function 에서 기본적으로 제공한다.
  • 즉 boolean 반환 식이 필요한 곳에서 사용이 가능하다.
  • 형태는 다음과 같이 되어있다. (개인적으로 만든 코드이다.)
1
2
3
4
@FunctionalInterface
public interface Predicate_Exam<T> {
boolean test(T t);
}
  • 아래는 Predicate를 활용한 예제코드다. (위의 Predicate_Exam 를 사용하였다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static void runTest4Predicate() {
문자열이 공백인지 아닌지를 체크하는 predicate 정의 (Lambda)
Predicate_Exam<String> testPredicate = (String s) -> !s.isEmpty();

결과를 출력하는 로직
for(String s : filter(makeStringList(), testPredicate)) {
System.out.println("s = " + s);
}
}

/**
* String 형태의 리스트를 만들어주는 메서드
* @return
*/
public static List<String> makeStringList() {
return Arrays.asList("Test1", "Test2", "", "Test4");
}

/**
* 프레디케이트를 테스트하는 메서드
* @param list String을 담은 리스트
* @param p 함수형 인터페이스 (Predicate)
* @return 가공된 String
*/
public static <T> List<T> filter(List<T> list, Predicate_Exam<T> p) {
List<T> resultList = new ArrayList<>();
for(T t : list) { if (p.test(t)) { resultList.add(t); } }
return resultList;
}

2. Consumer

  • 제너릭 형식 T 객체를 받아서 void를 반환하는 accept 추상 메서드를 정의하고 있는 함수형 인터페이스다.
  • T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 사용한다.
  • 형태는 다음과 같이 되어있다. (개인적으로 만든 코드이다.)
1
2
3
4
@FunctionalInterface
public interface Consumer_Exam<T> {
void accept(T t);
}
  • 아래는 Consumer를 활용한 예제코드이다. (위의 Consumer_Exam을 사용하였다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void runTest4Consumer() {
Consumer_Exam<String> testConsumer = (String s) -> System.out.println("s = " + s);
forLoop(makeStringList(), testConsumer);
}

/**
* Consumer를 테스트 하는 메서드
* @param list String을 담은 리스트
* @param c 함수형 인터페이스 (Consumer)
*/
public static <T> void forLoop(List<T> list, Consumer_Exam<T> c) {
for (T t : list) { c.accept(t); }
}

3. Function

  • 제너릭 형식 T를 인수로 받아서 제너릭 형식 R 객체를 반환하는 apply 추상 메서드를 정의하고 있는 함수형 인터페이스다.
  • 입력을 출력으로 매핑하는 람다를 정의할 때 사용한다.
  • 형태는 다음과 같이 되어있다. (개인적으로 만든 코드이다.)
1
2
3
4
@FunctionalInterface
public interface Function_Exam<T, R> {
R apply(T R);
}
  • 아래는 Functiondmf 활용한 예제코드이다. (위의 Function_Exam을 사용하였다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void runTest4Function() {
//String을 전달받아서 해당 문자열의 길이를 반환
Function_Exam<String, Integer> testFunc = (String s) -> s.length();

for(Integer i : checkData(makeStringList(), testFunc)) {
System.out.println("i = " + i);
}
}

/**
* Function을 테스트 하는 메서드
* @param list String을 담은 리스트
* @param f 함수형 인터페이스 (Function)
* @return List<R> 을 반환
*/
public static <T, R> List<R> checkData(List<T> list, Function_Exam<T, R> f) {
List<R> resultList = new ArrayList<>();
for(T t : list) { resultList.add(f.apply(t)); }
return resultList;
}
  • 아래는 Function의 다른 예제코드이다.
1
2
3
4
5
6
7
8
9
10
11
// andThen - 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환
Function<Integer, Integer> f = x -> x+1;
Function<Integer, Integer> g = x -> x*2;
Function<Integer, Integer> h = f.andThen(g);;
int result = h.apply(1); // 4를 반환

//compose - 인자로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공
Function<Integer, Integer> f = x -> x+1;
Function<Integer, Integer> g = x -> x*2;
Function<Integer, Integer> h = f.compose(g);;
int result = h.apply(1); // 3를 반환

기본형 특화

  • 위에서 간단하게 살펴본 제너릭 함수형 인터페이스는 모두 참조형만을 사용하였다.
  • 사유는 제네릭 파라메터에서는 참조형만 사용이 가능하기 때문이다.
  • 기본형에서 참조형으로 형 변환을 박싱(Boxing) 이라 한다.
  • 참조형에서 기본형으로 형 변환을 언박싱(Unboxing) 이라 한다.
  • 박싱, 언박싱을 자동으로 이뤄지게 해주는 것을 오토박싱(Autoboxing) 이라 한다.
  • 박싱의 경우 기본형을 감싸는 레퍼형 클래스로써 힙에 저장이 되고, 이로 인해 박싱한 값은 메모리를 더 소비하고, 기본형으로 가져올 때도 메모리 탐색 등의 과정으로 인해 소모가 발생한다.
  • 자바8 에서는 기본형을 입출력으로 사용해야 하는 상황에서 오토박싱 동작을 피할 수 있도록 기본형 특화 함수형 인터페이스를 제공한다.
  • 아래 표는 자바8의 대표적인 함수형 인터페이스를 나타낸다.
함수형 인터페이스 함수 디스크립터 기본형 특화
Predicate < T> T -> boolean IntPredicate, LongPredicate, DoublePredicate
Consumer< T> T -> void IntConsumer, LongConsumer, DoubleConsumer
Function< T,R> T -> R IntFunction< R >, IntToDoubleFunction, IntToLongFunction, LongFunction< R >, LongToDoubleFunction, LongToIntFunction, DoubleFunction< R>, ToIntFunction< T>, ToDoubleFunction< T>, ToLongFunction< T>
Supplier< T> () -> T BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier
UnaryOperator< T> T -> T IntUnaryOperator,
BinaryOperator< T> (T, T) -> T IntBinaryOperator
BiPredicate< L, R> (L, R) -> boolean
BiConsumer< T,U> (T, U) -> void ObjIntConsumer,
BiFunction< T, U, R> (T, U) -> R ToIntBiFuntion<T, U>,

람다와 함수형 인터페이스 예제

사용 사례 람다 예제 대응하는 함수
boolean 표현 (List list) -> list.isEmpty() Predicate< List< String>>
객체 생성 () -> new Apple(10) Supplier< Apple>
객체에서 소비 (Apple a) -> System.out.println(a.getWeight()) Consumer< Apple>
객체에서 선택/추출 (String s) -> s.length() Function< String, Integer> 또는 ToIntFunction< String>
두 값 조회 (int a, int b) -> a * b IntBinaryOperator
두 객체 비교 (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) BiFunction< Apple,Apple,Integer> 또는 ToIntBiFunction< Apple, Apple>

Java8에서 제공하는 기본 함수형 인터페이스

1. Runnable

  • 기존부터 존재하던 인터페이스로 스레드를 생성할때 주로사용하였으며 가장 기본적인 함수형 인터페이스다.
  • void 타입의 인자없는 메서드를 갖고있다.
1
2
Runnable r = () -> System.out.println("hello functional");
r.run();

2. Supplier

  • 인자는 받지않으며 리턴타입만 존재하는 메서드를 갖고있다.
  • 순수함수에서 결과를 바꾸는건 오직 인풋(input) 뿐이다.
  • 그런데 인풋이 없다는건 내부에서 랜덤함수같은것을 쓰는게 아닌이상 항상 같은 것을 리턴하는 메서드라는걸 알 수 있다.
1
2
Supplier<String> s = () -> "hello supplier";
String result = s.get();

3. Consumer

  • void 타입의 인자를 받는 메서드를 갖고있다.
  • 인자를 받아 소모한다는 뜻으로 인터페이스 명칭을 이해하면 될듯 하다.
1
2
Consumer<String> c = str -> System.out.println(str);
c.accept("hello consumer");

4. Function<T, R>

  • 인터페이스 명칭에서부터 알 수 있듯이 전형적인 함수를 지원한다고 보면 된다.
  • 하나의 인자와 리턴타입을 가지며 그걸 제네릭으로 지정해줄수있다.
  • 그래서 타입파라미터(Type Parameter)가 2개다.
1
2
Function<String, Integer> f = str -> Integer.parseInt(str);
Integer result = f.apply("1");

5. Predicate

  • 하나의 인자와 리턴타입을 가진다.
  • Function과 비슷해보이지만 리턴타입을 지정하는 타입파라미터가 안보인다.
  • 반환타입은 boolean 타입으로 고정되어있다.
  • Function<T, Boolean>형태라고 보면된다.
1
2
Predicate<String> p = str -> str.isEmpty();
boolean result = p.test("hello");

6. UnaryOperator

  • 하나의 인자와 리턴타입을 가진다.
  • 그런데 제네릭의 타입파라미터가 1개이며, 인자와 리턴타입의 타입이 같아야한다.
1
2
UnaryOperator<String> u = str -> str + " operator";
String result = u.apply("hello unary");

7. BinaryOperator

  • 동일한 타입의 인자 2개와 인자와 같은 타입의 리턴타입을 가진다.
1
2
BinaryOperator<String> b = (str1, str2) -> str1 + " " + str2;
String result = b.apply("hello", "binary");

8. BiPredicate<T, U>

  • 서로 다른 타입의 2개의 인자를 받아 boolean 타입으로 반환한다.
1
2
BiPredicate<String, Integer> bp = (str, num) -> str.equals(Integer.toString(num));
boolean result = bp.test("1", 1);

9. BiConsumer<T, U>

  • 서로 다른 타입의 2개의 인자를 받아 소모(void)한다.
1
2
BiConsumer<String, Integer> bc = (str, num) -> System.out.println(str + " :: " + num);
bc.accept("숫자", 5);

10. BiFunction<T, U, R>

  • 서로 다른 타입의 2개의 인자를 받아 또 다른 타입으로 반환한다.
1
2
BiFunction<Integer, String, String> bf = (num, str) -> String.valueOf(num) + str;
String result = bf.apply(5, "678");

11. Comparator

  • 자바의 전통적인 인터페이스 중 하나이다.
  • 객체간 우선순위를 비교할때 사용하는 인터페이스인데 전통적으로 1회성 구현을 많이 하는 인터페이스이다.
  • 람다의 등장으로 Comparator의 구현이 매우 간결해져 Comparable 인터페이스의 실효성이 많이 떨어진듯 하다.
1
2
Comparator<String> c = (str1, str2) -> str1.compareTo(str2);
int result = c.compare("aaa", "bbb");

형식 검사

  • 먼저 아래의 코드를 참고하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<Apple> tempList = filterApples(appleList, (Apple a) -> a.getWeight()>70);
```
* 대략 람다식을 확인해보면 Apple 객체를 받아서 a의 무게가 70 이상일 경우를 찾는 것까지 추론할 수 있다.
* 즉 람다 표현식 자체에서는 람다가 어떤 함수형 인터페이스를 구현했는지 정보는 알 수 없다.
* 하지만 람다에 사용되는 컨텍스트(메서드 파라미터[전달인자], 할당 변수 등) 를 통해 람다의 형식을 추론할 수 있다.
* 위의 컨텍스트를 통해서 기대되는 람다 표현식의 형식을 <b> 대상 형식 (Target Type)</b> 이라 한다.

<br>

## 같은 람다 다른 함수형 인터페이스

* 아래의 코드를 보자

```java
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());
  • 람다식은 동일한데 다른 함수형 인터페이스에 사용을 하고 있다.
  • 위와 비슷한 것으로 다이아몬드 연산자가 있다.

List<String> str4List = new ArrayList<>();

  • 위에서 리스트 인터페이스의 제네릭을 String으로 정의하였기 때문에 뒤의 ArrayList<> 에서는 앞의 형식을 추론하여 생략이 가능하다.
  • 또한 람다의 바디에 일반 표현식이 있다면 void를 반환하는 함수 디스크립터와 호환된다.
1
2
3
4
5
// Predicate는 boolean 반환 값을 갖는다.
Predicate<String> p = (String s) -> list.add(s);

//Consumer는 void 반환값을 갖는다.
Consumer<String> c = (String s) -> list.add(s);

형식 추론

  • 자바 컴파일러는 람다 표현식이 사용된 컨텍스트(대상 형식)를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다.
  • 이를 통해 람다식의 코드를 좀 더 단순화 시킬 수 있다.
  • 아래의 코드를 참고해보자.
1
2
3
4
5
6
//일반적으로 작성된 람다식
List<Apple> greenApples = filter(inventory, (Apple a) -> "green".equals(a.getColor()));

//파라미터 a 의 Type 인 Apple 선언 생략 (전달인자가 1개일 경우 괄호도 생략 가능)

List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor()));
  • 자바 컴파일러가 명시적으로 형식의 추론이 가능하다면 람다 표현식의 간략한 작성이 가능하다.
  • 이것이 이해가 안될 경우 다이아몬드 연산자를 참고하자.

지역 변수 사용

  • 람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수(Free Variable)(파라메터에서 전달한 값이 아닌 외부 변수)를 사용할 수 있다.
1
2
int portNum = 1337;
Runnable r = () -> System.out.println(portNum);
  • 위와 같은 동작을 람다 캡쳐링(Capturing lambda) 라고 한다.
  • 하지만 자유 변수를 람다에서 사용할 때는 제약이 존재한다.
  • final로 선언되어 있거나 final로 선언된 변수와 똑같이 사용되어야 한다.
  • 즉 자유 변수를 변경하는 행위는 불가능 하다.
1
2
3
int portNum = 1337;
Runnable r = () -> System.out.println(portNum);
portNum = 2222; //에러 발생
  • 이런 제약이 생긴 이유는 아래와 같다.
  • 지역 변수(클래스 내의 메서드에 선언된 변수)는 힙 메모리 공간에 저장이 된다.
  • 인스턴스 변수(클래스로 바로 접근 가능한 변수)의 경우 스택 메모리 공간에 저장이 된다.
  • 람다식에서 지역 변수에 바로 접근이 가능하다고 할 때 람다가 쓰레드에서 실행될 때 변수를 할당한 쓰레드가 사라질 경우 변수 할당이 해제되었음에도 불구하고 람다를 실행하는 쓰레드에서는 해당 변수에 접근하게 되고 에러가 발생한다.
  • 위와 같은 이유로 인해 자바에서는 원래 변수에 접근을 허용하는 것이 아닌 자유 지역 변수의 복사본을 제공.
  • 복사본의 값은 바뀌지 않아야 하기 때문에 한번만 값을 할당(final 처럼) 해야 한다는 제약이 생긴 것.

메서드 레퍼런스

1
2
someList.sort((Item a1, Item a2) -> a1.getCount().compareTo(a2.getCount())); //기존 람다식
someList.sort(comparing(Item::getCount)); //메서드 레퍼런스 사용
  • 람다식 보다 가독성이 좋다.
  • 메서드 레퍼런스를 만드는 방법은 세 가지이다.
    1. 정적 메서드 레퍼런스 (static method)
      • Integer의 parseInt의 경우 Integer::parseInt 로 표현 가능
    2. 다양한 형식의 인스턴스 메서드 레퍼런스 (객체의 메서드 - 클래스 명으로 접근)
      • String의 length 메서드는 String::length 로 표현 가능
      • String의 toUpperCase 메서드는 String::toUpperCase 로 표현 가능
    3. 기존 객체의 인스턴스 메서드 레퍼런스 (객체의 메서드 - 변수 명으로 접근)
      • 특정 클래스 객체의 변수 명으로 접근
      • member라는 객체에 String을 반환하는 메서드인 getName()이 있다면 member::getName() 으로 표현 가능

생성자 레퍼런스

  • ClasssName::new 이런 형태로 생성자 레퍼런스를 만들 수 있다.
  • Market이라는 클래스내에 Apple의 객체를 받는 생성자가 있다면 아래의 예제 코드와 같이 표현할 수 있다.
1
2
Market market = Apple::new;
Apple apple = market.getApple();

람다, 메서드 레퍼런스 활용하기

  • 일반적으로 객체를 상속받거나 구현을 하여 전달을 해보고, 이것이 익명 클래스로 사용이 될 수 있다면, 람다식으로 표현이 가능하다.
  • 아래의 코드를 참고
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class LambdaMethodReferExam {
private List<Apple> inventory; //예제를 위해 Apple 객체를 담을 리스트

/**
* Default Construct
*/
public LambdaMethodReferExam() {
inventory = getAppleList(Apple::new);
}

/**
* 실제 실행 코드
*/
public void runCode() {
System.out.println("Sort before");
inventory.forEach(apple -> System.out.println("Apple weight = " + apple.getWeight() + " color = " + apple.getColor()));

//코드 전달 방법
//inventory.sort(new AppleComparator());

//익명 클래스 사용
// inventory.sort(new Comparator<Apple>() {
// @Override
// public int compare(Apple o1, Apple o2) {
// return o1.getWeight().compareTo(o2.getWeight());
// }
// });

//lambda 사용
//inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));

//메서드 레퍼런스 사용
inventory.sort(Comparator.comparing(Apple::getWeight));

System.out.println("Sort after");
inventory.forEach(apple -> System.out.println("Apple weight = " + apple.getWeight() + " color = " + apple.getColor()));
}


/**
* 예제의 리스트를 사용하기 위한 메서드
* @param bf
* @return
*/
private List<Apple> getAppleList(BiFunction<Integer, String, Apple> bf) {
List<Apple> result = new ArrayList<>();
result.add(bf.apply(300, "Red"));
result.add(bf.apply(110, "Green"));
result.add(bf.apply(90, "RedBlue"));
result.add(bf.apply(210, "RedGreen"));
result.add(bf.apply(220, "GreenYellow"));
return result;
}


/**
* Apple 객체의 무게를 비교하기 위한 내부 클래스 (코드 전달 예제)
*/
class AppleComparator implements Comparator<Apple> {

/**
* 동일한 경우 0, 첫 값이 큰 경우 양수, 첫 값이 작은 경우 음수
* @param o1
* @param o2
* @return
*/
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
}
}

람다 표현식을 조합할 수 있는 유용한 메서드

  • Comparator, Predicate, Function 와 같은 함수형 인터페이스를 통해 람다식을 조합할 수 있도록 유틸리티 메서드를 제공한다.
  • Comparator
    • reversed() : 역 정렬
    • thenComparing() : 비교 대상의 값이 같은 경우 다음 정렬 대상 지정
  • Predicate
    • or() : 체인으로 사용
    • and() : 체인으로 사용
  • Function
    • andThen() : 앞의 연산을 먼저 수행한 값을 전달인자의 함수와 연산
    • compose() : 전달인자의 함수 연산을 먼저 수행 후 앞의 연산을 진행