java - que - crear un objeto de una clase dentro de otra clase
¿Cómo se escapa esta referencia a una clase externa mediante la publicación de una instancia de clase interna? (3)
Esto se preguntó de manera un poco diferente antes, pero estaba pidiendo una respuesta sí / no, pero estoy buscando la explicación que falta en el libro (Java Concurrency in Practice), de cómo este aparente gran error sería explotado maliciosamente o accidentalmente.
Un mecanismo final por el cual se puede publicar un objeto o su estado interno es publicar una instancia de clase interna, como se muestra en ThisEscape en el Listado 3.7. Cuando ThisEscape publica EventListener, también publica implícitamente la instancia de ThisEscape que lo encierra, porque las instancias de clase interna contienen una referencia oculta a la instancia de encierro .
Listado 3.7. Permitiendo implícitamente esta referencia a Escape. No hagas esto.
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
3.2.1. Prácticas seguras de construcción
ThisEscape ilustra un caso especial importante de escape, cuando este hace referencia a la fuga durante la construcción. Cuando se publica la instancia interna EventListener, también lo es la instancia adjunta ThisEscape. Pero un objeto está en un estado predecible y consistente solo después de que su constructor regrese, por lo que publicar un objeto desde su constructor puede publicar un objeto construido de forma incompleta. Esto es cierto incluso si la publicación es la última declaración en el constructor. Si esta referencia se escapa durante la construcción, el objeto se considera no construido correctamente. [8]
[8] Más específicamente, esta referencia no debería escapar del hilo hasta que el constructor regrese. El constructor puede almacenar esta referencia en algún lugar siempre que no sea utilizada por otro subproceso hasta después de la construcción. SafeListener en el Listado 3.8 usa esta técnica.
No permita que esta referencia escape durante la construcción.
¿Cómo alguien codificaría esto para llegar a OuterClass antes de que termine de construir?
¿Cuál es la
hidden inner class reference
mencionada en cursiva en el primer párrafo?
El punto clave aquí es que a menudo es fácil olvidar que un objeto anónimo en línea todavía tiene una referencia a su objeto padre y así es como este fragmento de código expone una instancia de sí mismo aún no completamente inicializada.
¡Imagine
EventSource.registerListener
inmediatamente llama a
EventLister.doSomething()
!
Se hará algo en un objeto cuyo padre está incompleto.
public class ThisEscape {
public ThisEscape(EventSource source) {
// Calling a method
source.registerListener(
// With a new object
new EventListener() {
// That even does something
public void onEvent(Event e) {
doSomething(e);
}
});
// While construction is still in progress.
}
}
Hacerlo de esta manera taponaría el agujero.
public class TheresNoEscape {
public TheresNoEscape(EventSource source) {
// Calling a method
source.registerListener(
// With a new object - that is static there is no escape.
new MyEventListener());
}
private static class MyEventListener {
// That even does something
public void onEvent(Event e) {
doSomething(e);
}
}
}
Modificaré un poco el ejemplo, para que quede más claro. Considera esta clase:
public class ThisEscape {
Object someThing;
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e, someThing);
}
});
someThing = initTheThing();
}
}
Detrás de escena, la clase interna anónima tiene acceso a la instancia externa.
Puede decir esto, porque puede acceder a la variable de instancia
someThing
y, como mencionó Shashank, puede acceder a la instancia externa a través de
ThisEscape.this
.
El problema es que al dar la instancia de clase interna anónima al exterior (en este caso, el objeto
EventSource
) también llevará consigo la instancia de ThisEscape.
¿Qué puede pasar mal con eso? Considere esta implementación de EventSource a continuación:
public class SomeEventSource implements EventSource {
EventListener listener;
public void registerListener(EventListener listener) {
this.listener = listener;
}
public void processEvent(Event e) {
listener.onEvent(e);
}
}
En el constructor de
ThisEscape
registramos un
EventListener
que se almacenará en la variable de instancia de
listener
.
Ahora considere dos hilos.
Uno llama al constructor
ThisEscape
, mientras que el otro llama a
processEvent
con algún evento.
Además, supongamos que la JVM decide cambiar del primer subproceso al segundo, justo después de la línea
source.registerListener
y justo antes de
someThing = initTheThing()
.
El segundo subproceso ahora se ejecuta y llamará al método onEvent, que, como puede ver, hace algo con algo.
Pero, ¿qué es
someThing
?
Es nulo, porque el otro subproceso no terminó de inicializar el objeto, por lo que esto (probablemente) causará una NullPointerException, que en realidad no es lo que desea.
Para resumir: tenga cuidado de no escapar de los objetos que no se han inicializado completamente (o, en otras palabras, su constructor aún no ha terminado). Una forma sutil de hacer esto sin darse cuenta es escapando de las clases internas anónimas del constructor, que escapará implícitamente de la instancia externa, que no está completamente inicializada.
Por favor vea
este artículo.
Allí se explica claramente lo que podría suceder cuando dejas escapar
this
.
Y aquí hay un follow-up con más explicaciones.
Es el increíble boletín de Heinz Kabutz, donde se discuten este y otros temas muy interesantes. Lo recomiendo altamente.
Aquí está la muestra tomada de los enlaces, que muestran
cómo se
escapa
this
referencia:
public class ThisEscape {
private final int num;
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
num = 42;
}
private void doSomething(Event e) {
if (num != 42) {
System.out.println("Race condition detected at " +
new Date());
}
}
}
Cuando se compila, javac genera dos clases. La clase externa se ve así:
public class ThisEscape {
private final int num;
public ThisEscape(EventSource source) {
source.registerListener(new ThisEscape$1(this));
num = 42;
}
private void doSomething(Event e) {
if (num != 42)
System.out.println(
"Race condition detected at " + new Date());
}
static void access$000(ThisEscape _this, Event event) {
_this.doSomething(event);
}
}
A continuación tenemos la clase interna anónima:
class ThisEscape$1 implements EventListener {
final ThisEscape this$0;
ThisEscape$1(ThisEscape thisescape) {
this$0 = thisescape;
super();
}
public void onEvent(Event e) {
ThisEscape.access$000(this$0, e);
}
}
Aquí la clase interna anónima creada en el constructor de la clase externa se convierte en una clase de acceso a paquete que recibe una referencia a la clase externa (la que permite que
this
escape).
Para que la clase interna tenga acceso a los atributos y métodos de la clase externa, se crea un método de acceso de paquete estático en la clase externa.
Esto es
access$000
.
Esos dos artículos muestran cómo se produce el escape real y qué podría suceder.
El ''qué'' es básicamente una condición de carrera que podría conducir a una
NullPointerException
o cualquier otra excepción al intentar usar el objeto mientras aún no se ha inicializado por completo.
En el ejemplo, si un subproceso es lo suficientemente rápido, podría ocurrir que ejecute el método
doSomething()
mientras
num
aún no se ha inicializado correctamente a
42
.
En el primer enlace hay una prueba que muestra exactamente eso.
EDITAR: Faltan algunas líneas sobre cómo codificar contra este problema / característica. Solo puedo pensar en apegarme a un conjunto (quizás incompleto) de reglas / principios para evitar este problema y otros por igual:
-
Solo llame a métodos
private
desde el constructor -
Si le gusta la adrenalina y desea llamar a métodos
protected
desde el constructor, hágalo, pero declare estos métodos comofinal
, para que no puedan ser anulados por subclases - Nunca cree clases internas en el constructor, ya sea anónimo, local, estático o no estático.
-
En el constructor, no pase
this
directamente como argumento a nada -
Evite cualquier combinación transitiva de las reglas anteriores, es decir, no cree una clase interna anónima en un método
protected final
private
oprotected final
que se invoque desde dentro del constructor - Use el constructor para construir una instancia de la clase y deje que solo inicialice los atributos de la clase, ya sea con valores predeterminados o con argumentos proporcionados
Si necesita hacer más cosas, use el generador o el patrón de fábrica.