3장
1. 람다란 무엇인가?
람다 표현식(Lambda expression)은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이다. 람다의 특징은 다음과 같다.
익명: 보통의 메서드와 달리 이름이 없다. 구현해야 할 코드가 줄어든다.
함수: 람다는 메서드처럼 특정 클래스에 종속되지 않아 함수라고 부른다. 그러나 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함한다.
전달: 람다 표현식을 메서드의 인수로 전달하거나 변수로 저장할 수 있다.
간결성: 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없다.
람다를 활용하면 코드를 간결하고 유연하게 바꿀 수 있다. 2장에서 작성한 Comparator 객체를 생각해보자.
Comparator<Apple> byWeight = new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
};
위 코드를 람다 표현식으로 변경하면 다음과 같다.
Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
람다는 아래 그림처럼 세 부분으로 이루어진다.

람다의 기본 문법은 두 가지이다.
표현식 스타일(expression style): (parameters) -> expression
블록 스타일(block style): (parameters) -> { statements; }
람다 예제와 사용 사례를 통해 람다를 알아보자.
boolean 표현식
(List<String> list) -> list.isEmpty()
객체 생성
() -> new Apple(10)
객체에서 소비
(Apple a) -> { System.out.println(a.getWeight()); } (Apple a) -> System.out.println(a.getWeight()); // 위와 동일
객체에서 선택/추출
(String s) -> s.length()
두 값을 조합
(int a, int b) -> a * b
두 객체 비교
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
2. 어디에, 어떻게 람다를 사용할까?
2장에서 구현했던 필터 메서드에도 람다를 활용할 수 있다.
List<Apple> greenApples = filter(inventory, (Apple a) -> GREEN.equals(a.getColor()));
람다 표현식은 이 밖에도 함수형 인터페이스 문맥에서 사용할 수 있다. 위 예제에서는 함수형 인터페이스 Predicate를 기대하는 filter 메서드의 두 번째 인수로 람다 표현식을 전달한다.
2.1. 함수형 인터페이스
함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스이다. @FunctionalInterface 어노테이션은 함수형 인터페이스를 가리킨다. 지금까지 살펴본 자바 API의 함수형 인터페이스로 Comparator, Runnable 등이 있다.
// java.util.Comparator
public interface Comparator<T> {
int compare(T o1, T o2);
}
// java.lang.Runnable
public interface Runnable {
void run();
}
람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있다. 따라서 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다. 예를 들어 Runnable은 함수형 인터페이스이므로 다음과 같이 구현할 수 있다.
// 익명 클래스 사용
Runnable r1 = new Runnable {
public void run() {
System.out.println("Hello world 1!");
}
}
// 람다 표현식 사용
Runnable r2 = () -> System.out.println("Hello world 2!");
2.2. 함수 디스크립터
함수 디스크립터(function description)란 시그니처를 서술하는 메서드를 의미한다. 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처와 동일한다. 예를 들어 Runnable 인터페이스의 유일한 추상 메서드 run은 인수와 반환값이 없으므로 Runnable 인터페이스는 인수와 반환값이 없는 시그니처이다. 따라서 () -> void로 표기한다. 한 개의 void를 반환하는 경우 중괄호로 감쌀 필요가 없음을 참고하자.
3. 람다 활용 : 실행 어라운드 패턴
실행 어라운드 패턴(execute around pattern)은 자원을 처리하는 코드를 설정(setup)과 정리(cleanup) 두 과정이 둘러싸는 형태를 의미한다.

