[Java] Effectively Final이란?

최민재·2022년 6월 14일
3
post-thumbnail

들어가며 👋

지난번에 람다에 대하여 정리하는 글을 올렸습니다.
남다른 람다 정리 보기!!
뭐 이런 글을 하나 올렸는데 하나의 댓글이 달렸죠.

좋은 피드백 정말 감사합니다!!

피드백에 이런 생각이 들었습니다.

'아 다음에 저 내용에 대하여 정리해볼까??'

수행평가가 많다는 핑계로 지금까지 미루다가
이제 Effectively Final에 대하여 정리해보도록 하겠습니다.

Effectively Final? 🔥

혹시 자바에 Final에 대해서 아시나요??
final field 파이널 필드란
만약에 필드에 final을 붙인다면 그 필드는 더이상 수정할 수 없습니다.

public Test {
	final String name = "snack655";
    
    public changeName(String name) {
    	this.name = name;	// 오류 발생!!
    }
}

위의 코드는 요류가 나는 코드입니다.
그 이유는 당연히 final이 붙은 필드를 변경하려고 했기 때문입니다.
그렇다면 Effectively Final의 이름을 보시면 당연히 final과 관련이 있겠쥬??

자바8에서 도입된 Effectively Final이란??

변수에 final을 붙이지 않았지만,
값이 변경되지 않아 final과 유사하게 동작하는 것

이런 Effectively Final은 흔히 람다식 혹은 익명 클래스에서 자주 접할 수 있습니다.

그 이유는 익명 클래스 혹은 람다식에서는 참조하는 외부 지역 변수는 final로 선언되었거나
Effectively Final이어야 하기 때문입니다.

여기서 람다식이 무엇인지 잘 모르시겠다면
남다른 람다 정리 보기!! ㅎㅎ..

위의 말을 코드로 보신다면 더욱 이해가 잘 되실거에유!!

public void method() {
    int value = 10;
    Calc calc = new Calc() {
        @Override
        public void minus() {
            value--;	// 오류 발생!!
        }
    };
}

@FunctionalInterface
interface Calc {
    void minus();
}

위와 같은 익명 클래스를 사용한 코드가 있습니다.
만약 IntelliJ를 사용하신다면 아래와 같은 오류가 발생합니다.

final 혹은 effectively final이 필요하다고 합니다!
위의 코드를 람다식을 바꾸어 보겠습니다.

public void method() {
    int value = 10;
    Calc calc = () -> value--;	// 오류 발생!!
}

마찬가지로 똑같은 오류가 발생합니다!
간단하게 한가지 더 보자면

int value = 10;
Calc calc = () -> System.out.println(value);
value++;

이렇게 밖에서 변경하려고 해도 마찬가지로 오류가 발생합니다.

그런데 한가지 의문이 듭니다..?

public void method() {
    final int[] value = {10};
    Calc calc = () -> value[0]--;
}

위의 코드에서는 오류가 발생하지 않습니다.

"아니.. 도대체 effectively final의 조건이 정확히 뭐에유??"

그래서 조건을 한번 살펴보자면

  • 초기화 후 다시 값을 넣지 않습니다.
  • final이 붙어있지 않아야 합니다.
  • 객체의 경우 - 참조를 변경하지만 않으면 됩니다.

따라서 위와 같이
배열 객체를 참조하고 있는

final int[] value = {10};

위의 코드는 안의 값을 변경해도 상관이 없는 것입니다!

왜 이렇게 사용할까? 🧐

람다식 내부에서 외부 지역 변수를 참조할 때 final 혹은 effectively final을 사용해야하는 이유가 무엇일까유??

일단 아래와 같은 코드가 있습니다.

public void method() {
    int value = 0;
    Calc calc = () -> System.out.println(value);
}

위의 코드를 보면 람다식은 외부 지역 변수인 value를 참조하고 있습니다.
그러면 람다 내부에서 사용할 수 있도록 똑같은 복사본을 생성합니다.

이렇게 외부의 변수를 사용할 때 람다식 내부에서 사용할 수 있도록 복사복을 생성하는 것을 람다 캡쳐링(Lambda Capturing)이라고 합니다.

이렇게 복사를 하지 않고 그냥 외부의 값을 그대로 쓴다고 가정해봅시다.

public void threadTest() {
    int value = 0;
    new Thread(() -> {
        try {
            Thread.sleep(100000);
            System.out.println(value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

threadTest()를 돌리는 스레드가 있고
value를 출력하는 스레드가 있습니다.

그러면 value를 출력하는 스레드가 출력을 하기도 전에
threadTest()를 돌리는 스레드가 끝나고 말죠.
스택 영역에 할당된 지역 변수 value는 스레드가 끝나며 함께 사라집니다.
그러면 value를 참조할 수가 없는 상황이 벌어지고 맙니다.

복사한다면 이런 문제가 발생하지 않죠!!

그런데 만약에 복사된 값이 바뀐다면 복사된 값이 최신 값임을 보장할 수 있을까요?

보장할 수 없고 이것이 바로 값이 바뀌면 안되는 이유입니다!

다른거는?

자바에는 클래스 변수도 있고 인스턴스 변수도 있습니다.
마찬가지 일까요??

먼저 클래스 변수와 인스턴스 변수에 대하여 간단하게 보도록 하죠.

public class EffectivelyFinal {
    int a = 0;  // 인스턴스 변수
    static int b = 0;   // 클래스 변수
    
    public void method() {
        int value = 0; // 지역 변수
    }
}

간단하게 이것입니다.

여기서 인스턴스 변수는 힙 영역,
클래스 변수는 메서드 영역에 할당되죠!

따라서 람다식에서는 이 두 변수를 복사할 필요도 없는 것이죠!
따라서 변경되어도 상관없습니다!

그럼 코드로 살펴보도록 하죠!

public class EffectivelyFinal {
    int a = 0;  // 인스턴스 변수
    static int b = 0;   // 클래스 변수

    public void method() {
        int value = 0; // 지역 변수
        Calc calc = () -> System.out.println(a--);
        Calc calc2 = () -> System.out.println(b--);
        Calc calc2 = () -> System.out.println(value--); // 오류 발생!!
    }
}

이렇게 인스턴스 변수와 클래스 변수는 변경이 가능합니다!

마치며 👊

짧게 Effectively final에 대하여 정리해 보았습니다.
지식이 부족하여 부족한 부분도 있을거 같아 꽤 아쉽기도 합니다.
그래도 최대한 좋게 적고자 했습니다!!
혹시 수정이 필요할 거 같은 부분이 있다면 댓글로 알려주세요!

profile
Android Engineer

0개의 댓글