back-end-study / effective-java

🔥 이펙티브 자바 스터디
42 stars 4 forks source link

[item42] 람다에서의 this 키워드는 바깥 인스턴스를 가르키는 이유 #65

Open jun108059 opened 1 year ago

jun108059 commented 1 year ago

https://github.com/back-end-study/effective-java/pull/61#discussion_r1016694822

258p
람다에서의 this 키워드는 바깥 인스턴스를 가르킨다.
반면 익명 클래스에서의 this는 익명 클래스의 인스턴스 자신을 가르킨다.

람다도 익명 객체인 것 같은데 this 키워드가 바깥 인스턴스를 가르키는지 이해가 안되네요ㅠㅠ

혹시 힌트가 있을까 하고 컴파일된 클래스 파일을 열어봤는데 궁금증 해결이 안됐습니다..

image
gnoyes-mik commented 1 year ago

람다에서 this 키워드가 바깥 인스턴스를 가르키는 이유는 자바 스펙을 참고해보면 아래와 같아요

Practically speaking, it is unusual for a lambda expression to need to talk about itself (either to call itself recursively or to invoke its other methods), while it is more common to want to use names to refer to things in the enclosing class that would otherwise be shadowed (this, toString()). If it is necessary for a lambda expression to refer to itself (as if via this), a method reference or an anonymous inner class should be used instead. (JLS, 15.27.2)

람다식에서 자신에 자신을 참조할 필요가 있는 일은 흔치 않은 일이다 (재귀적으로 자신을 호출하거나 다른 메소드를 호출하는 것) 이름을 사용하여 둘러싸는 클래스의 사물을 참조하는 것이 더 일반적이다 람다식이 자신을 참조할 필요가 있는 경우('this'처럼), 람다 대신 메서드 참조 또는 익명 내부 클래스를 사용해야 한다

okeydokey commented 1 year ago

저도 람다가 어떤 메커니즘으로 동작하는지 모르지만 ㅠ 익명 클래스

public class Test {
    private final int value = 100;
    public LambdaTest test = new LambdaTest() {
        final int value = 200;
        @Override
        public String getValue() {
            return "value는? " + this.value;
        }
    };

    public static void main(String[] args) {
        Test test = new Test();
        System.out.println(test.test.getValue());
    }
}

람다

public class Test2 {
    private final int value = 100;
    public LambdaTest test = () -> {
        final int value = 200;
        return "value는? " + this.value;
    };

    public static void main(String[] args) {
        Test2 test = new Test2();
        System.out.println(test.test.getValue());
    }
}

최대한 변수 등을 동일하게 해서 바이트 코드를 비교해봤을때 익명 클래스와 람다는 다르게 해석되는거 같습니다. 람다 -> 익명 클래스 보다는 세용님이 공유해주신 의도에 따라 다르게 구현된것으로 보입니다. 바이트 코드 비교는 아래 링크를 참고해주세요! https://www.diffchecker.com/RYHssXE9

youngreal commented 1 year ago

image

근본적인 원인

  1. 익명 객체를 생성하게될경우 new를 사용해 heap에 객체가 생성됩니다

  2. 위에서 생성된 객체(anonymous)는 해당객체를 감싸고 있는 멤버메서드 (print()) 의 실행이 끝난후에도 heap영역에 존재하여 사용할수 있습니다

  3. 하지만 print()메서드 내부에 있는 지역변수, 매개변수는 stack영역에 할당되어 메소드 실행이 끝나면 참조할 수 없습니다.

  4. 따라서 메소드 내부에 생성된 익명객체가 자신을 감싸고있는 메서드(print)의 매개변수, 지역변수를 사용하려할때 문제가 생길수 있는 상황이 있습니다.

객체 내부로 값을복사

이 문제를 해결하기위해 자바는 메서드의 매개변수, 지역변수를 멤버메서드 내부에서 생성한 객체가 사용할경우
컴파일시점에 객체 내부로 값을 복사해 사용합니다

이때, 변수에 final키워드가 붙거나 사실상 final(effectively final)인 성격을 가져야 한다는 제약이 있습니다 =>effectively final : final 선언은 아니지만 값이 한번만 할당되어 final처럼 쓰인다는 의미

https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html 언급

제약이 있는 이유

이 제약이 있는이유는 객체는 heap에있어 스레드간 공유가 가능한데, 지역변수는 stack에있어 스레드간 공유가 불가능하여. 동시성 이슈 발생 가능성 때문입니다.

결론

자바는 이를 해결하기위해 heap영역에 존재하는 객체가 stack 영역의 변수를 안전하게 사용할수있도록 값 복사를 합니다. 그래서 엄밀히말하면 람다는 바깥의 필드값(effectively final)을 복사한 값을 참조하는게 맞을것같네요

영준님 코드 람다표현식에선 Test의 멤버변수 value를 복사한값을 참조하는것 같습니다

익명 객체는 만들때마다 별도의 객체를 계속 생성하지만, 람다는 별도의 객체를 생성하거나 별도의 클래스를 생성하지않고 새로운 메서드를 static으로 생성해 메서드를 실행시킵니다. 익명클래스와 람다의 스콥이 다를수밖에 없습니다 (https://sujl95.tistory.com/76 에 익명객체, 람다표현식 바이트코드 비교내용 보시면 이해되실듯합니다)

변수 캡처, effectively final 관련 자료 https://www.baeldung.com/java-lambda-effectively-final-local-variables