주고받을 수 있는 동작
람다 표현식은 숫자나 문자열처럼 메서드에 넘기거나, 변수에 저장하거나, 반환할 수 있는 간결한 코드 블록입니다. 람다가 등장하기 전에는 동작을 넘기려면 메서드 하나를 감싸기 위해 클래스 전체(혹은 장황한 익명 클래스)를 작성해야 했습니다. 람다는 이를 본질, 즉 매개변수와 본문만으로 압축합니다.
람다는 항상 함수형 인터페이스, 즉 추상 메서드가 정확히 하나뿐인 인터페이스를 구현합니다(인터페이스 페이지 끝에서 만나 보았습니다). 컴파일러는 문맥을 통해 어떤 인터페이스를 의미하는지 알아내고, 여러분의 람다를 그 유일한 메서드에 대응시킵니다.
x -> x * 2 가 apply 의 구현 전체입니다. new 도, 클래스 본문도, 메서드 이름도 없습니다. 그 모든 것은 인터페이스가 제공합니다.
화살표 문법
모든 람다는 매개변수 -> 본문 형태를 가집니다. 필요한 만큼 각 부분이 유연하게 바뀝니다.
() -> 42 // 매개변수 없음
x -> x + 1 // 매개변수 한 개, 괄호 선택 사항
(x, y) -> x + y // 매개변수가 둘 이상이면 괄호 필요
(int x, int y) -> x + y // 타입은 선택 사항 - 보통 추론된다
x -> { // 블록 본문에는 중괄호와 return 이 필요
int doubled = x * 2;
return doubled + 1;
}
단일 표현식 본문(x -> x + 1)은 return 키워드 없이 그 값을 암묵적으로 반환합니다. 중괄호를 쓰는 순간 일반 블록을 작성하는 것이며, 메서드가 무언가를 반환한다면 return 을 명시적으로 써야 합니다. 흔한 실수는 둘을 섞는 것입니다. x -> { x + 1 } 는 컴파일되지 않습니다. 블록에는 문장이 필요하기 때문입니다(return x + 1;).
람다는 익명 클래스를 대체한다
람다가 무엇을 가져다주는지 가장 분명하게 보여 주는 것은 전후 비교입니다. 사용자 정의 Comparator 로 정렬하는 코드는 예전에 이렇게 생겼습니다.
// 이전 - 익명 클래스
names.sort(new Comparator<String>() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});
같은 일을 람다로 하면 읽기 좋은 한 줄이 됩니다.
Comparator 는 함수형 인터페이스이므로(유일한 추상 메서드가 compare 입니다) 람다가 그대로 끼워집니다. new, 클래스, 메서드 시그니처 같은 형식적인 부분은 모두 사라지고 비교 로직만 남습니다.
java.util.function 도구 상자
직접 함수형 인터페이스를 선언해야 하는 경우는 드뭅니다. java.util.function 패키지에는 흔히 쓰는 형태가 들어 있고, 자바 라이브러리 API의 거의 전부가 이를 받아들입니다.
Function<T, R>-T를 받아R을 반환한다 (apply)Predicate<T>-T를 받아boolean을 반환한다 (test)Supplier<T>- 아무것도 받지 않고T를 반환한다 (get)Consumer<T>-T를 받고 아무것도 반환하지 않는다 (accept)
이들은 제네릭 인터페이스입니다. Function<String, Integer> 는 이전 페이지에서 본 제네릭을 재사용해 타입 안전성을 유지합니다. 코드가 받아들이고 만들어 내야 하는 것에 맞는 형태를 고르세요.
메서드 참조
람다가 이미 존재하는 메서드 하나를 호출하는 일밖에 하지 않을 때는, :: 를 사용해 메서드 참조로 바꿀 수 있습니다. 같은 값을 더 직접적으로 쓴 것입니다.
메서드 참조에는 여러 형태가 있습니다. String::toUpperCase(각 인수에 대해 호출되는 인스턴스 메서드), Math::max(정적 메서드), System.out::println(특정 객체의 메서드), ArrayList::new(생성자)입니다. 명확하게 읽힐 때만 사용하세요. 어떤 형태가 적용되는지 고민해야 한다면 평범한 람다로 충분합니다.
변수 캡처
람다는 자신을 둘러싼 메서드의 지역 변수를 사용할 수 있지만, 그것이 final 또는 사실상 final, 즉 한 번 할당된 뒤 절대 바뀌지 않는 경우에만 가능합니다. 자바는 람다가 생성되는 순간의 값을 캡처하므로, 이후에 바뀔 수 있는 변수는 모호해집니다.
factor 를 어디에서든 재할당하면 컴파일러는 "variable used in lambda expression should be final or effectively final" 이라며 람다를 거부합니다. 정말로 변경 가능한 공유 상태가 필요하다면 대신 객체(필드, 배열 요소, 또는 AtomicInteger)를 캡처하세요. 내용이 바뀌더라도 참조 자체는 고정되어 있기 때문입니다. 또한 람다는 익명 클래스와 달리 자신만의 스코프를 만들지 않습니다. 람다 안의 this 는 람다 자체가 아니라 둘러싼 인스턴스를 가리킵니다.
다음: Streams
람다는 구성 요소이지 목적지가 아닙니다. 그 진가는 Streams API에서 드러납니다. 거기서는 filter, map, reduce 같은 연산(각각 람다를 받습니다)을 연결해, 데이터 변환을 뒤엉킨 반복문이 아니라 읽기 좋은 파이프라인으로 표현합니다. 그것이 다음 페이지입니다.
자주 묻는 질문
자바에서 람다 표현식이란 무엇인가요?
람다는 함수형 인터페이스, 즉 추상 메서드가 하나뿐인 인터페이스의 인스턴스를 짧게 작성하는 방법입니다. 익명 클래스 전체를 쓰는 대신 매개변수 -> 본문 형태로 작성합니다. 컴파일러가 람다를 인터페이스의 유일한 메서드에 대응시키므로 Runnable r = () -> System.out.println("hi"); 는 완전한 Runnable 이 됩니다. 람다를 사용하면 동작을 데이터처럼 주고받을 수 있습니다.
자바에서 람다와 메서드 참조의 차이는 무엇인가요?
둘 다 함수형 인터페이스의 인스턴스를 만듭니다. 람다에는 명시적인 매개변수와 본문이 있고(s -> s.toUpperCase()), 메서드 참조는 이미 존재하는 하나의 메서드를 호출하기만 하는 람다를 줄여 쓴 표기입니다(String::toUpperCase). 람다가 인수를 이름 있는 메서드 하나에 그대로 넘기는 일밖에 하지 않는다면 메서드 참조를 쓰세요. 더 짧고 읽기 좋습니다.
자바 람다에서 사용하는 변수가 final 또는 사실상 final이어야 하는 이유는 무엇인가요?
람다는 자신을 둘러싼 메서드의 지역 변수를 캡처할 수 있지만, 할당 이후 한 번도 바뀌지 않는 경우에만 가능합니다. 이것이 바로 "사실상 final"의 의미입니다. 자바는 변수에 대한 살아 있는 참조가 아니라 값을 캡처하므로, 재할당을 허용하면 모호해지고 스레드 간에 안전하지 않습니다. 변경 가능한 공유 상태가 필요하다면 대신 필드나 배열, AtomicInteger 같은 래퍼를 사용하세요.