자원 처리에 사용하는 순환 패턴(recurrent pattern)은 자원을 열고 처리한 다음 자원을 닫는 순서로 이루어진다. 자원 처리 예제는 다음과 같다.
public String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine(); // 핵심 코드
}
}
3.1. 1단계 : 동작 파라미터화
현재 코드는 파일에서 한 번에 한 줄만 읽는다. 다른 조건을 적용하기 위해서는 processFile의 동작을 파라미터화해야 한다. 람다를 활용해서 동작을 전달할 수 있다. 다음은 BufferReader에서 두 행을 출력하는 코드이다.
String result = processFile((BufferReader br) -> br.readLine() + br.readLine());
3.2. 2단계 : 함수형 인터페이스를 활용한 동작 전달
함수형 인터페이스 자리에 람다를 사용할 수 있다. BufferedReader -> String과 IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스는 다음과 같다.
@FunctinalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
정의한 인터페이스는 processFile 메서드의 인수로 전달할 수 있다.
public String processFile(BufferedReaderProcessor p) throws IOException { /* ... */ }
3.3. 3단계 : 동작 실행
이제 BufferedReaderProcessor에 정의된 process 메서드의 시그니처(BufferedReader -> String)과 일치하는 람다를 전달할 수 있다.
public String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
3.4. 4단계 : 람다 전달
이제 람다를 활용해서 다양한 동작을 processfile 메서드로 전달할 수 있다.
// 한 행을 처리하는 코드
String oneLine = processFile((BufferedReader br) -> br.readLine());
// 두 행을 처리하는 코드
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
4. 함수형 인터페이스
함수형 인터페이스는 오직 하나의 추상 메서드를 지정한다. 함수형 인터페이스의 추상 메서드는 람다 표현식의 시그니처를 정의한다. 함수형 인터페이스의 추상 메서드 시그니처는 함수 디스크립터(function descriptor)라고 한다. 다양한 람다 표현식을 활용하기 위해 자바 8의 함수형 인터페이스를 알아보자.
4.1. Predicate
@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> notemptyStringPredicate = (String s) -> !s.isEmpty();
List<String> notEmpty = filter(listOfStrings, nonEmptyStringPredicate);
java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 boolean을 반환한다. T 형식의 객체를 사용하는 boolean 표현식이 필요한 상황에서 사용할 수 있다.
4.2. Consumer
@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)
);
java.util.function.Consumer<T> 인터페이스는 제네릭 형식 T 객체를 받아 void를 반환하는 accept라는 추상 메서드를 정의한다. T 형식의 객체를 인수로 받아 어떤 동작을 하고 싶을 때 사용할 수 있다.
4.3. Function
@FunctionalInterface
public interface Function<T, R> {
R applt(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 s) -> s.length()
);
java.util.function.Function<T, R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의한다. 입력을 출력으로 매핑하는 람다를 정의할 때 사용할 수 있다.
5. 형식 검사, 형식 추론, 제약
람다 표현식 자체에는 어떤 함수형 인터페이스를 구현하는지 정보가 포함되어 있지 않다. 따라서 람다 표현식을 제대로 이해하려면 람다의 실제 형식을 알아야 한다.
5.1. 형식 검사
람다가 사용되는 콘텍스트(context)를 이용해서 람다의 형식을 추론할 수 있다. 대상 형식(target type)은 어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 말한다.
List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);
위 코드는 그림 3-4의 순서대로 형식 확인 과정을 거친다.

