익명 클래스에서 최종 변수에만 액세스 할 수있는 이유는 무엇입니까?


354
  1. a여기서 만 최종적 일 수 있습니다. 왜? 어떻게 재 할당 할 수 있습니다 aonClick()개인 회원으로 그것을 유지하지 않고 방법은?

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                int b = a*5;
    
            }
        });
    }
    
  2. 5 * a클릭했을 때 어떻게 반환 합니까? 내말은,

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                 int b = a*5;
                 return b; // but return type is void 
            }
        });
    }
    

1
나는 자바 익명 클래스가 당신이 기대하는 일종의 람다 클로저를 제공한다고 생각하지 않지만, 내가 틀렸다면 누군가 나를 수정하십시오 ...
user541686

4
당신은 무엇을 달성하려고합니까? "f"가 완료되면 클릭 핸들러를 실행할 수 있습니다.
Ivan Dubrov

@Lambert onClick 메소드에서 a를 사용하려면 최종 @Ivan이어야합니다. f () 메소드가 onClick () 메소드가 클릭했을 때 int를 반환하는 것처럼 작동하는 방법

2
그것이 의미하는 바입니다. 그것은 최종 변수가 아닌 변수에 대한 액세스를 허용하지 않기 때문에 완전 폐쇄를 지원하지 않습니다.
541686

4
참고 : Java 8부터 변수는 효과적으로 최종적
이어야합니다

답변:


489

주석에서 언급했듯이, 일부는 Java 8에서는 관련이 final없으며 암시적일 수 있습니다. 익명의 내부 클래스 또는 람다 식 에는 효과적으로 최종 변수 만 사용할 수 있습니다.


기본적으로 Java가 클로저를 관리하는 방식 때문 입니다.

익명 내부 클래스의 인스턴스를 만들면 해당 클래스 내에서 사용되는 모든 변수 는 자동 생성 생성자를 통해 값이 복사됩니다. 이렇게하면 컴파일러가 "로컬 변수"의 논리적 상태를 유지하기 위해 다양한 추가 유형을 자동 생성하지 않아도됩니다. 예를 들어 C # 컴파일러는 다음과 같습니다. (C #은 익명 함수에서 변수를 캡처 할 때 실제로 변수를 캡처합니다. 클로저는 메소드의 본문에서 볼 수있는 방식으로 변수를 업데이트 할 수 있습니다.

값이 익명 내부 클래스의 인스턴스에 복사되었으므로 변수가 나머지 메소드로 수정 될 수 있으면 이상하게 보일 것입니다. 오래된 변수로 작동하는 것으로 보이는 코드가있을 수 있습니다 ( 그 효과적으로 무엇 때문에 일이있을 ... 당신이 다른 시간에 촬영 한 사본)와 함께 일하게 될 것입니다. 마찬가지로 익명의 내부 클래스 내에서 변경을 수행 할 수있는 경우 개발자는 해당 변경 사항이 엔 클로징 메서드 본문 내에서 표시 될 것으로 기대할 수 있습니다.

변수를 final로 설정하면 이러한 모든 가능성이 제거됩니다. 값을 전혀 변경할 수 없기 때문에 이러한 변경이 표시 될지 걱정할 필요가 없습니다. 메소드와 익명의 내부 클래스가 서로의 변경 사항을 볼 수있게하는 유일한 방법은 변경 가능한 유형의 설명을 사용하는 것입니다. 이것은 둘러싸는 클래스 자체, 배열, 변경 가능한 래퍼 유형 등이 될 수 있습니다. 기본적으로 그것은 한 메소드와 다른 메소드 사이의 통신과 비슷합니다. 한 메소드의 매개 변수 에 대한 변경 사항은 호출자 가 볼 수 없지만 매개 변수가 참조 하는 객체의 변경 사항 은 볼 수 있습니다.

