자바 람다식 내부에서는 외부 지역 변수를 사용할 수 있다!
그러나 그 변수는 final 또는 effectively final이어야 한다.
final: 명시적으로 final 선언된 변수
effectively final : 한 번 초기화 된 이후로 절대 수정되지 않는 변수(값이 변하지 않으면 final처럼 간주되는 변수)
int num = 10;
Runnable r = () -> System.out.println(num); // OK (num은 수정되지 않음)
r.run();
그러나 아래와같이 변수의 값을 변경하려하면 컴파일 에러가 발생
int num = 10;
Runnable r = () -> {
num++; // ❌ 컴파일 에러: 변수 num은 final 또는 effectively final이어야 함
};
배열을 사용한다
배열은 "참조"를 캡처하기 때문에 내부 값 변경이 가능함.
final int[] num = {10};
Runnable r = () -> num[0]++;
r.run();
System.out.println(num[0]); // 출력: 11
캡처(Capture)
람다식(혹은 익명 클래스)이 만들어질 때, 외부에 있던 변수의 값을 기억해두거나, 참조를 붙잡아두는 것을 말한다.int x = 10; Runnable r = () -> System.out.println(x);여기서 x는 Runnable 안에서 사용되고 있음. 그래서 람다가 만들어질 때 x를 "캡처" 한다.
그런데 x는 final이거나 effectively final이어야 함.(x가 변하지 않아야 한다는 뜻)
왜냐면 람다가 만들어진 시점 이후에도 x를 계속 참조할 수 있어야 하고, 값이 변하면 일관성이 깨질 수 있으니까.
그런데 배열은 왜 괜찮을까?
final int[] arr = new int[1]; arr[0] = 10; Runnable r = () -> System.out.println(arr[0]);arr는 final이다. (배열 자체를 가리키는 참조가 final)
하지만 배열 안의 내용(arr[0])은 변할 수 있음.
즉, 람다는 arr라는 "참조"를 캡처해놓고, 그 안에 있는 값(arr[0])은 마음껏 바꿀 수 있다는 뜻.
| 캡처 대상 | 설명 |
|---|---|
| 기본형 변수 (int, String 등) | 값(value)을 복사하거나 기억함. 변경 불가 (final) |
| 참조형 변수 (배열, 객체 등) | 참조(reference)를 캡처함. 객체 내용은 변경 가능 |
👉 캡처는 "값을 복사"하거나 "참조를 기억"하는 행동을 말하는 것!
for문에서 i를 직접 람다 안에서 사용하면 예상치 못한 결과가 나온다.
List<Runnable> runners = new ArrayList<>();
for (int i = 0; i < 3; i++) {
runners.add(() -> System.out.print(i)); // ❌
}
for (Runnable r : runners) {
r.run();
}
예상되는 결과값은 012 겠지만 실제로는 333이 출력된다.
i는 for문이 끝난 후 3이 되어버리고, 모든 람다가 i라는 동일한 변수를 참조하기 때문이다.
새로운 final 변수를 복사하면 된다.
List<Runnable> runners = new ArrayList<>();
for (int i = 0; i < 3; i++) {
final int copy = i;
runners.add(() -> System.out.print(copy));
}
for (Runnable r : runners) {
r.run();
}
출력결과 : 012
for문을 돌 때마다 copy라는 새로운 변수를 만들어서 캡쳐했기 때문에 원하는 값이 출력된다.
스트림에서도 마찬가지로 람다 내부에서 외부 변수를 사용할 때 똑같은 규칙이 적용된다.
int sum = 0;
List<Integer> list = Arrays.asList(1, 2, 3);
// ❌ 컴파일 에러
list.forEach(x -> sum += x);
sum은 effectively final이 아니기 때문에 컴파일 에러가 발생한다.
따라서 이때도 배열을 사용하여 해결할 수 있다.
int[] sum = {0};
List<Integer> list = Arrays.asList(1, 2, 3);
list.forEach(x -> sum[0] += x);
System.out.println(sum[0]); // 출력: 6
또는 Stream.reduce() 같은 메서드를 사용하는것도 하나의 방법
List<Integer> list = Arrays.asList(1, 2, 3);
int sum = list.stream()
.reduce(0, Integer::sum);
System.out.println(sum); // 출력: 6