5.2. 같은 람다, 다른 함수형 인터페이스
대상 형식이라는 특징 대문에 같은 람다 표현식이라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있다.
5.3. 형식 추론
자바 컴파일러는 람다 표현식이 사용된 콘텍스트(대상 형식)로 람다 표현식과 관련된 함수형 인터페이스를 추론한다. 대상 형식을 이용해서 함수 디스크립터를 알 수 있으므로 람다의 시그니처도 추론할 수 있다. 결과적으로 컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있기 때문에 람다 문법에서 생략할 수 있다.
// 형식을 추론하지 않음
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
// 형식을 추론함
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
5.4. 지역 변수 사용
람다 표현식에서는 자유 변수를 활용할 수 있다. 자유 변수(free variable)란 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수를 의미한다. 람다 표현식에서 자유 변수를 활용하는 걸 람다 캡처링(capturing lambda)이라고 말한다. 다음은 람다 캡처링 예제이다.
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
다만 지역 변수는 final로 선언되거나 실질적으로 final처럼 취급되어야 한다. 만일 portNumber에 두 번 값을 할당하면 컴파일할 수 없다.
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 1338; // 컴파일 에러
6. 메서드 참조
메서드 참조를 활용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다.
// 기존
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
// 메서드 참조
inventory.sort(comparing(Apple::getWeight));
메서드 참조는 특정 메서드만을 호출하는 람다 표현식의 축약형으로 코드의 가독성을 높인다. 메서드 명 앞에 구분자(::)를 붙이는 방식으로 메서드 참조를 활용할 수 있다.
(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
6.1. 메서드 참조를 만드는 법
메서드 참조는 세 가지 유형으로 구분한다.
정적 메서드 참조
(args) -> ClassName.staticMethod(args)
ClassName::staticMethod
다양한 형식의 인스턴스 메서드 참조
(arg0, rest) -> arg0.instanceMethod(rest)
ClassName::instanceMethod
기존 객체의 인스턴스 메서드 참조
(args) -> expr.instanceMethod(args)
expr::instanceMethod
6.2. 생성자 참조
ClassName::new처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다.
// 람다 표현식 활용
Supplier<Apple> c1 = () -> new Apple();
Apple a1 = c1.get();
// 생성자 참조 활용
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();
생성자 참조는 다양한 상황에 응용할 수 있다.
7. 람다, 메서드 참조 활용하기
앞서 배운 내용을 활용해서 람다를 복습해보자. 최종적으로 만들 코드는 다음과 같다.
inventory.sort(comparing(Apple::getWeight));
7.1. 1단계 : 코드 전달
sort 메서드는 다음과 같은 시그니처를 갖는다.
void sort(Comparator<? super E> c)
Comparator 객체를 인수로 받아 두 사과를 비교하는데 객체 안에 동작을 포함시키는 방식으로 다양한 전략을 전달할 수 있다. sort에 전달된 정렬 전략에 따라 sort의 동작이 달라지므로 sort의 동작은 파라미터화 되었다. 다음은 첫 번째 정렬 코드이다.
public class AppleComparator implements Comparator<Apple> {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
7.2. 2단계 : 익명 클래스 사용
다음은 익명 클래스를 활용한 두 번째 정렬 코드이다.
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
});
7.3. 3단계 : 람다 표현식 사용
자바 8에서는 람다 표현식을 활용해서 코드를 전달할 수 있다. 함수형 인터페이스를 기대하는 곳 어디서나 람다 표현식을 사용할 수 있다. 함수형 인터페이스는 오직 하나의 추상 메서드를 정의하고 추상 메서드의 시그니처(함수 디스크립터)는 람다 표현식의 시그니처를 정의한다. Comparator의 함수 디스크립터는 (T, T) -> int다. 사과를 사용하면 (Apple, Apple) -> int다. 다음은 람다 표현식을 활용한 세 번째 정렬 코드이다.
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 활용해서 람다의 파라미터 형식을 추론하므로 보다 간결하게 작성 가능하다.
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
comparing 메서드를 사용하면 조금 더 간단하게 작성할 수 있다.
import static java.util.Comparator.comparing;
inventory.sort(comparing(apple -> apple.getWeight()));
7.4. 4단계 : 메서드 참조 사용
메서드 참조를 활용하면 람다 표현식의 인수를 더 깔끔하게 전달할 수 있다.
import static java.util.Comparator.comparing;
inventory.sort(comparing(Apple::getWeight));
8. 람다 표현식을 조합할 수 있는 유용한 메서드
자바 8 API의 Comparator, Function, Predicate 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 메서드를 제공한다. 간단히 말해 여러 개의 람다 표현식을 조합해 복잡한 람다 표현식을 만들 수 있다. 이것을 가능하게 하는 건 디폴트 메서드(default method)다.
8.1. Comparator 조합
Comparator 인터페이스는 reverse, thenComparing 두 가지 디폴트 메서드를 제공한다. reverse는 주어진 비교자의 순서를 뒤바꾼다. thenComparing은 함수를 인수로 받아 첫 번째 비교자를 이용해서 두 객체가 같다고 판단되면 두 번째 비교자에 객체를 전달한다.
// 기본
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
// 기본 + 역정렬
inventory.sort(comparing(Apple::getWieght).reversed());
// 기본 + 역정렬 + Comperator 연결
inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry));
8.2. Predicate 조합
Predicate 인터페이스는 복잡한 프레디케이트를 만들 수 있도록 negate, and, or 세 가지 메서드를 제공한다.
// 기본
Predicate<Apple> redApple = redApple;
// neagte(반전)
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(a.getColor()));
8.3. Function 조합
Function 인터페이스는 andThen, compose 두 가지 디폴트 메서드를 제공한다. andThen 메서드는 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환한다. compose 메서드는 인수로 주어진 함수를 먼저 실행한 다음 그 결과를 외부 함수의 인수로 제공한다.
// andThen
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g); // g(f(x))
int result = h.apple(1); // 4를 반환
// compose
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g); // f(g(x))
int result = h.apple(1); // 3을 반환
참고 자료
모던 자바 인 액션 - 한빛미디어
Last updated