Java와 C # 클로저의보다 자세한 비교에 관심이 있다면 더 자세히 다루는 기사 가 있습니다. 이 답변에서 Java 측에 집중하고 싶었습니다. :)


4
@Ivan : 기본적으로 C #처럼. 그러나 다른 범위의 변수가 다른 횟수로 "인스턴스화"될 수있는 C #과 동일한 종류의 기능을 원할 경우 상당히 복잡합니다.
Jon Skeet


11
Java 7의 경우에도 마찬가지입니다. Java 8에서는 클로저가 도입되었으며 이제 내부 클래스에서 클래스의 비 최종 필드에 액세스 할 수 있습니다.
Mathias Bader

22
@MathiasBader : 정말요? 나는 그것이 여전히 본질적으로 동일한 메커니즘이라고 생각했습니다. 컴파일러는 이제 유추하기에 충분히 영리합니다 final(그러나 여전히 효과적으로 최종적이어야합니다).
Thilo

3
@Mathias Bader : 최종 변수 가 아닌 필드에 항상 액세스 할 수 있습니다.이 필드로컬 변수 와 혼동되어서는 안됩니다.이 변수는 최종적이어야하며 여전히 최종적으로 유효해야하므로 Java 8은 의미를 변경하지 않습니다.
Holger

41

익명 클래스가 외부 범위의 데이터를 업데이트 할 수있는 트릭이 있습니다.

private void f(Button b, final int a) {
    final int[] res = new int[1];
    b.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
            res[0] = a * 5;
        }
    });

    // But at this point handler is most likely not executed yet!
    // How should we now res[0] is ready?
}

그러나이 문제는 동기화 문제로 인해 그리 좋지 않습니다. 핸들러가 나중에 호출되는 경우 1) 다른 스레드에서 핸들러가 호출 된 경우 re에 대한 액세스를 동기화해야합니다. 2) res가 업데이트되었음을 ​​나타내는 일종의 플래그 또는 표시가 있어야합니다.

그러나 익명 클래스가 동일한 스레드에서 즉시 호출되면이 트릭은 정상적으로 작동합니다. 처럼:

// ...

final int[] res = new int[1];
Runnable r = new Runnable() { public void run() { res[0] = 123; } };
r.run();
System.out.println(res[0]);

// ...

2
답변 주셔서 감사합니다. 나는이 모든 것을 알고 내 솔루션이 이것보다 낫습니다. 내 질문은 "왜 최종"입니까?

6
정답은 다음과 같습니다.)
Ivan Dubrov

1
감사. 위의 트릭을 직접 사용했습니다. 그것이 좋은 아이디어인지 확실하지 않았습니다. Java가이를 허용하지 않으면 적절한 이유가있을 수 있습니다. 귀하의 답변은 내 List.forEach코드가 안전하다는 것을 분명히합니다 .
RuntimeException

읽기 stackoverflow.com/q/12830611/2073130을 "왜 단지 마지막"뒤에있는 근거의 좋은 토론을위한.
lcn

몇 가지 해결 방법이 있습니다. 광산은 : final int resf = res; 처음에는 배열 방식을 사용했지만 너무 성가신 구문을 찾았습니다. AtomicReference가 약간 느려질 수 있습니다 (객체 할당).
zakmck

17

익명 클래스는 내부 클래스 이며 엄격한 규칙은 내부 클래스 (JLS 8.1.3)에 적용됩니다 .

내부 클래스에서 선언되었지만 선언되지 않은 로컬 변수, 공식 메서드 매개 변수 또는 예외 처리기 매개 변수 는 final로 선언해야합니다 . 내부 클래스에서 사용되었지만 선언되지 않은 로컬 변수는 내부 클래스 의 본문 앞에 반드시 지정해야합니다 .

jls 또는 jvms에 대한 이유 또는 설명을 아직 찾지 못했지만 컴파일러는 각 내부 클래스에 대해 별도의 클래스 파일을 작성하고이 클래스 파일에 선언 된 메소드가 있는지 확인해야합니다 ( 바이트 코드 수준에서)는 적어도 지역 변수의 값에 액세스 할 수 있습니다.

