Con las clases anónimas, en realidad estás declarando una clase anidada "sin nombre". Para las clases anidadas, el compilador genera una nueva clase pública independiente con un constructor que tomará todas las variables que usa como argumentos (para las clases anidadas "con nombre", esta siempre es una instancia de la clase original / adjunta). Esto se hace porque el entorno de tiempo de ejecución no tiene noción de clases anidadas, por lo que debe haber una conversión (automática) de una clase anidada a una independiente.
Tome este código por ejemplo:
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);
}
}
Eso no funcionará, porque esto es lo que hace el compilador debajo del capó:
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);
}
La clase anónima original se reemplaza por una clase independiente que genera el compilador (el código no es exacto, pero debería darle una buena idea):
public class EnclosingClass$1 extends Thread {
String shared;
public EnclosingClass$1(String shared) {
this.shared = shared;
}
public void run() {
System.out.println(shared);
}
}
Como puede ver, la clase independiente tiene una referencia al objeto compartido, recuerde que todo en Java es paso por valor, por lo que incluso si la variable de referencia 'compartida' en EnclosingClass cambia, la instancia a la que apunta no se modifica , y todas las demás variables de referencia que lo apuntan (como la de la clase anónima: Incluyendo $ 1), no se darán cuenta de esto. Esta es la razón principal por la que el compilador lo obliga a declarar estas variables 'compartidas' como finales, para que este tipo de comportamiento no se convierta en su código ya en ejecución.
Ahora, esto es lo que sucede cuando usa una variable de instancia dentro de una clase anónima (esto es lo que debe hacer para resolver su problema, mover su lógica a un método de "instancia" o un constructor de una clase):
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);
}
}
Esto compila bien, porque el compilador modificará el código, de modo que la nueva clase generada Enclosing $ 1 tendrá una referencia a la instancia de EnclosingClass donde se instancia (esto es solo una representación, pero debería ayudarlo):
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);
}
}
De esta manera, cuando la variable de referencia 'compartida' en EnclosingClass se reasigna, y esto sucede antes de la llamada a Thread # run (), verá "otro hola" impreso dos veces, porque ahora la variable de encierro EnclosingClass $ 1 # mantendrá una referencia al objeto de la clase donde se declaró, por lo que los cambios en cualquier atributo de ese objeto serán visibles para las instancias de EnclosingClass $ 1.
Para obtener más información sobre el tema, puede ver esta excelente publicación de blog (no escrita por mí): http://kevinboone.net/java_inner.html