Variable used in lambda expression should be final or effectively final
아래와 같이 java 코드를 작성하면 intellij에서 오류를 뱉으며 컴파일되지 않는다.
int number = 1234;
number = 2;
Runnable r = () -> System.out.println(number);
결론부터 말하면, 람다에서는 파라메터가 아닌 외부 변수(free variable, 자유변수)를 사용 할 수 있다. 단, 지역변수는 final이거나 final처럼 사용되어야한다.(effectively final)
그 이유를 알아보자.
변수
변수에는 클래스변수, 인스턴스 변수, 지역변수가 있다.
public class test {
int iv; // 인스턴스 변수
static int cv; // 클래스 변수
void method() {
int lv; // 지역 변수
}
}
인스턴스변수는 인스턴스가 생성될 때 생성되고,
클래스 변수는 클래스의 변수이므로, 모든 인스턴스가 공유하게 되고,
지역 변수는 메소드 등 지역 내에서만 사용할 수 있다.
자유변수란, 람다식 외부에서 정의된 지역변수를 람다식 내부에서 참조하는 변수를 의미한다.
람다 캡처링
람다 캡처링(capturing lambda)이란 파라미터로 넘겨받은 데이터가 아닌 람다식 외부에서 정의된 변수를 참조하는 변수를 람다식 내부에 저장하고 사용하는 동작을 의미한다.
int number = 1234;
Runnable r = () -> System.out.println(number);
이 때 제약사항이 존재한다. 지역변수인 자유변수는 final이거나 effectively final인 것이다.
왜일까?
이는 JVM 메모리 구조와 관련이 있다. JVM 메모리상으로 힙이나 메서드 영역에 저장되는 인스턴스나 클래스 변수 등과는 달리, 지역변수는 스택에 저장되기 때문이다.
예를 들어 '지역변수의 값을 캡처하는 람다'를 반환하는 메서드가 있다고 한 번 생각해보자.
해당 메서드의 실행이 종료되었을 때 해당 메서드에서 선언되고 사용된 모든 지역변수의 할당은 해제된다.
그럼에도 불구하고 람다는 더 이상 메모리에 존재하지 않는 지역변수의 값을 아무런 문제 없이 참조하여 사용할 수 있다.
람다에서지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되엇는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다.
람다 내부에서 지역변수가 사용될 때 원본 지역변수를 복제해오기 때문이다. 따라서 데이터가 그대로 유지되는 것이며, 복제된 값이 변하지 않게 하기 위해 한번만 할당 가능하다는 제약이 생긴 것이다.
참고
한편, 클래스에 static으로 선언된 필드나 인스턴스 필드의 경우 람다 내부에서 접근하는 데에 아무런 제약이 존재하지 않는다. 람다에서 캡처되는 필드의 경우 값이 자유자재로 변해도 되며, 람다는 자신이 실행될 때 해당 시점의 필드 값을 사용하게 된다. 이는 이들이 JVM 메모리상으로 힙 영역 혹은 메서드(데이터) 영역에 저장되어 있으며, 람다식 내부에서는 클래스 혹은 인스턴스를 통해 해당 데이터에 접근하기 때문이다. 이는 필드의 값이 객체든, 원시값이든 동일하게 적용된다.
class lambdaCaptureTest {
private int i = 0;
void capturing() {
Supplier<Integer> r = () -> this.i;
this.i = 100;
System.out.println(r.get()); //100; //컴파일 에러 발생하지 않는다.
}
}
'java' 카테고리의 다른 글
[java] java 17의 새로운 점들! (0) | 2024.01.11 |
---|---|
[java] lambda를 이용해 callback 구현하기 (0) | 2023.07.28 |
[java] 익명클래스 (0) | 2023.04.05 |
[java] 코드 실행시간 측정하기 (0) | 2023.03.31 |
[java] List, Map에서 유용하지만 생소한 함수 정리 (0) | 2023.03.24 |