람다(Lambda)의 바디에서는 파라미터가 아닌 바디 외부에 있는 변수를 참조할 수 있다.
유사하게 로컬 클래스, 익명 클래스에서도 참조가 가능하다.
public class VariableCapture {
private void run() {
// 로컬 클래스, 익명 클래스, 람다에서 이 변수를 참조하면 effective final로 변경
int baseNumber = 10;
// 람다
IntConsumer lambda = (i) -> System.out.println(i + baseNumber); // i + 10
// 로컬 클래스
class LocalClass {
void printBaseNumber() {
System.out.println(baseNumber); // 10
}
}
// 익명 클래스
IntConsumer intConsumer = new IntConsumer() {
@Override
public void accept(int i) {
System.out.println(i + baseNumber); // i + 10
}
};
}
}
람다 시그니처의 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수를 자유 변수라고 한다.
또 람다 바디에서 자유 변수를 참조하는 것을 람다 캡쳐링(Lambda Capturing)이라고 한다.
지역 변수를 람다 캡쳐링 할 때 캡쳐링 당하는 자유 변수는 두 가지 제약 조건이 존제한다.
final
로 선언되어 있어야 한다. (java 8 이전)final
로 선언되지 않은 자유 변수는 final처럼 동작해야 한다. (effectively final)
위 코드에서 확인해보자.
람다에서 변수를 참조하는데 baseNumber
에 final
선언을 하지 않았기 때문에 baseNumber
는 effectively final이다. 그렇기 때문에 baseNumber
에 수정을 가하면 오류가 발생한다.
먼저 JVM의 메모리 구조를 살펴보자.
지역 변수는 JVM 영역 중 stack 영역에 생성된다. 그리고 쓰레드별로 이 stack영역이 별도로 생성된다. 즉, 지역 변수는 쓰레드끼리 공유가 안된다. 반면 인스턴스 변수는 힙 영역에 생성된다. 따라서 인스턴스 변수는 쓰레드끼리 공유가 가능하다.
람다는 별도의 쓰레드에서 실행이 가능하다. 따라서 지역 변수(자유 변수)가 있는 쓰레드가 사라졌을 때, 람다가 이 변수를 참조하고 있다면 오류가 날 것이다. 하지만 위에서 본 코드처럼 람다에서 자유 변수 참조가 가능하다.
어떻게 이럴 수 있을까?
이는 람다 (또는 로컬 클래스, 익명 클래스)가 자유 변수를 참조할 때 직접 그 변수를 참조하는 것이 아니라 자유 변수를 자신의 stack에 복사하여 참조하기 때문이다. 이를 variable capture (또는 lambda capturing)라고 한다.
때문에 variable capture가 될 자유 변수는 수정이 불가하도록 final
이거나 final
처럼 동작해야 한다. 자바 8 이전에는 이런 이유로 final
이 아닌 변수는 익명/로컬 클래스, 람다에서 참조를 하지 못했는데 자바 8 이후로 final
을 붙이지 않아도 effectively final로 선언이 된다.
위처럼 자유 변수를 참조할 수 있다는 점은 람다, 로컬 클래스, 익명 클래스 모두 공통점이지만 차이점도 존재한다. 바로 scope이다.
람다의 scope는 자유 변수의 scope와 같기 때문에 람다에서 자유 변수와 같은 이름의 변수를 생성하면 에러를 뱉어낸다.
반면 로컬 클래스, 익명 클래스의 내부에 자유 변수와 같은 이름의 변수를 선언하는 것은 가능하다. 자유 변수보다 로컬 클래스, 익명 클래스 내부에 생성된 변수의 스코프가 더 지엽적이기 때문에 shadowing 된다.