Java8 In Action - 2. 람다 표현식


Java8 In Action 을 읽고 정리한 내용이다. 람다 표현식을 어떻게 만드는지, 어떻게 사용하는지, 어떻게 코드를 간결하게 만들수 있는지 설명한다.

람다란 무엇인가?

람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단수화한 것이라고 할 수 있다.

1
2
3
4
5
6
Comparator<Apple> byWeight = new Comparator<>() {
@Override
public int compare(Apple a1, Apple a2) {
return a1.getColor().compareTo(a2.getColor());
}
};

람다를 이용한 코드

1
Comparator<Apple> byWeightLambda = (Apple a1, Apple a2) -> a1.getColor().compareTo(a2.getColor());

람다를 이용한 코드가 더 간결해졌다.

  1. (Apple a1, Apple a2)는 람다 파라미터
  2. ->는 화살표
  3. a1.getColor().compareTo(a2.getColor());는 람다 바디

람다 예제표

사용 사례 람다 예제
불린 표현식 (List list) -> list.isEmpty()
객체 생성 () -> new Apple(10)
객체에서 소비 (Apple a) -> {System.out.println(a.getWeight());}
객체에서 선택/추출 (String s) -> s.length()
두 값을 조합 (int a, intb) -> a * b
두 객체 비교 (Apple a1, Apple a2) -> a1.getColor().compareTo(a2.getColor())

어떻게 람다를 사용할까?

함수형 인터페이스라는 문맥에서 람다표현식을 사용할 수 있다.

함수형 인터페이스

함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스이다. 자바 API의 함수형 인터페이스 Comparator, Runnable 등이 있다.

Comparator.java
1
2
3
public interface Comparator<T> {
int compare(T o1, T o2);
}

Runnable.java
1
2
3
public interface Runnable {
void run();
}

오직 하나의 추상 메서드를 가진 함수 인터페이스 Runnable을 람다로 표현 했다.

1
Runnable run = () -> System.out.println("Hello World!");

함수 디스크립터

람다 표현식의 시그너처를 서술하는 메서드를 함수 디스크립터(function descriptor)라고 부른다. 예로 (Apple) -> booleantest메서드의 시그니처는 boolean이다.

1
Predicate<Apple> a = (Apple a) -> true;

람다 활용: 실행 어라운드 패턴

실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸인 형태를 실행 어라운드 패턴이라고 부른다.

자바 7에 새로 추가된 try-with-resources 구문을 사용하면 명시적으로 닫을 필요가 없다.

1
2
3
4
5
public static String processFile() throws Exception {
try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}

1단계: 함수형 인터페이스를 이용해서 동작 전달

시그니처와 일치하는 함수형 인터페이스 생성

BufferedReaderProcessor.java
1
2
3
4
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}

2단계: 동작 실행!

BuffrerdReaderProcessor에 정의된 process 메서드의 시그니처(BufferedReader -> String)와 일치하는 람다를 전달할 수 있다.

1
2
3
4
5
public static String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}

3단계: 람다 전달

1
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());

함수형 인터페이스 사용

자바8 라이브러리 java.util.function 패키지로 여러 가지 새로운 함수형 인터페이스를 제공한다.

Predicate

java.util.function.Predicate<T>인터페이스는 test라는 추상 메서드를 정의하면 test는 제네릭 형식 T의 객체를 인수로 받아 불린을 반환한다.

Predicate.java
1
2
3
4
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}

LambdaPredicate.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LambdaPredicate {
public static void main(String[] args) {
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();

List<String> listOfString = Arrays.asList("green", "red", "");
List<String> nonEmpty = filter(listOfString, nonEmptyStringPredicate);
}

public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for (T s : list) {
if (p.test(s)) {
results.add(s);
}
}
return results;
}
}

Consumer

java.util.function.Consumer<T>인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환 하는 accept라는 추상 메서드를 정의한다.

Consumer.java
1
2
3
4
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}

LambdaCunsumer.java
1
2
3
4
5
6
7
8
9
10
11
public class LambdaCunsumer {
public static void main(String[] args) {
forEach(Arrays.asList(1, 2, 3, 4, 5), (Integer i) -> System.out.println(i));
}

public static <T> void forEach(List<T> list, Consumer<T> c) {
for (T i : list) {
c.accept(i);
}
}
}

Function

java.util.function.Function<T, R>인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 apply라는 추상 메서드를 정의한다.

Function.java
1
2
3
4
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}

LambdasFunction.java
1
2
3
4
5
6
7
8
9
10
11
12
13
public class LambdasFunction {
public static void main(String[] args) {
List<Integer> l = map(Arrays.asList("lambdas" ,"in", "action"), (String s) -> s.length());
}

public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<>();
for (T s : list) {
result.add(f.apply(s));
}
return result;
}
}

