[java] 다른 메소드에 정의 된 내부 클래스 내에서 비 ​​최종 변수를 참조 할 수 없습니다.

편집 : 타이머를 통해 여러 번 실행되는 여러 변수의 값을 변경해야합니다. 타이머를 통해 반복 할 때마다 값을 계속 업데이트해야합니다. 값을 업데이트 할 수 없으므로 값을 final로 설정할 수 없지만 아래의 초기 질문에서 설명하는 오류가 발생합니다.

이전에 아래 내용을 작성했습니다.

“다른 방법으로 정의 된 내부 클래스 내의 비 최종 변수를 참조 할 수 없습니다”라는 오류가 발생합니다.

이것은 price라고하는 가격과 priceObject라는 가격에 발생합니다. 내가 왜이 문제가 발생하는지 아십니까? 최종 선언이 필요한 이유를 이해하지 못합니다. 또한 내가하려는 일을 볼 수 있다면이 문제를 해결하기 위해 무엇을해야합니까?

public static void main(String args[]) {

    int period = 2000;
    int delay = 2000;

    double lastPrice = 0;
    Price priceObject = new Price();
    double price = 0;

    Timer timer = new Timer();

    timer.scheduleAtFixedRate(new TimerTask() {
        public void run() {
            price = priceObject.getNextPrice(lastPrice);
            System.out.println();
            lastPrice = price;
        }
    }, delay, period);
}



답변

Java는 여기에서 사용하는 것과 같은 익명 클래스를 사용하는 것이 일종의 클로저처럼 보이지만 진정한 클로저를 지원하지 않습니다 new TimerTask() { ... }.

편집아래 주석을 참조하십시오-KeeperOfTheSoul이 지적한대로 다음은 올바른 설명이 아닙니다.

이것이 작동하지 않는 이유입니다.

변수 lastPrice와 가격은 main () 메소드의 로컬 변수입니다. 익명 클래스로 만든 객체는 main()메서드가 반환 될 때까지 지속될 수 있습니다 .

main()메소드가 리턴 (같은 지역 변수 lastPrice와는 price) 스택에서 정리됩니다, 그래서 그들은 이후 더 이상 존재하지 않을 것이다 main()돌아갑니다.

그러나 익명 클래스 객체는 이러한 변수를 참조합니다. 익명 클래스 객체가 변수를 정리 한 후 변수에 액세스하려고하면 상황이 끔찍하게 잘못 될 수 있습니다.

함으로써 lastPrice그리고 price final, 그들은 더 이상 변수 만 상수는 정말 아니다. 그러면 컴파일러는 익명 클래스 의 사용 lastPriceprice익명 클래스 의 사용을 상수 값 (물론 컴파일 타임에)으로 대체 할 수 있으며 더 이상 존재하지 않는 변수에 액세스하는 데 문제가 없습니다.

클로저를 지원하는 다른 프로그래밍 언어는 이러한 변수를 특수하게 처리하여 메소드가 종료 될 때 변수가 손상되지 않도록하여 클로저가 여전히 변수에 액세스 할 수 있도록합니다.

@Ankur : 당신은 이것을 할 수 있습니다 :

public static void main(String args[]) {
    int period = 2000;
    int delay = 2000;

    Timer timer = new Timer();

    timer.scheduleAtFixedRate(new TimerTask() {
        // Variables as member variables instead of local variables in main()
        private double lastPrice = 0;
        private Price priceObject = new Price();
        private double price = 0;

        public void run() {
            price = priceObject.getNextPrice(lastPrice);
            System.out.println();
            lastPrice = price;
        }
    }, delay, period);
}


답변

익명 대리자가 참조한 Java 변수의 클로저로 이상한 부작용을 피하려면 lastPrice타이머 작업 내에서 참조 하고 가격 을 정하기 위해 최종으로 표시해야합니다.

변경하기를 원하기 때문에 분명히 효과가 없을 것입니다.이 경우 클래스 내에서 캡슐화하는 것을 봐야합니다.

public class Foo {
    private PriceObject priceObject;
    private double lastPrice;
    private double price;

    public Foo(PriceObject priceObject) {
        this.priceObject = priceObject;
    }

    public void tick() {
        price = priceObject.getNextPrice(lastPrice);
        lastPrice = price;
    }
}

