[JAVA] lambda에서 지역변수가 fianl이어야 하는 이유

2022. 1. 1. 23:42JAVA

728x90

lambda의 특징

  1. 람다식에서 사용되는 외부 지역 변수는 복사본이다.
  2. 람다식에서 사용하는 변수가 외부 지역변수일 경우

final 혹은 effectively final 인 경우에만 접근할 수 있다.

### effectively final

> *A non-final local variable or method parameter whose value is never changed after initialization is known as effectively final.*
> 

Java 8 에 추가된 syntactic sugar 일종으로, **초기화 된 이후 값이 한번도 변경되지 않았다면** effectively final 이라고 할 수 있다.
  1. 복사된 지역 변수 값은 람다식 내부에서도 변경할 수 없다.

위의 특징이 생기는 이유

1. 람다식에서 사용되는 외부 지역 변수는 복사본이다.

  • 지역 변수는 스택 영역에 생성됨. 따라서 지역 변수가 선언된 block이 끝나면 스택에서 제거됨.

    ⇒ 추후에 람다식이 수행될 때 참조할 수 없음.

  • 지역변수를 관리하는 Thread와 람다식이 실행되는 Thread가 다를 수 있음.

    ⇒ 스택은 Thread 고유의 공간이고 Thread끼리 공유되지 않음.

위와 같은 이유로 람다식에서는 외부 지역 변수를 직접 참조하지 않고 복사본을 전달받아 사용함.

2. 람다식에서 사용하는 변수가 외부 지역변수일 경우

final 혹은 effectively final 인 경우에만 접근할 수 있다.

  • 참조하고자 하는 지역 변수가 final 혹은 effectively final 인 경우.

  • 즉 변경이 가능한 경우*

       public void executelocalVariableInMultiThread() {
           boolean shouldRun = true;
           executor.execute(() -> {
               while (shouldRun) {
                   // do operation
               }
           });
    
           shouldRun = false;
       }

    람다식이 어떤 Thread에서 수행될 지는 미리 알 수 없음.

    ⇒ 외부 지역 변수를 다루는 Thread와 람다식이 수행되는 Thread가 다를 수 있음.

    위의 코드에서 shouldRun을 제어하는 Thread가 A이고
    람다식을 수행하는 Thread가 B 라고 가정한다면.

    B가 수행될때의 shouldRun 값이 A에서 가장 최신 값으로 복사되어 전달됐는지 확신할 수 없다.

    ⇒ shouldRun은 변경이 가능한 지역변수이고, 앞에서 말했듯이 Thread별로 스택이 다르므로 Thread간 지역변수를 공유할수도, sync 해줄수도 없음.

    즉. 값이 보장되지 않는다면 매번 다른 결과가 도출될 수 있음.

    이러한 이유로 인해 외부 지역 변수는 전달되는 복사본이 변경되지 않은 최신 값 임을 보장하기 위해 final 혹은 effectively final 이어야 함.

3. 복사된 지역 변수 값은 람다식 내부에서 변경할 수 없다.

위에서 말했듯이

복사된 값이 final 이라면 당연히 변경할 수 없고,

effectively final 이라면 Thread끼리 sync해줄 수 없으므로 동시성을 가질 수 없어 변경 불가.

지역 변수는 안되고 필드(인스턴스 변수, 클래스 변수)는 가능한 이유.

필드는 Heap 영역 혹은 메서드 영역에 선언이 되어 Thread별로 모두 접근이 가능하기때문에 람다식에서 바로바로 접근이 되어 복사 과정이 불필요하고 참조시 최신 값임을 보장할 수 있음.

또한 값이 메모리에서 바로 회수되지 않기때문에 람다 실행시 값이 존재하는것을 보장할 수 있음.

다만 멀티 스레드 환경에서는 sync를 맞춰주는 작업이 필요할 수 있음.


effectively final를 응용해 지역변수를 람다식 내부에서 사용하는 법.

effecively final을 보장하기 위해 초기화 한 이후 값을 변경시키지 않으려면 배열이나 객체를 사용하면 됨.

⇒ 배열 내부나 객체 내부의 값이 변경된다고 해도 주소값은 변경되지 않아서 가능함.

public static void main(String[] args){
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);

    int[] result = {0};
    list.stream()
    .forEach(v->{
        result[0] += v;
    });
    System.out.println(result[0]); // output : 6
}
728x90