Dlaczego dostępne są tylko zmienne końcowe w anonimowej klasie?

354
  1. amoże być tylko ostateczny tutaj. Dlaczego? W jaki sposób można przypisać aw onClick()sposób bez zachowania go jako prywatnego członka?

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                int b = a*5;
    
            }
        });
    }
  2. Jak mogę zwrócić 5 * apo kliknięciu? Mam na myśli,

    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 
            }
        });
    }
Andrii Abramov
źródło
1
Nie sądzę, że anonimowe klasy Java zapewniają takie zamknięcie lambda, jakiego można się spodziewać, ale ktoś, proszę, popraw mnie, jeśli się mylę ...
user541686
4
Co próbujesz osiągnąć? Moduł obsługi kliknięć można uruchomić po zakończeniu „f”.
Ivan Dubrov
@Lambert, jeśli chcesz użyć metody onClick, musi to być ostateczna @Ivan, w jaki sposób metoda f () może zachowywać się tak, jak metoda onClick () zwraca int po kliknięciu
2
Mam na myśli to - nie obsługuje pełnego zamknięcia, ponieważ nie pozwala na dostęp do zmiennych nie-końcowych.
user541686,
4
Uwaga: od wersji Java 8 twoja zmienna musi być efektywnie ostateczna
Peter Lawrey

Odpowiedzi:

489

Jak zauważono w komentarzach, niektóre z nich stają się nieistotne w Javie 8, gdzie finalmoże być niejawne. Tylko skutecznie zmienna końcowy może być stosowany w anonimowej klasy wewnętrznej lub lambda wyrażenia chociaż.


Zasadniczo wynika to ze sposobu, w jaki Java zarządza zamknięciami .

Kiedy tworzysz instancję anonimowej klasy wewnętrznej, wszystkie zmienne używane w tej klasie mają swoje wartości kopiowane za pomocą automatycznie generowanego konstruktora. Dzięki temu kompilator nie musi automatycznie generować różnych dodatkowych typów w celu utrzymania stanu logicznego „zmiennych lokalnych”, jak na przykład kompilator C # ... (Gdy C # przechwytuje zmienną w funkcji anonimowej, naprawdę przechwytuje zmienną - zamknięcie może zaktualizować zmienną w sposób widoczny dla głównej części metody i odwrotnie.)

Ponieważ wartość została skopiowana do instancji anonimowej klasy wewnętrznej, wyglądałoby to dziwnie, gdyby zmienna mogła zostać zmodyfikowana przez resztę metody - możesz mieć kod, który wydaje się działać z nieaktualną zmienną ( bo tak faktycznie by się działo ... pracowałbyś z kopią wykonaną w innym czasie). Podobnie, jeśli możesz wprowadzić zmiany w anonimowej klasie wewnętrznej, programiści mogą oczekiwać, że zmiany te będą widoczne w treści metody zamykającej.

Ostateczne zakończenie zmiennej usuwa wszystkie te możliwości - ponieważ wartości w ogóle nie można zmienić, nie musisz się martwić, czy takie zmiany będą widoczne. Jedynym sposobem, aby pozwolić metodzie i anonimowej klasie wewnętrznej zobaczyć się nawzajem, jest użycie zmiennego typu opisu. Może to być sama klasa zamykająca, tablica, zmienny typ opakowania ... cokolwiek takiego. Zasadniczo jest to trochę jak komunikacja między jedną metodą: zmiany dokonane w parametrach jednej metody nie są widoczne dla jej obiektu wywołującego, ale zmiany dokonane w obiektach, do których odnoszą się parametry, są widoczne.

Jeśli interesuje Cię bardziej szczegółowe porównanie między zamknięciami Java i C #, mam artykuł, który omawia to bardziej szczegółowo. W tej odpowiedzi chciałem skupić się na stronie Java :)