( Jon은 완전한 대답을 가지고 있습니다-JLS 규칙에 관심이있을 수 있으므로 삭제하지 마십시오)


11

클래스 레벨 변수를 작성하여 리턴 값을 얻을 수 있습니다. 내말은

class A {
    int k = 0;
    private void f(Button b, int a){
        b.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
            k = a * 5;
        }
    });
}

이제 K의 가치를 얻고 원하는 곳에서 사용할 수 있습니다.

이유는 다음과 같습니다.

로컬 내부 클래스 인스턴스는 Main 클래스에 연결되며 포함하는 메서드의 최종 로컬 변수에 액세스 할 수 있습니다. 인스턴스가 포함하는 메소드의 최종 로컬을 사용하는 경우 변수가 범위를 벗어난 경우에도 변수는 인스턴스 작성시 보유한 값을 유지합니다 (이는 사실상 Java의 조잡하고 제한된 클로저 버전입니다).

로컬 내부 클래스는 클래스 또는 패키지의 멤버가 아니므로 액세스 수준으로 선언되지 않습니다. (단, 자신의 멤버는 일반 클래스와 같은 액세스 레벨을 갖습니다.)


나는 "개인 회원으로 유지하지 않고"

6

Java에서 변수는 매개 변수뿐만 아니라 클래스 수준 필드와 같이 최종 변수가 될 수 있습니다.

