[JAVA] 람다와 스트림에서 외부 변수 다룰 때 주의사항

세하·2025년 4월 26일

JAVA

목록 보기
10/17

외부 변수를 사용할 때 주의사항

자바 람다식 내부에서는 외부 지역 변수를 사용할 수 있다!
그러나 그 변수는 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문 안에서 외부 변수 캡쳐할 때 주의사항

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라는 새로운 변수를 만들어서 캡쳐했기 때문에 원하는 값이 출력된다.

스트림(Stream)에서도 주의해야 한다

스트림에서도 마찬가지로 람다 내부에서 외부 변수를 사용할 때 똑같은 규칙이 적용된다.

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

0개의 댓글