Jon Skeet
źródło
4
@Ivan: Zasadniczo jak C #. Jest jednak dość skomplikowany, jeśli chcesz mieć taką samą funkcjonalność jak C #, gdzie zmienne z różnych zakresów mogą być „tworzone” z różnych liczb.
Jon Skeet,
11
Tak było w przypadku Java 7, należy pamiętać, że w Javie 8 wprowadzono zamknięcia i teraz rzeczywiście można uzyskać dostęp do niekończącego się pola klasy z jego klasy wewnętrznej.
Mathias Bader
22
@MathiasBader: Naprawdę? Myślałem, że nadal jest to w zasadzie ten sam mechanizm, kompilator jest teraz na tyle sprytny, aby wnioskować final(ale nadal musi być skuteczny).
Thilo,
3
@Mathias Bader: zawsze można było uzyskać dostęp do pól nie-końcowych , których nie należy mylić ze zmiennymi lokalnymi , które musiały być ostateczne i nadal muszą być faktycznie ostateczne, więc Java 8 nie zmienia semantyki.
Holger
41

Istnieje sztuczka, która pozwala anonimowej klasie aktualizować dane w zakresie zewnętrznym.

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?
}

Jednak ta sztuczka nie jest zbyt dobra z powodu problemów z synchronizacją. Jeśli program obsługi zostanie wywołany później, musisz 1) zsynchronizować dostęp do res, jeśli program obsługi został wywołany z innego wątku 2) musi mieć jakąś flagę lub informację, że res został zaktualizowany

Ta sztuczka działa OK, jeśli anonimowa klasa zostanie natychmiast wywołana w tym samym wątku. Lubić:

// ...

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

// ...
Ivan Dubrov
źródło
2
dzięki za odpowiedź. Wiem o tym wszystkim i moje rozwiązanie jest lepsze niż to. moje pytanie brzmi „dlaczego tylko ostateczny”?
6
Odpowiedź brzmi: w jaki sposób są one wdrażane :)
Ivan Dubrov
1
Dzięki. Sam skorzystałem z powyższej sztuczki. Nie byłem pewien, czy to dobry pomysł. Jeśli Java na to nie pozwala, może to mieć dobry powód. Twoja odpowiedź wyjaśnia, że ​​mój List.forEachkod jest bezpieczny.
RuntimeException
Przeczytaj stackoverflow.com/q/12830611/2073130, aby uzyskać dobre omówienie uzasadnienia „dlaczego tylko ostateczny”.
lcn
istnieje kilka obejść. Mój to: final int resf = res; Początkowo korzystałem z metody tablicowej, ale uważam, że ma ona zbyt skomplikowaną składnię. AtomicReference jest może nieco wolniejszy (przydziela obiekt).
zakmck
17

Klasa anonimowa to klasa wewnętrzna, a do klas wewnętrznych obowiązuje ścisła reguła (JLS 8.1.3) :

Każda zmienna lokalna, parametr metody formalnej lub parametr procedury obsługi wyjątków użyty, ale nie zadeklarowany w klasie wewnętrznej, musi zostać zadeklarowany jako końcowy . Każda zmienna lokalna, używana, ale nie zadeklarowana w klasie wewnętrznej, musi zostać definitywnie przypisana przed treścią klasy wewnętrznej .

Nie znalazłem jeszcze powodu ani wyjaśnienia na temat plików jls lub jvms, ale wiemy, że kompilator tworzy osobny plik klasy dla każdej klasy wewnętrznej i musi się upewnić, że metody zadeklarowane w tym pliku klasy ( na poziomie kodu bajtowego) przynajmniej mają dostęp do wartości zmiennych lokalnych.

( Jon ma kompletną odpowiedź - nie usuwam tego, ponieważ ktoś może być zainteresowany zasadą JLS)

Andreas Dolk
źródło
11

Możesz utworzyć zmienną na poziomie klasy, aby uzyskać zwracaną wartość. mam na myśli

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;
        }
    });
}

teraz możesz uzyskać wartość K i używać jej tam, gdzie chcesz.

Odpowiedź na pytanie dlaczego:

Lokalna instancja klasy wewnętrznej jest powiązana z klasą główną i może uzyskać dostęp do końcowych zmiennych lokalnych swojej metody zawierającej. Gdy instancja korzysta z ostatecznego ustawienia lokalnego swojej metody zawierającej, zmienna zachowuje wartość, którą trzymała w momencie jej tworzenia, nawet jeśli zmienna wykroczyła poza zakres (jest to w rzeczywistości surowa, ograniczona wersja zamknięć Javy).

Ponieważ lokalna klasa wewnętrzna nie jest członkiem klasy ani pakietu, nie jest zadeklarowana z poziomem dostępu. (Wyjaśnij jednak, że jego członkowie mają poziomy dostępu jak w normalnej klasie).

