728x90

개요

당연하게 사용하던 Java의 람다 기능에 대해서 의문을 갖지 않고 막 써댔다.

그렇지만, 면접에서의 질문을 받았을 때 당황했다. 뜬금포로 final을 쓰고 안쓰고의 차이를 물어보셨는데 순간 답변을 하지 못했다.

그래서 정리해본다.

 

람다 캡쳐링에 대해서 알아보기 이전에 Effectively final에 대해 먼저 알아보자.

Effectively Final 이란?

해당 단어를 deepL을 통해 해석해보면 사실상 최종 이라고 해석해준다.

final을 선언한 상수와 같이 변경되지 않았다면 그와 같은 수준으로 컴파일러가 해석해준다.

 

effectively final이 되려면 아래의 3가지 조건을 만족해야 한다.

아래 3가지 조건은 공식문서를 통해서 나와있는 정보들이다.

  1. 명시적인 final을 선언하지 않았다.
  2. 재 할당을 하지 않아야 한다.
  3. 접두 또는 후미에 증감연산자를 추가해서 데이터를 바꾸지 않아야한다.

객체라면 참조 주소값만 바뀌지 않는다면 그대로 계속 effectively final 로 유지할 수 있다.

 

정상적인 Effectively Final

정상적인 lambda식

Effectively Final이 제대로 되지않은 경우

비정상적인 lambda식

자바가 친절하게 설명을 해준다.

 

자, 이제 변수값을 내부에서 변경하지 않으면 잠정 final로 보고 람다식에 데이터를 명확하게 넣어줄 수 있다.

Lambda Capturing에 대해 알아볼 시간이다!

Lambda Capturing이란?

외부에서 정의한 변수를 사용할 때 람다식(익명 클래스의 function)에서 복사본을 생성하게 된다.

외부라는 의미는 지역변수나 전역변수(인스턴스)와 클래스 변수들을 전부 아우르는 표현이다.

그럼 Capturing을 적용하지 않는 경우도 있을거 아닌가?

당연히 사용하지 않을 수 있다. 그래서 변수를 넣지 않고 동작할 수 있는데 이때는 외부의 변수를 주입받아 사용하는게 아니라서 캡쳐링이

적용되지 않아서 non-capturing이라고 한다.

Lambda Capturing은 왜 복사본을 만드는가?

일단 지역변수는 메모리 구조상 스택 영역에 할당된다.

스택은 스레드가 실행됐을 때 고유한 영역으로 가지고 있게된다. 그래서 스레드끼리는 공유할 수 없고, 스레드가 종료되면 해당 스택 영역도 사라진다.

여기서 이제 문제가 발생하는 것이다.

 

아래 코드에서 ss 라는 문자열을 복사해서 갖고있지 않는다면 new Thread 부분에선 지역변수로 묶여있는 test()가 스레드보다 더 빨리 수행되고 끝날 가능성이 존재하기 때문에 null을 줄 수도 있을 것이다. 이렇기 때문에 복사본을 만들어 유지하는 것이다.

public void test() {
    String ss = "test";

    new Thread(() -> {
        try {
            Thread.sleep(1000L);
            System.out.println("thread1 ss : " + ss);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }).start();

    System.out.println("ss : " + ss);
}

 

여기서 근데 effectively final이어야 하는 이유는 위에서 봤듯이 멀티 스레드 환경에서 람다식이 동작할 수 있기 때문에

지역변수는 또 스레드마다 공유하지 않기도 한다. 때문에 어떤 복사본이 최신인지를 자바 입장에서는 확인할 방법이 없기 때문에 final변수로만 지역변수를 사용해야 하는것이다.

조용하던 인스턴스 변수나 클래스 변수는?

인스턴스 변수는 메모리 구조상 힙에 할당된다.

우리는 알고있다. 힙은? -> 모든 스레드가 공유할 수 있는 메모리이다.

클래스변수는 static 변수가 이 부분에 포함되는데, 메소드 영역에 할당이 된다.

그래서 값이 바뀌던 말던 그 데이터는 항상 같게끔 유지할 수 있기 때문에 할당할 수 있는 것이다.

정리

람다식 내부에서 지역변수를 사용하는 경우 final이나 effectively final 변수를 사용해야 한다.

-> 이유는 메모리 구조의 stack 영역에 저장되기 때문

final이 아니라면 복사되는 값이 어떤 스레드에서 바꾼것이 가장 최신의 복사본인지 알 수 있는 방도가 전혀 없다.

 

728x90

'Java' 카테고리의 다른 글

참조 유형  (0) 2022.09.12
Checked Exception, Unchecked Exception  (0) 2022.09.07
변성  (0) 2022.08.11
일급 컬렉션  (0) 2022.08.09

+ Recent posts