이제 마지막으로 새 Foo를 만들고 타이머에서 .tick를 호출하십시오.

public static void main(String args[]){
    int period = 2000;
    int delay = 2000;

    Price priceObject = new Price();
    final Foo foo = new Foo(priceObject);

    Timer timer = new Timer();
    timer.scheduleAtFixedRate(new TimerTask() {
        public void run() {
            foo.tick();
        }
    }, delay, period);
}


답변

익명 클래스를 사용할 때는 포함하는 클래스에서만 최종 변수에 액세스 할 수 있습니다. 따라서 final로 사용되는 변수를 선언하거나 ( lastPriceprice를 변경 하고 있으므로 옵션이 아님 ) 익명 클래스를 사용하지 않아야합니다.

따라서 옵션은 변수를 전달하고 정상적인 방식으로 사용할 수있는 실제 내부 클래스를 만드는 것입니다.

또는:

lastPrice가격 변수에 대한 빠른 (그리고 내 견해로는 추악한) 해킹이 있습니다.

final double lastPrice[1];
final double price[1];

익명 클래스에서 다음과 같이 값을 설정할 수 있습니다

price[0] = priceObject.getNextPrice(lastPrice[0]);
System.out.println();
lastPrice[0] = price[0];


답변

이미 제공하려는 것을 할 수없는 이유에 대한 좋은 설명. 해결책으로 다음을 고려하십시오.

public class foo
{
    static class priceInfo
    {
        public double lastPrice = 0;
        public double price = 0;
        public Price priceObject = new Price ();
    }

    public static void main ( String args[] )
    {

        int period = 2000;
        int delay = 2000;

        final priceInfo pi = new priceInfo ();
        Timer timer = new Timer ();

        timer.scheduleAtFixedRate ( new TimerTask ()
        {
            public void run ()
            {
                pi.price = pi.priceObject.getNextPrice ( pi.lastPrice );
                System.out.println ();
                pi.lastPrice = pi.price;

            }
        }, delay, period );
    }
}

아마도 그보다 더 나은 디자인을 할 수있는 것처럼 보이지만 아이디어는 변경되지 않은 클래스 참조 내에서 업데이트 된 변수를 그룹화 할 수 있다는 것입니다.


답변

익명 클래스를 사용하면 실제로 “이름이없는”중첩 클래스를 선언합니다. 중첩 클래스의 경우 컴파일러는 인수로 사용하는 모든 변수를 인수로 사용하는 생성자를 사용하여 새 독립 실행 형 공용 클래스를 생성합니다 ( “명명 된”중첩 클래스의 경우 항상 원본 / 클래 싱 클래스의 인스턴스 임). 런타임 환경에는 중첩 클래스에 대한 개념이 없기 때문에 중첩 클래스에서 독립형 클래스로 (자동) 변환해야합니다.

이 코드를 예로 들어 보겠습니다.

public class EnclosingClass {
    public void someMethod() {
        String shared = "hello";
        new Thread() {
            public void run() {
                // this is not valid, won't compile
                System.out.println(shared); // this instance expects shared to point to the reference where the String object "hello" lives in heap
            }
        }.start();

        // change the reference 'shared' points to, with a new value
        shared = "other hello";
        System.out.println(shared);
    }
}

이것은 컴파일러가 후드에서하는 일이기 때문에 작동하지 않습니다.

public void someMethod() {
    String shared = "hello";
    new EnclosingClass$1(shared).start();

    // change the reference 'shared' points to, with a new value
    shared = "other hello";
    System.out.println(shared);
}

원래의 익명 클래스는 컴파일러가 생성하는 독립형 클래스로 대체됩니다 (코드는 정확하지 않지만 좋은 아이디어를 제공해야합니다).

public class EnclosingClass$1 extends Thread {
    String shared;
    public EnclosingClass$1(String shared) {
        this.shared = shared;
    }

    public void run() {
        System.out.println(shared);
    }
}

보시다시피 독립형 클래스는 공유 객체에 대한 참조를 보유하고 있으며 Java의 모든 것은 값을 전달한다는 점을 기억하십시오. 따라서 EnclosingClass의 ‘shared’참조 변수가 변경 되더라도 가리키는 인스턴스는 수정되지 않습니다. 익명 클래스의 Enclosing $ 1과 같은 다른 모든 참조 변수는이를 인식하지 못합니다. 이것이 컴파일러가이 ‘공유’변수를 최종 변수로 선언하도록하는 주된 이유입니다. 따라서 이러한 유형의 동작은 이미 실행중인 코드에 영향을 미치지 않습니다.

