¿Cómo se escapa la referencia `this` a una clase externa mediante la publicación de una instancia de clase interna?

Esto se formuló de manera ligeramente diferente, pero solicitando una respuesta de sí / no, pero estoy buscando la explicación que falta en el libro (Concurrencia de Java en la práctica), sobre cómo este aparente gran error podría explotarse maliciosa o accidentalmente.

Un último mecanismo mediante 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 adjunta, porque las instancias de clase interna contienen una referencia oculta a la instancia adjunta .

Listado 3.7. Permitir 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 importante caso especial de escape, cuando las referencias de este escapes durante la construcción. Cuando se publica la instancia de EventListener interno, también lo hace la instancia de ThisEscape adjunta. Pero un objeto está en un estado predecible y consistente solo después de que su constructor retorna, por lo que la publicación de un objeto desde su constructor puede publicar un objeto construido de manera incompleta. Esto es cierto incluso si la publicación es la última statement en el constructor. Si la referencia de este 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 después de que el constructor regrese. El constructor puede almacenar esta referencia en alguna parte, siempre que no sea utilizada por otra cadena hasta después de la construcción. SafeListener en el Listado 3.8 usa esta técnica.

No permita que esta referencia se 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 menciona en cursiva en el primer párrafo?

Por favor mira este artículo. Allí se explica claramente qué podría pasar cuando dejes escapar this .

Y aquí hay un seguimiento con más explicaciones.

Es el boletín increíble 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 comstack, 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 de 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é puede pasar.

El ‘qué’ es básicamente una condición de carrera que podría conducir a una NullPointerException o cualquier otra excepción cuando intente utilizar el objeto mientras aún no se haya inicializado por completo. En el ejemplo, si un hilo es lo suficientemente rápido, podría suceder que ejecute el método doSomething() , mientras que num aún no se haya inicializado correctamente a 42 . En el primer enlace hay una prueba que muestra exactamente eso.

EDITAR: Faltaban algunas líneas sobre cómo codificar en contra de este problema / función. Solo puedo pensar en apegarme a un conjunto de reglas / principios (quizás incompletos) para evitar este problema y otros por igual:

  • Solo llame a métodos private desde dentro del constructor
  • Si le gusta la adrenalina y desea llamar a métodos protected desde el constructor, hágalo, pero declare estos métodos como final , de modo que no puedan ser reemplazados por subclases.
  • Nunca cree clases internas en el constructor, ya sea anónimo, local, estático o no estático
  • En el constructor, no pases 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 o protected final que se invoque desde el constructor.
  • Use el constructor para simplemente construir una instancia de la clase, y permita 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.

Voy a modificar el ejemplo un poco, 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. Puedes decir esto, porque puedes acceder a la variable de instancia someThing y, como mencionó Shashank, puedes 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 del listener .

Ahora considere dos hilos. Uno está llamando al constructor ThisEscape , mientras que el otro llama a processEvent con algún evento. Además, supongamos que la JVM decide pasar del primer subproceso al segundo, justo después de la línea source.registerListener y justo antes de someThing = initTheThing() . El segundo hilo 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) provocará una NullPointerException, que no es realmente lo que desea.

En resumen: tenga cuidado de no escapar de objetos que no se hayan inicializado por completo (o, en otras palabras, su constructor no haya terminado todavía). Una manera sutil en la que podrías hacer esto inadvertidamente es escapar de las clases internas anónimas del constructor, que escapará implícitamente de la instancia externa, que no está completamente inicializada.

El punto clave aquí es que a menudo es fácil olvidar que un objeto anónimo alineado todavía tiene una referencia a su objeto padre y así es como este fragmento de código está exponiendo una instancia de sí mismo aún no iniciada por completo.

Imagine EventSource.registerListener inmediatamente llama a EventLister.doSomething() ! Que se llamará a doSomething en un objeto cuyo padre this es 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 sería tapar 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); } } }