람다표현에서는 바깥쪽 스코프의 변수(static 변수, 지역 변수, 인스턴스 변수)를 참조 할 수 있으며 이를 capturing lambda
라고 한다.
지역 변수를 참조하는 경우에만 컴파일러는 해당 변수가 final 또는 effectively final 인지 체크하여 아니라면 컴파일 오류를 표출하게 되는데 이에 대해서 알아보고자 한다.
아래 메서드는 람다를 리턴한다. 하지만 람다표현식에서 함수의 파라미터(지역변수)를 사용하고 있으며 이를 증가시키고 있으므로 컴파일 오류를 표출한다.
final 로 선언되지 않았으며 값을 변경했으므로 effectively final(final은 붙지 않았지만 할당 후 값을 변경하지 않아 final과 같은 효과) 도 아니다.
Supplier<Integer> incrementer(int start) {
return () -> start++; // local variables referenced from a lambda expression must be final or effectively final
}
컴파일 되지 않는 기본적인 이유는 람다는 참조하는 지역변수에 대한 복사본을 가지고 동작하기 때문이다.
람다는 왜 참조하는 외부스코프 지역변수에 대해 복사본을 가지고 동작할까?
위 코드에서 incrementer 함수는 실행되면서 start를 증가시키지 않고 람다를 리턴할 뿐이다. incrementer 함수의 실행이 끝난 후의 다른 시점에서 리턴한 람다표현식이 실행되어 start 지역변수가 증가할 수 있으며, 그 시점이 오기전에 GC에 의해서 start 지역변수가 정리될 수도 있다.(함수의 실행이 끝나면 지역변수는 GC에 의해 정리대상이 된다.) 그러므로 실행 시점에 지역변수가 사라질 것을 방지하지 위해서 람다는 복사본을 생성하여 동작하게 된다.(또한 지역변수는 Stack영역에 저장되기 때문에 공유자원이 아니므로 다른 thread에서 람다가 실행된다면 변수에 접근할 수 없다.)
복사본을 가지고 미래에 동작할 예정인데 복사본에서 값이 바뀌어 버리면(= final or effective final이 아니면, = 최종상태값과 복사본값이 다르면) 오류를 발생할 여지가 생기므로 컴파일 단계에서 막는 것이다.
static 변수와 인스턴스 변수의 경우에는?
private int start = 0;
Supplier<Integer> incrementer() {
return () -> start++;
}
지역변수가 아닌 인스턴스 변수는 컴파일 에러도 나지 않으며 정상 동작한다. 인스턴스 변수는 Heap영역에 저장되고 static 변수는 Method영역에 저장되며 두 영역모두 공유자원이므로 다른 thread에서 접근이 가능하다. 자유롭게 접근이 가능하므로 실행되는 시점에 최종상태값을 읽어 증가시킬 수 있다. 그러니 변수에 대한 제약조건이 없는 것이다.
이에대한 간단한 예제 코드이다. 변수 holder가 참조하는 배열객체는 heap 영역에 저장되어 자유롭게 참조가 가능하다. 그러므로 sums.sum()로 람다가 실행되는 시점에 holder[0]의 최종상태값을 읽어들여 계산하므로 6이 리턴된다.
public int workaroundSingleThread() {
int[] holder = new int[] { 2 };
IntStream sums = IntStream
.of(1, 2, 3)
.map(val -> val + holder[0]);
holder[0] = 0;
return sums.sum();
}