1. 람다식은 외부 block 에 있는 변수에 접근할 수 있다.
  2. 외부에 있는 변수가 지역 변수 일 경우 final 혹은 effectively final 인 경우에만 접근이 가능하다.

Java에서는 위의 두 가지 특성이 존재한다.

람다식에서는 1번 특성으로 외부 변수의 사용 여부에 따라

Capturing lambda와 Non-Capturing lambda로 나눌 수 있다.

Capturing Lambda

외부 변수를 이용하는 람다식을 의미한다. 외부 변수로는 클래스 변수, 인스턴스 변수, 지역 변수로 나눌 수 있다.

public class LambdaCapturing{
	int num1 = 10;
    static int num2 = 20;

    public void method(){
    	final int num3 = 30;
        int num4 = 40;

        //인스턴스 변수
        Runnable r1 = () -> {
        	System.out.println(num1);
        };

        //클래스 변수
        Runnable r2 = () -> {
        	System.out.println(num2);
        };

        //final 지역 변수
        Runnable r3 = () -> {
        	System.out.println(num3);
        };

        //effectively final 지역 변수
        Runnable r4 = () -> {
        	System.out.println(num4);
        };
    }
}

Non - Capturing Lambda

외부 변수를 사용하지 않는 람다식이다.

public class NonCapturingLambda(){
	public void method(){
    	Runnable r1 = () -> {
        	System.out.println("Non-Capturing");
        };

        Runnable r2 = () -> {
        	String mes = "Non-Capturing";
            System.out.println(mes);
        };
    }
}

Capturing lambda는 외부 변수가 지역 변수인지에 따라서

Local Capturing lambda와 Non-Local Capturing lambda로 나뉜다.

Local Capturing Lambda

public class LambdaCapturing{

    public void method(){
    	final int num3 = 30;
        int num4 = 40;

        //final 지역 변수
        Runnable r3 = () -> {
        	System.out.println(num3);
        };

        //effectively final 지역 변수
        Runnable r4 = () -> {
        	System.out.println(num4);
        };
    }
}

“외부에 있는 변수가 지역 변수 일 경우 final 혹은 effectively final 인 경우에만 접근이 가능하다.” 라는 두 번째 특성은 “Local Capturing lamda의 외부 지역 변수는 final 특성을 가져야한다” 와 같다.

  • 이때 final 특성이란 무엇일까?

final 변수면 final이지 왜 final 특성을 가진 경우라고 말할까?final 특성은 위에서 볼 수 있 듯이 final과 effectively를 통틀어 말하는 경우이다. 딱히 final이라고 명시해주지는 않았지만 초기화 된 이후 값이 한 번도 변경되지 않았다면 effectively final라고 할 수 있다. 값이 변경되는 순간 effectively final의 특성을 잃어버린다고 할 수 있다.

따라서 위에서 num4는 final이라고 명시되지는 않았지만 값이 변경되지 않기 때문에 effectively final로 lambda식에서 접근이 가능하다. 만약. 초기화 된 이후 한번이라도 값이 변경된다면 lambda식에서는 접근이 불가능하다.

Non - Local Capturing Lambda

public class LambdaCapturing{
	int num1 = 10;
    static int num2 = 20;

    public void method(){

        //인스턴스 변수
        Runnable r1 = () -> {
        	System.out.println(num1);
        };

        //클래스 변수
        Runnable r2 = () -> {
        	System.out.println(num2);
        };
    }
}

Non-Local Capturing Lambda는 외부 지역 변수가 아닌 인스턴스 변수와 클래스 변수에 접근하는 람다식이다. 클래스 변수와 인스턴스 변수는 따로 final 특성이 필요없다.

이를 이해하기 위해서는 먼저 Java에서 변수 저장의 메모리 구조가 필요하다.

JVM에서는 지역 변수는 메서드 내에서 선언되어 메서드 내에서만 사용할 수 있는 변수로, 메서드가 실행될 때 메모리를 할당받아 메서드가 종료되면 소멸된다. 이때, 스택이라는 영역에 생성되며, 스레드들은 각각의 별도의 스택을 가지고 있다. 따라서 어떤 스택에 저장된 지역 변수는 다른 스택을 사용하는 스레드에서는 공유가 불가능하다.

클래스 변수는 해당 클래스의 모든 인스턴스가 공통으로 가지는 값으로 인스턴스 없이도 접근할 수 있다. 클래스가 로딩될 때 한번 메모리에 올라가며 종료될 때까지 유지되며 메소드 영역에 생성된다.

인스턴스 변수는 인스턴스가 생성될 때 생성되며, 힙 영역에 올라간다.

이때 차이점은 지역 변수는 다른 스택에서 참조가 불가능하며, 인스턴스 변수와 클래스 변수는 각각 힙과 메소드 영역이기에 다른 스택에서 참조할 수 있다는 점이다그래서 인스턴스 변수와 클래스 변수는 다른 제약없이 람다식에서 접근 가능하다. 이때 멀티 스레드일 경우 synchronized를 이용하여 sync만 맞춰주면 된다. 이제 지역 변수만을 살펴보자.

두 번째로, 람다식의 경우 별도의 스레드에서 실행이 가능하므로 원래 지역 변수가 있는 스레드가 사라지고 람다가 실행 중인 스레드가 살아있을 경우에 문제가 발생할 수 있다. 또한 같은 스레드라도 지역 변수가 선언된 블록이 끝나면 변수는 스택에서 제거되는데, 메서드 내에서 지역변수를 참조하는 람다식을 리턴하는 메서드가 있을 경우 메서드 블록이 끝나면 지역 변수가 스택에서 제거되므로 추후에 람다식이 수행될때 참조할 수 없다.

두 가지 이유로 람다식은 외부 지역 변수의 경우에 “복사본”을 사용한다.

람다식이 복사본을 사용하면 변수의 값이 변경될 때, 어느 시점에서 복사되었는지 불명확하기 때문에 최신값으로 복사되었는지 알. 수 없다. 따라서 지역 변수를 스레드간에 sync를 맞춰줄 수 없다. 따라서 복사한 값의 신뢰도를 지켜주기 위해서 final 특성의 제약이 있다.

마지막으로 컴파일된 람다식은 static 메소드 형태로 변경되는데, 이때 복사된 값이 파라미터로 전달되고 스택 영역에 생성되므로 원래 값과 sync를 맞춰 줄 수 없다. 람다식내에서 복사된 값을 변경시키고 다시 그 값을 전달해 줄 수 없다는 뜻이다. 따라서 람다식 내부에서도 값이 변경되어서는 안된다.

그래서 람다식에서 사용하는 지역 변수들은 final 이거나 effectively final로 final 특성을 가져야 한다.

참조

https://jeong-pro.tistory.com/211

https://perfectacle.github.io/2019/06/30/java-8-lambda-capturing/

https://vagabond95.me/posts/lambda-with-final/

https://velog.io/@woo00oo/변수의-종류와-메모리-구조

업데이트:

댓글남기기