이제 이것은 익명 클래스 내에서 인스턴스 변수를 사용할 때 발생하는 현상입니다 (문제를 해결하고 로직을 “인스턴스”메서드 또는 클래스 생성자로 이동하려면 수행해야합니다).

public class EnclosingClass {
    String shared = "hello";
    public void someMethod() {
        new Thread() {
            public void run() {
                System.out.println(shared); // this is perfectly valid
            }
        }.start();

        // change the reference 'shared' points to, with a new value
        shared = "other hello";
        System.out.println(shared);
    }
}

컴파일러가 코드를 수정하여 새로 생성 된 클래스 Enclosing $ 1이 인스턴스화 된 EnclosingClass 인스턴스에 대한 참조를 보유하기 때문에 이것은 잘 컴파일됩니다 (이것은 단지 표현 일뿐입니다).

public void someMethod() {
    new EnclosingClass$1(this).start();

    // change the reference 'shared' points to, with a new value
    shared = "other hello";
    System.out.println(shared);
}

public class EnclosingClass$1 extends Thread {
    EnclosingClass enclosing;
    public EnclosingClass$1(EnclosingClass enclosing) {
        this.enclosing = enclosing;
    }

    public void run() {
        System.out.println(enclosing.shared);
    }
}

이와 같이 EnclosingClass에서 ‘shared’참조 변수가 재 할당되고 Thread # run ()을 호출하기 전에이 문제가 발생하면 EnclosingClass $ 1 # enclosing 변수가 참조를 유지하므로 “other hello”가 두 번 인쇄됩니다. 선언 된 클래스의 개체에 대한 속성이므로 해당 개체의 모든 특성에 대한 변경 내용은 EnclosingClass $ 1 인스턴스에 표시됩니다.

주제에 대한 자세한 내용은 다음과 같은 우수한 블로그 게시물을 참조하십시오 (저는 작성하지 않음). http://kevinboone.net/java_inner.html


답변

이 문제를 우연히 발견하면 생성자를 통해 객체를 내부 클래스로 전달합니다. 프리미티브 또는 변경 불가능한 객체 (이 경우와 같이)를 전달해야하는 경우 래퍼 클래스가 필요합니다.

편집 : 실제로 익명 클래스를 사용하지 않고 적절한 하위 클래스를 사용합니다.

public class PriceData {
        private double lastPrice = 0;
        private double price = 0;

        public void setlastPrice(double lastPrice) {
            this.lastPrice = lastPrice;
        }

        public double getLastPrice() {
            return lastPrice;
        }

        public void setPrice(double price) {
            this.price = price;
        }

        public double getPrice() {
            return price;
        }
    }

    public class PriceTimerTask extends TimerTask {
        private PriceData priceData;
        private Price priceObject;

        public PriceTimerTask(PriceData priceData, Price priceObject) {
            this.priceData = priceData;
            this.priceObject = priceObject;
        }

        public void run() {
            priceData.setPrice(priceObject.getNextPrice(lastPrice));
            System.out.println();
            priceData.setLastPrice(priceData.getPrice());

        }
    }

    public static void main(String args[]) {

        int period = 2000;
        int delay = 2000;

        PriceData priceData = new PriceData();
        Price priceObject = new Price();

        Timer timer = new Timer();

        timer.scheduleAtFixedRate(new PriceTimerTask(priceData, priceObject), delay, period);
    }


답변

Java 언어 사양에 나와 있기 때문에 비 최종 변수를 참조 할 수 없습니다. 8.1.3부터 ​​:
“내부 클래스에서 선언되었지만 선언되지 않은 모든 로컬 변수, 공식 메소드 매개 변수 또는 예외 핸들러 매개 변수는 final로 선언되어야합니다.” 전체 단락.
로컬 변수의 수정을 예약하는 것은 이상한 생각입니다. 함수를 떠날 때 지역 변수가 존재하지 않습니다. 어쩌면 클래스의 정적 필드가 더 좋을까요?