형식 검사, 형식 추론, 제약

람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지의 정보가 포함되어 있지 않기 때문에, 람다의 실제 형식을 파악해야 한다.

형식 검사

람다가 사용되는 콘테스트(context)를 이용해서 람다의 형식(type)을 추론할 수 있다. 어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 대상 형식(target type)이라고 한다.

1
List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);

형식 확인

  1. filter 메서드의 선언을 확인한다.
  2. filter 메서드는 두 번재 파라미터로 Predicate<Apple> 형식(대상 형식)을 기대한다.
  3. Predicate<Apple>은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스다.
  4. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사한다.
  5. filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 한다.

형식 추론

자바 컴파일러는 람다 표현식이 사용된 콘텍스트(대상 형식)를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다.

1
2
3
4
// 형식을 추론 하지 않음
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getColor().compareTo(a2.getColor());
// 형식을 추론함
Comparator<Apple> c2 = (a1, a2) -> a1.getColor().compareTo(a2.getColor());

직역 변수 사용

자유 변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용 하는 것을 람다 캡처링(capturing lambda)라고 부른다.

1
2
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);

람다는 한번 만 할당할 수 있는 지역 변수를 캡처할 수 있다.(인스턴스 변수 캡처는 final 지역 변수 this를 캡처하는 것과 마찬가지다.)

1
2
3
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 1337;

컴파일 에러 발생

메서드 레퍼런스

메서드 레퍼런스를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다.

메서드 레퍼런스 요약

람다 메서드 레퍼런스 단축 표현
(Apple a) -> a.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

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

동작 파라미터, 익명 클래스, 람다 표현식, 메서드 레퍼런스 예제로 확인 한다.

1단계: 코드 전달

자바8의 List API에서 정렬 메서드(sort)를 제공해준다. 2개의 값을 받아 비교하는 코드를 작성 하면 sort의 동작은 파라미터화 된다.

AppleComparator.java
1
2
3
4
5
6
public class AppleComparator implements Comparator<Apple> {
@Override
public int compare(Apple a1, Apple a2) {
return a1.getColor().compareTo(a2.getColor());
}
}

Lambdas.java
1
2
3
4
5
6
7
8
9
10
11
public class Lambdas {
public static void main(String[] args) {
List<Apple> inventory = Arrays.asList(
new Apple(100, "white")
, new Apple(160, "green")
, new Apple(170, "red"));
inventory.sort(new AppleComparator());

inventory.forEach(System.out::println);
}
}

결과 화면

1
2
3
Apple(weight=160, color=green)
Apple(weight=170, color=red)
Apple(weight=100, color=white)

2단계: 익명 클래스 사용

Lambdas.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Lambdas {
public static void main(String[] args) {
List<Apple> inventory = Arrays.asList(
new Apple(100, "white")
, new Apple(160, "green")
, new Apple(170, "red"));
inventory.sort(new Comparator<Apple>() {
@Override
public int compare(Apple a1, Apple a2) {
return a1.getColor().compareTo(a2.getColor());
}
});

inventory.forEach(System.out::println);
}
}

결과 화면

1
2
3
Apple(weight=160, color=green)
Apple(weight=170, color=red)
Apple(weight=100, color=white)

3단계: 람다 표현식 사용

Lambdas.java
1
2
3
4
5
6
7
8
9
10
11
public class Lambdas {
public static void main(String[] args) {
List<Apple> inventory = Arrays.asList(
new Apple(100, "white")
, new Apple(160, "green")
, new Apple(170, "red"));
inventory.sort((Apple a1, Apple a2) -> a1.getColor().compareTo(a2.getColor()));

inventory.forEach(System.out::println);
}
}

결과 화면

1
2
3
Apple(weight=160, color=green)
Apple(weight=170, color=red)
Apple(weight=100, color=white)

형식을 추론

Lambdas.java
1
2
3
4
5
6
7
8
9
10
11
public class Lambdas {
public static void main(String[] args) {
List<Apple> inventory = Arrays.asList(
new Apple(100, "white")
, new Apple(160, "green")
, new Apple(170, "red"));
inventory.sort((a1, a2) -> a1.getColor().compareTo(a2.getColor()));

inventory.forEach(System.out::println);
}
}

결과 화면

1
2
3
Apple(weight=160, color=green)
Apple(weight=170, color=red)
Apple(weight=100, color=white)

정적 메서드 comparing 사용

Lambdas.java
1
2
3
4
5
6
7
8
9
10
11
public class Lambdas {
public static void main(String[] args) {
List<Apple> inventory = Arrays.asList(
new Apple(100, "white")
, new Apple(160, "green")
, new Apple(170, "red"));
inventory.sort(Comparator.comparing((a1) -> a1.getColor()));

inventory.forEach(System.out::println);
}
}

결과 화면

