함수를 값처럼 넘기는 문법. 추상 메서드가 하나뿐인 인터페이스(함수형 인터페이스) 자리에 쓸 수 있다.
list.sort((a, b) -> a.age() - b.age());
list.forEach(System.out::println); // 메서드 참조
익명 클래스와 무엇이 다른가
-
익명 클래스 — 컴파일 시점에 별도 클래스 파일이 생긴다 (Outer$1.class)
- this 가 익명 클래스 자신을 가리킨다
- 클래스 로딩 비용이 개수만큼 든다
-
람다 — 클래스 파일이 안 생긴다
- invokedynamic 명령 + LambdaMetafactory 가
- '첫 호출 시점' 에 구현 객체를 만들어 준다
- this 가 바깥 인스턴스를 가리킨다
class A {
Runnable r1 = new Runnable() { public void run() { System.out.println(this); } }; // 익명 클래스
Runnable r2 = () -> System.out.println(this); // A 인스턴스
}
상태가 없으면 인스턴스를 재사용한다
Runnable r = () -> System.out.println("hi"); // 캡처하는 값이 없다 → 싱글턴처럼 재사용
Runnable s = () -> System.out.println(x); // x 를 캡처한다 → 호출마다 새 객체
캡처가 없으면 객체 생성 비용도 거의 없다 — 그래서 스트림 연산에 람다를 남발해도 대체로 괜찮다.
사실상 final만 캡처할 수 있다
int count = 0;
list.forEach(x -> count++); // 컴파일 에러: effectively final 이어야 한다
// 우회
int[] count = {0};
list.forEach(x -> count[0]++); // 동작하지만 병렬에서는 안전하지 않다
long n = list.stream().filter(...).count(); // ✅ 이런 건 스트림으로 표현한다
지역 변수는 스택에 있고 람다는 그보다 오래 살 수 있어서 값을 복사해 갖는다. 그래서 원본을 바꿀 수 없다.
주요 함수형 인터페이스
Function<T,R> T 를 받아 R 을 반환 apply
Predicate<T> T 를 받아 boolean test
Consumer<T> T 를 받고 반환 없음 accept
Supplier<T> 받지 않고 T 를 반환 get
UnaryOperator<T> T → T
- 원시 타입 특화도 있다 — IntPredicate · ToIntFunction (박싱을 피한다)
메서드 참조 네 가지
String::valueOf // 정적 메서드
system::println // 특정 인스턴스의 메서드
String::length // 임의 객체의 인스턴스 메서드 (첫 인자가 수신자가 된다)
Member::new // 생성자
예외가 걸림돌이 된다
list.forEach(f -> Files.readString(f)); // 컴파일 에러 — Consumer 는 검사 예외를 못 던진다
// 감싸는 유틸을 만들거나, 애초에 검사 예외를 피하는 API 를 쓴다
list.forEach(f -> { try { Files.readString(f); } catch (IOException e) { throw new UncheckedIOException(e); } });
면접 함정
- ❌ "람다는 익명 클래스의 문법 설탕" → 컴파일 결과가 다르다(
invokedynamic). - ❌ "람다 안에서 지역 변수를 바꿀 수 있다" → effectively final만 캡처된다.