Ashfak Balooch
źródło
Wspomniałem, że „bez zachowania go jako członka prywatnego”
6

Cóż, w Javie zmienna może być ostateczna nie tylko jako parametr, ale jako pole na poziomie klasy

public class Test
{
 public final int a = 3;

lub jako zmienna lokalna, taka jak

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

Jeśli chcesz uzyskać dostęp do zmiennej z klasy anonimowej i ją zmodyfikować, możesz ustawić zmienną na poziomie klasy w klasie zamykającej .

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

Nie możesz mieć zmiennej jako ostatecznej i nadać jej nową wartość.finaloznacza po prostu, że: wartość jest niezmienna i ostateczna.

A ponieważ jest ostateczny, Java może bezpiecznie skopiować go do lokalnych anonimowych klas. Nie dostajesz żadnych odniesień do int (zwłaszcza, że ​​nie możesz mieć odniesień do prymitywów takich jak int w Javie, tylko odniesienia do Objects ).

Po prostu kopiuje wartość a do ukrytego int zwanego a w twojej anonimowej klasie.

Zach L.
źródło
3
Z „zmienną na poziomie klasy” kojarzy mi się static. Być może bardziej zrozumiałe jest użycie „zmiennej instancji”.
eljenso
1
No cóż, użyłem poziomu klasy, ponieważ technika działałaby zarówno ze zmiennymi instancji, jak i zmiennymi statycznymi.
Zach L
wiemy już, że finał jest dostępny, ale chcemy wiedzieć, dlaczego? czy możesz dodać jakieś wyjaśnienie, dlaczego?
Saurabh Oza
6

Powód, dla którego dostęp został ograniczony tylko do lokalnych zmiennych końcowych, jest taki, że jeśli wszystkie zmienne lokalne zostałyby udostępnione, najpierw musiałyby zostać skopiowane do osobnej sekcji, w której klasy wewnętrzne mogą mieć do nich dostęp i utrzymywać wiele kopii zmienne zmienne lokalne mogą prowadzić do niespójności danych. Natomiast zmienne końcowe są niezmienne, a zatem dowolna liczba ich kopii nie będzie miała żadnego wpływu na spójność danych.

Swapnil Gangrade
źródło
Nie jest to realizowane w językach takich jak C #, które obsługują tę funkcję. W rzeczywistości kompilator zmienia zmienną ze zmiennej lokalnej na zmienną instancji lub tworzy dodatkową strukturę danych dla tych zmiennych, która może przekroczyć zakres klasy zewnętrznej. Nie ma jednak „wielu kopii zmiennych lokalnych”
Mike76,
Mike76 Nie spojrzałem na implementację C #, ale Scala robi drugą rzecz, o której wspomniałeś. Myślę, że: jeśli Intjest przeniesiony do wewnątrz zamknięcia, zmień tę zmienną na instancję IntRef(zasadniczo zmiennego Integeropakowania). Każdy dostęp do zmiennej jest następnie odpowiednio przepisywany.
Adowrath,
3

Aby zrozumieć uzasadnienie tego ograniczenia, rozważ następujący program:

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 pozostaje w pamięci po initialize deklaracji metody, ale parametr val nie. JVM nie może uzyskać dostępu do zmiennej lokalnej poza swoim zakresem, więc Java wykonuje kolejne wywołanie printInteger , kopiując wartość val do niejawnego pola o tej samej nazwie w interfaceInstance . InterfaceInstance mówi się, że schwytany wartość parametru lokalnej. Jeśli parametr nie był końcowy (lub faktycznie końcowy), jego wartość mogłaby się zmienić, niesynchronizując się z przechwyconą wartością, potencjalnie powodując nieintuicyjne zachowanie.

Benjamin Curtis Drake
źródło
2

Metody w ramach anonimowej klasy wewnętrznej mogą być wywoływane długo po zakończeniu wątku, który je stworzył. W twoim przykładzie klasa wewnętrzna zostanie wywołana w wątku wysyłki zdarzeń, a nie w tym samym wątku, co ten, który ją utworzył. W związku z tym zakres zmiennych będzie inny. Aby więc chronić takie problemy z zakresem przypisywania zmiennych, musisz je zadeklarować jako ostateczne.

S73417H
źródło
2

Kiedy anonimowa klasa wewnętrzna jest zdefiniowana w ciele metody, wszystkie zmienne zadeklarowane jako ostateczne w zakresie tej metody są dostępne z klasy wewnętrznej. W przypadku wartości skalarnych po przypisaniu wartość końcowej zmiennej nie może się zmienić. W przypadku wartości obiektu odwołanie nie może się zmienić. Pozwala to kompilatorowi Java na „przechwytywanie” wartości zmiennej w czasie wykonywania i przechowywanie kopii jako pola w klasie wewnętrznej. Po zakończeniu metody zewnętrznej i usunięciu ramki stosu oryginalna zmienna zniknęła, ale prywatna kopia klasy wewnętrznej pozostaje w pamięci klasy.

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

ola_star
źródło
1
private void f(Button b, final int a[]) {

    b.addClickHandler(new ClickHandler() {

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

        }
    });
}
Bruce Zu
źródło
0

