Java 람다식은 이름 없는 코드 조각을 단독 값처럼 아무 곳에나 저장하는 문법이 아니다. 컴파일러가 Predicate<String>, Comparator<String> 같은 함수형 인터페이스를 target type으로 알아낼 수 있는 문맥에서 그 인터페이스의 단일 추상 메서드 구현을 표현한다. Java 8에서 람다가 도입됐다는 사실보다 이 연결을 이해하는 것이 먼저다.
기존에 JDK 1.8 람다를 공부하며 링크만 모아 둔 메모를, 현재 Java 언어 명세를 기준으로 다시 정리했다.
함수형 인터페이스가 target type이 된다
함수형 인터페이스는 하나의 추상 메서드 계약을 가진 인터페이스다. default와 static 메서드는 그 계약에 포함되지 않고, Object의 public 메서드를 재정의하는 선언도 함수형 메서드 계산에서 제외된다.
@FunctionalInterface
interface TextRule {
boolean test(String value);
default String description() {
return "문자열 조건";
}
}
@FunctionalInterface는 람다 사용에 필수는 아니다. 다만 인터페이스가 함수형 인터페이스 조건을 잃으면 compiler가 오류를 내므로 의도를 문서화하는 데 유용하다.
TextRule hasText = value ->
value != null && !value.trim().isEmpty();
System.out.println(hasText.test("lambda")); // true
위 람다의 매개변수와 반환 타입은 TextRule.test에서 결정된다. 람다 자체만 떼어 놓고는 어떤 인터페이스 인스턴스가 되어야 하는지 알 수 없다.
같은 모양도 문맥에 따라 다른 타입이 된다
import java.util.function.Predicate;
Predicate<String> hasText = value ->
value != null && !value.trim().isEmpty();
대입문에서는 왼쪽 타입이 target type이다. 메서드 호출의 argument, return 문맥, 명시적 cast에서도 target type을 얻을 수 있다. 반대로 호환되는 overload가 여러 개라면 compiler가 어느 함수형 인터페이스인지 결정하지 못해 모호성 오류가 날 수 있다.
이미 존재하는 메서드를 그대로 호출하는 람다는 method reference가 의도를 더 잘 드러낼 때가 있다.
import java.util.Comparator;
Comparator<String> byLength =
Comparator.comparingInt(String::length);
항상 method reference가 더 짧거나 읽기 좋은 것은 아니다. 매개변수 변환이나 조건이 들어가면 이름을 드러낸 람다가 더 명확할 수 있다.
지역 변수는 final 또는 effectively final이어야 한다
람다 body에서 바깥 지역 변수를 capture하려면 명시적으로 final이거나, 선언 후 다시 대입하지 않은 effectively final이어야 한다.
int minimumLength = 5;
Predicate<String> isLongEnough =
value -> value.length() >= minimumLength;
다음처럼 나중에 값을 다시 대입하면 capture할 수 없다.
int minimumLength = 5;
minimumLength = 10;
// compile error
Predicate<String> rule =
value -> value.length() >= minimumLength;
여기서 변수 재대입과 객체 상태 변경은 구분해야 한다. effectively final인 지역 변수가 mutable 객체를 가리킨다면 그 객체의 내용까지 immutable이 되는 것은 아니다. 가능하다는 이유로 람다에서 공유 mutable state를 바꾸면 병렬 stream이나 비동기 실행에서 race condition을 만들 수 있다.
람다의 this는 새 객체를 가리키지 않는다
람다 body의 this는 람다가 작성된 enclosing instance를 뜻한다. anonymous class처럼 별도의 this scope를 만들지 않는다.
class Greeter {
private final String prefix = "안녕하세요";
Runnable task() {
return () -> System.out.println(this.prefix);
}
}
람다 평가 결과는 target 함수형 인터페이스의 인스턴스지만, 특정 anonymous class로 번역된다고 가정해서는 안 된다. class name, 객체 identity, serialization 형태에 의존하는 코드도 피한다. 구현 방식은 JVM과 compiler가 바꿀 수 있는 영역이다.
선택 기준
- 동작 하나를 argument로 전달하거나 collection 처리 조건을 표현할 때 람다를 검토한다.
- 먼저 적절한 함수형 인터페이스와 입력·출력 계약을 정한다.
- 긴 business logic은 이름 있는 메서드나 클래스로 옮겨 테스트 경계를 만든다.
- capture한 mutable state가 있는지 확인한다.
- identity, 구체 class, serialization에 기대지 않는다.
참고 자료
'배움과 성장 > 소프트웨어 개발' 카테고리의 다른 글
| W3Schools Java 튜토리얼 복습: 추상 클래스·인터페이스·컬렉션 (0) | 2022.10.20 |
|---|---|
| Java 생성자와 빌더: 무조건 Builder가 정답은 아니었다 (0) | 2022.10.18 |
| Java String·StringBuilder·StringBuffer 차이: 문자열 연결 기준 정리 (4) | 2022.09.10 |
| Java Comparable과 Comparator 차이: 객체 정렬 기준을 안전하게 만드는 법 (4) | 2022.02.20 |
| Java 생성자와 초기화 순서: default constructor·this·super 정확히 이해하기 (0) | 2022.02.20 |
댓글