public class Test
{
 public final int a = 3;

또는 같은 지역 변수로

public static void main(String[] args)
{
 final int a = 3;

익명 클래스에서 변수에 액세스하고 변수를 수정하려면 해당 변수를 둘러싸는 클래스의 클래스 수준 변수 로 만들 수 있습니다 .

public class Test
{
 public int a;
 public void doSomething()
 {
  Runnable runnable =
   new Runnable()
   {
    public void run()
    {
     System.out.println(a);
     a = a+1;
    }
   };
 }
}

당신은 마지막으로 변수를 가질 수 없습니다 그리고 그것에게 새로운 가치를 제공합니다. final그 의미는 다음과 같습니다. 값이 변경 불가능하고 최종적입니다.

그리고 최종적으로 Java는 로컬 익명 클래스에 안전하게 복사 할 수 있습니다. int에 대한 참조 를 얻지 못합니다 (특히 Java의 int와 같은 프리미티브에 대한 참조를 가질 수 없으므로 Objects에 대한 참조 만 가능하기 때문에 ).

a의 값을 익명 클래스에서 a라는 암시 적 int로 복사합니다.


3
"클래스 수준 변수"를와 연결 static합니다. "인스턴스 변수"를 대신 사용하면 더 분명 할 것입니다.
eljenso

1
글쎄, 기술은 인스턴스 변수와 정적 변수 모두에서 작동하기 때문에 클래스 수준을 사용했습니다.
Zach L

우리는 이미 final에 접근 할 수 있다는 것을 알고 있지만 그 이유를 알고 싶습니까? 왜 측면에 대한 설명을 더 추가 할 수 있습니까?
Saurabh Oza

6

액세스가 로컬 최종 변수로만 제한되는 이유는 모든 로컬 변수에 액세스 할 수있는 경우 먼저 내부 클래스가 액세스 할 수있는 별도의 섹션에 복사해야하고 여러 사본을 유지 보수해야하기 때문입니다. 가변 지역 변수는 데이터가 일치하지 않을 수 있습니다. 최종 변수는 불변이므로 변수에 대한 사본의 수는 데이터 일관성에 영향을 미치지 않습니다.


이 기능을 지원하는 C #과 같은 언어로 구현 된 방식은 아닙니다. 실제로, 컴파일러는 변수를 로컬 변수에서 인스턴스 변수로 변경하거나 외부 클래스의 범위보다 오래 사용할 수있는 이러한 변수에 대한 추가 데이터 구조를 만듭니다. 그러나 "여러 지역 변수의 다중 사본"은 없습니다
Mike76

Mike76 C #의 구현을 보지 못했지만 Scala는 두 번째로 생각하는 Int것을 수행합니다. 클로저 내부에 다시 ​​할당되는 경우 해당 변수를 인스턴스 IntRef(기본적으로 변경 가능한 Integer래퍼) 로 변경 하십시오. 모든 변수 액세스는 그에 따라 다시 작성됩니다.
Adowrath

3

이 제한의 이론적 근거를 이해하려면 다음 프로그램을 고려하십시오.

public class Program {

    interface Interface {
        public void printInteger();
    }
    static Interface interfaceInstance = null;

    static void initialize(int val) {
        class Impl implements Interface {
            @Override
            public void printInteger() {
                System.out.println(val);
            }
        }
        interfaceInstance = new Impl();
    }

    public static void main(String[] args) {
        initialize(12345);
        interfaceInstance.printInteger();
    }
}

interfaceInstance는 애프터 메모리에 남아 초기화 메소드가 리턴하지만, 매개 변수의 발은 하지 않습니다. JVM은 해당 범위 밖의 로컬 변수에 액세스 할 수 없으므로 Java는 val 값을 interfaceInstance 내에서 동일한 이름의 내재 된 필드 에 복사하여 printInteger에 대한 후속 호출을 수행합니다 . interfaceInstance는 것으로 알려져 캡처 로컬 파라미터의 값. 매개 변수가 최종 (또는 효과적으로 최종)이 아닌 경우 값이 변경되어 캡처 된 값과 동기화되지 않아 직관적이지 않은 동작이 발생할 수 있습니다.


2

익명의 내부 클래스 내의 메소드는 생성 된 스레드가 종료 된 후에 호출 될 수 있습니다. 귀하의 예제에서 내부 클래스는 이벤트 발송 스레드에서 호출되며 생성 된 스레드와 동일한 스레드가 아닙니다. 따라서 변수의 범위가 달라집니다. 따라서 이러한 변수 할당 범위 문제를 보호하려면이를 최종적으로 선언해야합니다.


2

익명의 내부 클래스가 메소드 본문 내에 정의되면 해당 메소드의 범위에서 final로 선언 된 모든 변수는 내부 클래스에서 액세스 할 수 있습니다. 스칼라 값의 경우 일단 지정되면 최종 변수의 값을 변경할 수 없습니다. 객체 값의 경우 참조를 변경할 수 없습니다. 이를 통해 Java 컴파일러는 런타임시 변수 값을 "캡처"하고 사본을 내부 클래스의 필드로 저장할 수 있습니다. 외부 메소드가 종료되고 스택 프레임이 제거되면 원래 변수는 사라지지만 내부 클래스의 개인 사본은 클래스의 자체 메모리에 유지됩니다.

( http://en.wikipedia.org/wiki/Final_%28Java%29 )


1
private void f(Button b, final int a[]) {

    b.addClickHandler(new ClickHandler() {

        @Override
        public void onClick(ClickEvent event) {
            a[0] = a[0] * 5;

        }
    });
}

0

마찬가지로 존은 이 구현 세부 사항은 다른 가능한 대답은 JVM이 자신의 활성화를 종료 한 레코드 쓰기를 처리하지 않는 것이 답합니다.

적용되는 대신 람다가 어느 곳에 저장되어 나중에 실행되는 유스 케이스를 고려하십시오.

스몰 토크에서는 그러한 수정을하면 불법 상점이 생겼다는 것을 기억합니다.


0

이 코드를 사용해보십시오

배열 목록을 만들고 그 안에 값을 넣고 그것을 반환하십시오 :

private ArrayList f(Button b, final int a)
{
    final ArrayList al = new ArrayList();
    b.addClickHandler(new ClickHandler() {

         @Override
        public void onClick(ClickEvent event) {
             int b = a*5;
             al.add(b);
        }
    });
    return al;
}

OP는 왜 무언가가 필요한지 묻습니다. 따라서 코드가 어떻게 코드를 다루고 있는지 지적해야합니다.
NitinSingh

0

Java 익명 클래스는 Javascript 클로저와 매우 유사하지만 Java는 다른 방식으로 구현합니다. (앤더슨의 답변 확인)

따라서 Java 개발자가 Javascript 배경에서 오는 사람들에게 발생할 수있는 이상한 동작을 혼동하지 않기 위해. 그것이 그들이 우리를 강제로 사용하는 이유라고 생각합니다 final. 이것은 JVM 제한이 아닙니다.

아래의 자바 스크립트 예제를 보자.

var add = (function () {
  var counter = 0;

  var func = function () {
    console.log("counter now = " + counter);
    counter += 1; 
  };

  counter = 100; // line 1, this one need to be final in Java

  return func;

})();


add(); // this will print out 100 in Javascript but 0 in Java

Javascript에서는 처음부터 끝까지 counter하나의 counter변수 만 있기 때문에 값은 100 입니다.

그러나 Java에서는 내부 객체가 생성되는 동안 값이 내부 클래스 객체의 숨겨진 속성으로 복사 되기 때문에 가 없으면 final출력됩니다 . (여기에는 두 개의 정수 변수가 있습니다. 하나는 로컬 메서드에 있고 다른 하나는 내부 클래스 숨겨진 속성에 있습니다)00

따라서 내부 객체 생성 후 (1 행과 같은) 변경 사항은 내부 객체에 영향을 미치지 않습니다. 따라서 두 가지 결과와 동작 (Java와 Javascript 간)이 혼동됩니다.

이것이 바로 Java가 최종 데이터로 강제하기로 결정했기 때문에 데이터는 처음부터 끝까지 '일관 적'입니다.


0

내부 클래스 내의 Java 최종 변수

내부 클래스 만 사용할 수 있습니다

  1. 외부 클래스에서 참조
  2. 범위를 벗어난 참조 지역 유형의 최종 지역 변수 (예 : Object...)
  3. value (primitive) (예 : int...) 유형은 최종 참조 유형 으로 래핑 될 수 있습니다 . IntelliJ IDEA하나의 요소 배열로 은닉 할 수 있습니다.

non static nested( inner class) [소개] 새로운 클래스 - - 컴파일러에 의해 생성되는 <OuterClass>$<InnerClass>.class생성되고 바인딩 파라미터로 전달되는 생성자 [스택상의 국소 변수] . 폐쇄와 비슷합니다

최종 변수는 재 할당 할 수없는 변수입니다. 최종 참조 변수는 여전히 상태를 수정하여 변경할 수 있습니다

프로그래머로서 이런 식으로 만들 수 있기 때문에 이상 할 수 있습니다.

//Not possible 
private void foo() {

    MyClass myClass = new MyClass(); //address 1
    int a = 5;

    Button button = new Button();

    //just as an example
    button.addClickHandler(new ClickHandler() {


        @Override
        public void onClick(ClickEvent event) {

            myClass.something(); //<- what is the address ?
            int b = a; //<- 5 or 10 ?

            //illusion that next changes are visible for Outer class
            myClass = new MyClass();
            a = 15;
        }
    });

    myClass = new MyClass(); //address 2
    int a = 10;
}

-2

어쩌면이 트릭은 당신에게 아이디어를 줄 것입니다.

Boolean var= new anonymousClass(){
    private String myVar; //String for example
    @Overriden public Boolean method(int i){
          //use myVar and i
    }
    public String setVar(String var){myVar=var; return this;} //Returns self instane
}.setVar("Hello").method(3);
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.