Dzięki anonimowym klasom faktycznie deklarujesz „bezimienną” klasę zagnieżdżoną. W przypadku klas zagnieżdżonych kompilator generuje nową autonomiczną klasę publiczną z konstruktorem, który weźmie wszystkie zmienne, których używa jako argumenty (w przypadku klas nazwanych zagnieżdżonych jest to zawsze instancja klasy oryginalnej / zamykającej). Dzieje się tak, ponieważ środowisko wykonawcze nie ma pojęcia o zagnieżdżonych klasach, dlatego konieczna jest (automatyczna) konwersja z zagnieżdżonej do samodzielnej klasy.
Weźmy na przykład ten kod:
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);
}
}
To nie zadziała, ponieważ to właśnie robi kompilator pod maską:
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);
}
Oryginalną anonimową klasę zastępuje pewna samodzielna klasa generowana przez kompilator (kod nie jest dokładny, ale powinien dać ci dobry pomysł):
public class EnclosingClass$1 extends Thread {
String shared;
public EnclosingClass$1(String shared) {
this.shared = shared;
}
public void run() {
System.out.println(shared);
}
}
Jak widać, samodzielna klasa zawiera odniesienie do obiektu współdzielonego, pamiętaj, że wszystko w java jest przekazywane przez wartość, więc nawet jeśli zmienna odniesienia „shared” w EnclosingClass zostanie zmieniona, instancja, na którą wskazuje, nie jest modyfikowana , i wszystkie inne zmienne odniesienia do niego wskazujące (jak ta w klasie anonimowej: Załączanie 1 $), nie będą tego świadome. Jest to główny powód, dla którego kompilator zmusza cię do zadeklarowania tych „współdzielonych” zmiennych jako ostatecznych, aby tego rodzaju zachowanie nie przekształciło się w już działający kod.
Oto, co dzieje się, gdy używasz zmiennej instancji w anonimowej klasie (to powinieneś zrobić, aby rozwiązać problem, przenieść logikę do metody „instancji” lub konstruktora klasy):
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);
}
}
To kompiluje się dobrze, ponieważ kompilator zmodyfikuje kod, tak aby nowa generowana klasa Enclosing $ 1 zawierała odwołanie do instancji EnclosingClass, w której została utworzona instancja (jest to tylko reprezentacja, ale powinna zacząć działać):
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);
}
}
W ten sposób, gdy zmienna referencyjna „współdzielona” w EnclosingClass zostanie ponownie przypisana, a dzieje się to przed wywołaniem wątku # run (), zobaczysz dwa razy wydrukowane „other hello”, ponieważ teraz zmienna otaczająca EnclosingClass $ 1 zachowa referencję do obiektu klasy, w której został zadeklarowany, więc zmiany dowolnego atrybutu tego obiektu będą widoczne dla instancji klasy EnclosingClass $ 1.
Aby uzyskać więcej informacji na ten temat, możesz zobaczyć ten znakomity post na blogu (nie napisany przeze mnie): http://kevinboone.net/java_inner.html