Solución:
Primero, en general, no hay problema con llamar a métodos en un constructor. Los problemas son específicamente con los casos particulares de llamar a métodos reemplazables de la clase del constructor y de pasar el objeto this
referencia a métodos (incluidos los constructores) de otros objetos.
Las razones para evitar métodos anulables y “filtraciones this
“puede ser complicado, pero básicamente todos están relacionados con la prevención del uso de objetos inicializados de forma incompleta.
Evite llamar a métodos reemplazables
Las razones para evitar llamar a métodos reemplazables en constructores son una consecuencia del proceso de creación de instancias definido en §12.5 de la Especificación del Lenguaje Java (JLS).
Entre otras cosas, el proceso de §12.5 asegura que al crear una instancia de una clase derivada[1], la inicialización de su clase base (es decir, establecer sus miembros en sus valores iniciales y la ejecución de su constructor) ocurre antes de su propia inicialización. Esto está destinado a permitir la inicialización consistente de clases, a través de dos principios clave:
- La inicialización de cada clase puede centrarse en inicializar solo los miembros que declara explícitamente, con la certeza de que todos los demás miembros heredados de la clase base ya se han inicializado.
- La inicialización de cada clase puede utilizar de forma segura miembros de su clase base como entradas para la inicialización de sus propios miembros, ya que se garantiza que se han inicializado correctamente en el momento en que se produce la inicialización de la clase.
Sin embargo, hay un problema: Java permite el envío dinámico en constructores[2]. Esto significa que si un constructor de clase base que se ejecuta como parte de la instanciación de una clase derivada llama a un método que existe en la clase derivada, se llama en el contexto de esa clase derivada.
La consecuencia directa de todo esto es que al crear una instancia de una clase derivada, se llama al constructor de la clase base antes de que se inicialice la clase derivada. Si ese constructor realiza una llamada a un método que es anulado por la clase derivada, es el método de la clase derivada (no el método de la clase base) el que se llama, aunque la clase derivada aún no se haya inicializado. Evidentemente, esto es un problema si ese método usa algún miembro de la clase derivada, ya que aún no se ha inicializado.
Claramente, el problema es el resultado de los métodos de llamada del constructor de la clase base que pueden ser anulados por la clase derivada. Para evitar el problema, los constructores solo deben llamar a métodos de su propia clase que sean finales, estáticos o privados, ya que estos métodos no pueden ser anulados por clases derivadas. Los constructores de clases finales pueden llamar a cualquiera de sus métodos, ya que (por definición) no pueden derivarse de ellos.
El ejemplo 12.5-2 de JLS es una buena demostración de este problema:
class Super {
Super() { printThree(); }
void printThree() { System.out.println("three"); }
}
class Test extends Super {
int three = (int)Math.PI; // That is, 3
void printThree() { System.out.println(three); }
public static void main(String[] args) {
Test t = new Test();
t.printThree();
}
}
Este programa imprime 0
luego 3
. La secuencia de eventos en este ejemplo es la siguiente:
-
new Test()
se llama en elmain()
método. - Ya que
Test
no tiene un constructor explícito, el constructor predeterminado de su superclase (a saberSuper()
) se llama. - los
Super()
llamadas al constructorprintThree()
. Esto se envía a la versión anulada del método en elTest
clase. - los
printThree()
método delTest
clase imprime el valor actual de lathree
variable miembro, que es el valor predeterminado0
(desde elTest
instancia aún no se ha inicializado). - los
printThree()
método ySuper()
constructor cada salida, y elTest
instancia se inicializa (en cuyo puntothree
luego se establece en3
). - los
main()
llamadas a métodosprintThree()
de nuevo, que esta vez imprime el valor esperado de3
(desde elTest
instancia se ha inicializado ahora).
Como se describió anteriormente, §12.5 establece que (2) debe suceder antes de (5), para garantizar que Super
se inicializa antes Test
es. Sin embargo, el envío dinámico significa que la llamada al método en (3) se ejecuta en el contexto de Test
clase, lo que lleva al comportamiento inesperado.
Evite las fugas this
La restricción contra el paso this
de un constructor a otro objeto es un poco más fácil de explicar.
Básicamente, un objeto no puede considerarse completamente inicializado hasta que su constructor haya completado la ejecución (ya que su propósito es completar la inicialización del objeto). Entonces, si el constructor pasa el objeto this
a otro objeto, ese otro objeto tiene una referencia al objeto aunque no se haya inicializado por completo (ya que su constructor todavía se está ejecutando). Si el otro objeto intenta acceder a un miembro no inicializado o llama a un método del objeto original que depende de que esté completamente inicializado, es probable que se produzca un comportamiento inesperado.
Para ver un ejemplo de cómo esto puede resultar en un comportamiento inesperado, consulte este artículo.
[1] Técnicamente, todas las clases de Java excepto Object
es una clase derivada: solo uso los términos ‘clase derivada’ y ‘clase base’ aquí para delinear la relación entre las clases particulares en cuestión.
[2] No hay ninguna razón dada en el JLS (que yo sepa) de por qué este es el caso. La alternativa, no permitir el envío dinámico en los constructores, haría que todo el problema fuera discutible, lo que probablemente sea exactamente la razón por la que C ++ no lo permite.
Los constructores solo deben llamar a métodos que sean privados, estáticos o finales. Esto ayuda a deshacerse de los problemas que pueden aparecer con Overriding.
Además, los constructores no deberían iniciar subprocesos. Hay dos problemas al iniciar un hilo en un constructor (o inicializador estático):
- en una clase no final, aumenta el peligro de problemas con subclases
- abre la puerta para permitir que esta referencia escape del constructor
No hay nada de malo en crear un objeto de hilo en un constructor (o inicializador estático), simplemente no lo inicie allí.
Llamar al método de instancia en el constructor es peligroso ya que el objeto aún no está completamente inicializado (esto se aplica principalmente a los métodos que se pueden anular). También se sabe que el procesamiento complejo en el constructor tiene un impacto negativo en la capacidad de prueba.
Solo tenga cuidado al hacerlo, es una mala práctica hacerlo con métodos anulables.