1. 소개
간단히 말해: 람다 표현식은 별도의 클래스나 익명 클래스를 선언하지 않고도 “즉석에서” 함수형 인터페이스의 구현을 빠르게 만드는 방법입니다. 이름이 없는 미니 메서드처럼 인수로 전달하거나 변수에 저장할 수 있습니다.
오랫동안 Java는 “전통적인” OOP 언어였습니다. 버튼을 눌렀을 때 무엇을 할지 같은 동작을 전달하려면 익명 클래스를 작성해야 했습니다:
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick() {
System.out.println("버튼이 눌렸습니다!");
}
});
Java 8부터 람다의 마법이 등장했습니다:
button.setOnClickListener(() -> System.out.println("버튼이 눌렸습니다!"));
여기서 () -> System.out.println("버튼이 눌렸습니다!")가 바로 람다 표현식입니다.
형식적으로 말하면: 람다 표현식은 함수형 인터페이스의 단 하나의 추상 메서드 구현을 간결하게 표현한 형태입니다.
왜 중요할까요?
- 동작을 값처럼 전달할 수 있습니다(예: 리스트의 각 요소를 어떻게 처리할지).
- 간결하고 읽기 쉬운 코드를 작성할 수 있습니다.
- 현대적인 Java API를 사용할 수 있습니다: Stream API, 이벤트 처리, 비동기 작업 등.
2. 람다 표현식 문법
일반 형태
(매개변수) -> 식
// 또는
(매개변수) -> { 코드 블록 }
여러 인터페이스에 대한 예
1. 매개변수 없음 (예: Runnable):
Runnable r = () -> System.out.println("안녕, 람다!");
r.run();
2. 매개변수 하나 (예: Consumer):
Consumer<String> printer = s -> System.out.println("전달된 값: " + s);
printer.accept("Java");
매개변수가 하나이고 그 타입을 추론할 수 있다면 괄호를 생략할 수 있습니다.
String은 우리가 전달하는 변수(s)의 타입입니다.
3. 여러 매개변수 (예: Comparator):
Comparator<Integer> cmp = (a, b) -> a - b;
System.out.println(cmp.compare(10, 5)); // 5
Integer는 우리가 전달하는 변수(a, b)의 타입입니다.
4. 여러 줄 본문(명시적 반환이 있으면 중괄호와 return이 필요):
Function<Integer, Integer> square = x -> {
int result = x * x;
return result;
};
System.out.println(square.apply(6)); // 36
첫 번째 Integer는 함수의 반환 타입이고, 두 번째 Integer는 매개변수 타입입니다.
축약과 간결함
- 본문이 단일 표현식이라면 중괄호와 return을 생략할 수 있습니다.
- 매개변수가 없으면 빈 괄호를 씁니다: () -> ...
- 매개변수가 하나면 괄호를 생략할 수 있습니다: x -> ...
- 둘 이상이면 괄호가 필요합니다: (a, b) -> ...
요약 표
같은 아이디어를 람다와 익명 클래스로 각각 어떻게 표현하는지 살펴봅시다:
| 인터페이스 | 람다 표현식(예) | 익명 클래스와 동등한 코드 |
|---|---|---|
|
|
|
|
|
|
|
|
|
사용자 정의 인터페이스 예
다음과 같은 함수형 인터페이스가 있다고 합시다:
@FunctionalInterface
interface Operation {
int apply(int a, int b);
}
예전에는 이렇게 구현했습니다:
Operation sum = new Operation() {
@Override
public void apply(int a, int b) {
return a + b;
}
};
이제는 더 모던하게:
Operation sum = (a, b) -> a + b;
System.out.println(sum.apply(3, 5)); // 8
표준 인터페이스 예
Runnable:
Runnable hello = () -> System.out.println("Hello from thread!");
new Thread(hello).start();
Comparator:
List<String> list = Arrays.asList("사과", "바나나", "키위");
list.sort((a, b) -> a.length() - b.length());
System.out.println(list);
Function:
Function<String, Integer> parse = s -> Integer.parseInt(s);
System.out.println(parse.apply("123")); // 123
3. 스코프와 변수 캡처
외부 컨텍스트의 변수 (effectively final)
람다 표현식은 둘러싼 메서드의 변수를 사용할 수 있습니다. 하지만 규칙이 있습니다: 그런 변수는 effectively final이어야 합니다 — 즉 final로 명시적으로 선언했거나, 초기화 이후 변경되지 않아야 합니다.
예:
String prefix = "결과: ";
Function<Integer, String> f = x -> prefix + (x * 2);
// 여기서 prefix는 “고정”됩니다 — 이후에는 변경할 수 없습니다.
System.out.println(f.apply(5)); // 결과: 10
람다에서 사용된 후 prefix를 변경하려고 하면 컴파일 오류가 발생합니다.
왜 그럴까요?
람다 표현식은 변수가 선언된 메서드가 종료된 뒤에 호출될 수도 있습니다. “마법 같은” 버그를 피하기 위해 Java는 변경되지 않는 변수만 사용하도록 제한합니다.
익명 클래스와의 차이
익명 클래스에서도 동일한 규칙이 적용됩니다: 외부 메서드의 변수는 final/effectively final이어야 합니다. 하지만 스코프에는 차이가 있습니다. 익명 클래스 내부에서 this는 익명 클래스 자신을 가리키고, 람다에서는 외부 클래스를 가리킵니다.
public class Demo {
public void test() {
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println(this); // 출력: Demo$1 (익명 클래스)
}
};
Runnable r2 = () -> System.out.println(this); // 출력: Demo (외부 클래스)
r1.run();
r2.run();
}
}
람다와 클래스 필드
람다 표현식은 외부 클래스의 필드에 제한 없이 접근할 수 있습니다:
public class Counter {
private int base = 10;
public void printSum(int x) {
Function<Integer, Integer> sum = y -> base + y + x;
System.out.println(sum.apply(5));
}
}
여기서 base는 클래스 필드이므로 변경할 수 있습니다.
x는 effectively final이어야 합니다.
4. 실습: 몇 가지 람다 표현식을 작성해 봅시다
예: 숫자 리스트 필터링
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);
nums.stream()
.filter(n -> n % 2 == 0)
.forEach(n -> System.out.println("짝수: " + n));
Stream API에 대해서는 30단계에서 더 자세히 배우게 됩니다 :P
예: 문자열 변환 함수
Function<String, String> capitalize = s -> s.toUpperCase();
System.out.println(capitalize.apply("java")); // JAVA
예: 사용자 정의 함수형 인터페이스
@FunctionalInterface
interface StringTransformer {
String transform(String s);
}
StringTransformer exclaim = s -> s + "!";
System.out.println(exclaim.transform("안녕")); // 안녕!
예: 외부 컨텍스트의 변수 사용
int factor = 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n * factor));
// factor는 이 이후에 변경할 수 없습니다!
5. 람다 표현식 사용 시 흔한 실수
오류 1: 람다가 캡처한 변수를 변경하려는 경우.
이 코드는 컴파일되지 않습니다 — 변수는 effectively final이어야 합니다:
int sum = 0;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> sum += n); // 오류: sum은 final이 아닙니다!
값을 누적해야 한다면 배열이나 래퍼 객체를 사용하세요.
오류 2: this의 스코프 혼동.
람다 표현식 내부에서 this는 람다가 아니라 외부 클래스를 가리킵니다(익명 클래스와 대조적).
오류 3: 여러 줄 람다에서 중괄호와 return을 빠뜨림.
람다 본문이 단일 표현식이 아니라면 중괄호와 return이 필요합니다:
Function<Integer, Integer> square = x -> {
int y = x * x;
return y;
};
오류 4: 람다 표현식의 타입을 잘못 지정.
람다 표현식은 항상 함수형 인터페이스를 구현합니다. 다음처럼 쓸 수는 없습니다:
var f = x -> x + 1; // 오류! 어떤 인터페이스 타입인지 알 수 없습니다.
타입을 명시해야 합니다:
Function<Integer, Integer> f = x -> x + 1;
GO TO FULL VERSION