1
2
3
Apple(weight=160, color=green)
Apple(weight=170, color=red)
Apple(weight=100, color=white)

4단계: 메서드 레퍼런스 사용

Lambdas.java
1
2
3
4
5
6
7
8
9
10
11
public class Lambdas {
public static void main(String[] args) {
List<Apple> inventory = Arrays.asList(
new Apple(100, "white")
, new Apple(160, "green")
, new Apple(170, "red"));
inventory.sort(Comparator.comparing(Apple::getColor));

inventory.forEach(System.out::println);
}
}

결과 화면

1
2
3
Apple(weight=160, color=green)
Apple(weight=170, color=red)
Apple(weight=100, color=white)

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

자바 API의 몇몇 함수형 인터페이스는 다양한 유틸리티 메서드를 포함한다. Comparator, Predicate같은 항수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 메서드를 제공 한다.

Comparator 조합

역정렬

인터페이스 자체에서 조어진 비교자의 순서를 뒤바꾸는 reverse라는 디폴트 메서드를 제공한다.

Lambdas.java
1
2
3
4
5
6
7
8
9
10
11
public class Lambdas {
public static void main(String[] args) {
List<Apple> inventory = Arrays.asList(
new Apple(100, "white")
, new Apple(160, "green")
, new Apple(170, "red"));
inventory.sort(Comparator.comparing(Apple::getColor).reversed());

inventory.forEach(System.out::println);
}
}

결과 화면

1
2
3
Apple(weight=100, color=white)
Apple(weight=170, color=red)
Apple(weight=160, color=green)

Comparator 연결

2개의 값을 비교 할때 같은 값이 있으면, 다른 값으로 정렬하는 thenComparing을 이용하면 된다. 즉, 색깔을 내림차순으로 정렬하고 같은 색깔(red)이 있으면, 무게로 정렬 한다.

Lambdas.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Lambdas {
public static void main(String[] args) {
List<Apple> inventory = Arrays.asList(
new Apple(100, "white")
, new Apple(110, "green")
, new Apple(170, "red")
, new Apple(100, "red"));
inventory.sort(Comparator.comparing(Apple::getColor)
.reversed()
.thenComparing(Apple::getWeight));

inventory.forEach(System.out::println);
}
}

결과 화면

1
2
3
4
Apple(weight=100, color=white)
Apple(weight=100, color=red)
Apple(weight=170, color=red)
Apple(weight=110, color=green)

Predicate 조합

Predicate 인터페이스는 복잡한 프레디케이트를 만들 수 있도록 negate, and, or 세 가지 메서드를 제공한다.

negate

기존 프레디케이트 객체의 결과를 반전시킨 객체를 만든다.

Lambdas.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Lambdas {
public static void main(String[] args) {
List<Apple> inventory = Arrays.asList(
new Apple(100, "white")
, new Apple(110, "green")
, new Apple(170, "red")
, new Apple(100, "red"));
Predicate<Apple> appleGreenTrue = (Apple apple) -> "green".equals(apple.getColor());
inventory.forEach((Apple apple) -> System.out.println(appleGreenTrue.test(apple)));

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

Predicate<Apple> appleGreenFalse = appleGreenTrue.negate();
inventory.forEach((Apple apple) -> System.out.println(appleGreenFalse.test(apple)));
}
}

결과 화면

1
2
3
4
5
6
7
8
9
false
false
false
true
------------------------------------
true
true
true
false

and

&&와 같은 조건

Lambdas.java
1
2
3
4
5
6
7
8
9
10
11
12
public class Lambdas {
public static void main(String[] args) {
List<Apple> inventory = Arrays.asList(
new Apple(100, "white")
, new Apple(110, "green")
, new Apple(170, "red")
, new Apple(100, "red"));
Predicate<Apple> redAndHeavy = appleGreenTrue.and((Apple apple) -> apple.getWeight() > 90);
inventory.forEach((Apple apple) -> System.out.println(redAndHeavy.test(apple)));
inventory.forEach((Apple apple) -> System.out.println(redAndHeavy.test(apple)));
}
}

결과 화면

1
2
3
4
false
true
true
false

or

||와 같은 조건

Lambdas.java
1
2
3
4
5
6
7
8
9
10
11
12
public class Lambdas {
public static void main(String[] args) {
List<Apple> inventory = Arrays.asList(
new Apple(100, "white")
, new Apple(110, "green")
, new Apple(170, "red")
, new Apple(100, "red"));
Predicate<Apple> redAndHeavyOrGreen = appleGreenTrue.and((Apple apple) -> apple.getWeight() > 90)
.or((Apple apple) -> "green".equals(apple.getColor()));
inventory.forEach((Apple apple) -> System.out.println(redAndHeavyOrGreen.test(apple)));
}
}

결과 화면

1
2
3
4
false
true
true
true

소스코드

참조