1. 개요
C# 은 2007년, JavaScript 는 처음부터 람다(익명 함수)가 있었다. 자바는 2014년 Java 8 에서야 들어왔다. 거의 10년 넘게 늦었던 셈이다. 이 글은 왜 이렇게 늦었는지, 그리고 람다를 가능하게 하는 함수형 인터페이스와 @FunctionalInterface 가 실제로 무슨 일을 하는지 정리한다.
2. 핵심 내용
2-1. 왜 이렇게 늦었나
자바는 처음부터 “모든 것은 객체”라는 철학으로 설계됐다. 함수를 값처럼 전달하려면 이 철학과 충돌한다. 기존에는 익명 클래스로 흉내 냈다.
// Java 8 이전
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("실행");
}
};
// Java 8 이후
Runnable r2 = () -> System.out.println("실행");문제는 성능이었다. 익명 클래스는 매번 별도의 .class 파일과 객체를 만든다. 람다를 도입하려면 이걸 훨씬 가볍게 처리할 방법이 필요했는데, 그게 Java 7 에서 먼저 들어온 invokedynamic 바이트코드 명령어다. 람다 표현식은 컴파일 시점에 별도 클래스를 미리 만들지 않고, 실행 시점에 필요한 구현을 동적으로 연결한다. 언어 문법을 넣기 전에 JVM 바이트코드 차원의 기반 공사가 먼저 필요했던 셈이라, 다른 언어보다 늦어졌다.
2-2. 함수형 인터페이스: 람다가 담기는 그릇
람다식 자체는 타입이 없다. () -> System.out.println("실행") 이 대입될 수 있으려면 추상 메서드가 딱 하나뿐인 인터페이스(SAM, Single Abstract Method)가 필요하다. Runnable, Comparator, Callable 이 전부 이 조건을 만족해서 예전부터 람다 대상이 될 수 있었다.
@FunctionalInterface
interface Validator<T> {
boolean isValid(T value);
}
Validator<String> notEmpty = s -> !s.isEmpty();2-3. @FunctionalInterface 가 하는 일
이 애너테이션은 없어도 동작에는 지장이 없다. 하지만 붙여두면 컴파일러가 그 인터페이스에 추상 메서드가 정확히 하나인지 검사해준다. 누군가 실수로 추상 메서드를 하나 더 추가하면, 애너테이션이 없으면 그냥 컴파일되고 람다를 쓰던 모든 코드가 나중에야 깨진다. 붙여두면 그 자리에서 컴파일 에러로 잡힌다. 문서화 역할도 겸한다: “이 인터페이스는 람다 대상으로 설계됐다”는 의도를 코드로 남긴다.
2-4. java.util.function 패키지
매번 인터페이스를 직접 만들 필요 없이, 자바가 미리 정의해둔 범용 함수형 인터페이스들이 있다.
Function<String, Integer> length = String::length;
Predicate<String> isEmpty = String::isEmpty;
Consumer<String> print = System.out::println;
Supplier<String> greeting = () -> "hi";Function<T,R> 은 받아서 반환, Predicate<T> 는 받아서 boolean, Consumer<T> 는 받아서 반환 없음, Supplier<T> 는 안 받고 반환만 한다. 이 인터페이스들이 다음 글에서 다룰 스트림 API의 filter, map, forEach 파라미터 타입 그대로다.
3. 마무리
요약
- 람다는 Java 8(2014)에 도입됐는데, 이는 JVM 이
invokedynamic을 먼저 지원해야 했기 때문에 다른 언어보다 늦었다.- 람다식은 추상 메서드가 하나뿐인 함수형 인터페이스(SAM)에만 대입할 수 있다.
@FunctionalInterface는 컴파일 시점에 추상 메서드가 정확히 하나인지 검사해 실수를 막는다.Function,Predicate,Consumer,Supplier는 자바가 미리 만들어둔 범용 함수형 인터페이스다.