Ponieważ Jon ma odpowiedź dotyczącą szczegółów implementacji, inną możliwą odpowiedzią byłoby to, że JVM nie chce obsługiwać zapisu w rekordzie, który zakończył jego aktywację.

Rozważ przypadek użycia, w którym twoje lambdas zamiast się stosować, są przechowywane w pewnym miejscu i biegną później.

Pamiętam, że w Smalltalk można uzyskać nielegalny sklep, gdy zrobisz taką modyfikację.

matematyka
źródło
0

Wypróbuj ten kod,

Utwórz listę macierzy, umieść w niej wartość i zwróć ją:

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;
}
Wasim
źródło
OP pyta o powody, dla których coś jest wymagane. W związku z tym należy wskazać, w jaki sposób kod rozwiązuje ten
problem
0

Anonimowa klasa Java jest bardzo podobna do zamknięcia Javascript, ale Java implementuje to w inny sposób. (sprawdź odpowiedź Andersena)

Aby nie mylić programisty Java z dziwnym zachowaniem, które może wystąpić u osób pochodzących z tła Javascript. Chyba dlatego zmuszają nas do korzystaniafinal , to nie jest ograniczenie JVM.

Spójrzmy na poniższy przykład Javascript:

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

W języku Javascript counterwartość będzie wynosić 100, ponieważ jest tylko jednacounter od początku do końca zmienna.

Ale w Javie, jeśli nie final, zostanie wydrukowany 0, ponieważ podczas tworzenia obiektu wewnętrznego 0wartość jest kopiowana do ukrytych właściwości obiektu klasy wewnętrznej. (tutaj są dwie zmienne całkowite, jedna w metodzie lokalnej, druga w ukrytych właściwościach klasy wewnętrznej)

Zatem wszelkie zmiany po utworzeniu wewnętrznego obiektu (jak linia 1) nie wpłyną na wewnętrzny obiekt. Spowoduje to zamieszanie między dwoma różnymi wynikami i zachowaniem (między Javą a Javascriptem).

Uważam, że właśnie dlatego Java postanawia wymusić, aby był ostateczny, więc dane są „spójne” od początku do końca.

GMsoF
źródło
0

Ostateczna zmienna Java wewnątrz klasy wewnętrznej

klasa wewnętrzna może korzystać tylko

  1. referencja z klasy zewnętrznej
  2. końcowe zmienne lokalne spoza zakresu, które są typem odniesienia (np. Object...)
  3. inttyp wartości (pierwotny) (np. ...) może być zawinięty przez końcowy typ odwołania. IntelliJ IDEAmoże pomóc ukryć to w tablicy jednowymiarowej

Gdy kompilator generuje a non static nested( inner class) [About] - nowa klasa - <OuterClass>$<InnerClass>.classtworzona jest związana parametry i przekazywane do konstruktora [zmienna lokalna na stosie] . Jest podobny do zamknięcia

zmienna końcowa jest zmienną, której nie można ponownie przypisać. ostatnia zmienna odniesienia wciąż może być zmieniona poprzez modyfikację stanu

Możliwe, że to będzie dziwne, ponieważ jako programista możesz to zrobić

//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;
}
yoAlex5
źródło
-2

Może ta sztuczka daje ci pomysł

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);
Kapryśny
źródło