domingo, 27 de octubre de 2013

Java: Overridable method call in constructor


Introducción


En los dos artículos anteriores he explicado algunos detalles complejos de la creación de objetos en Java:

  1. Java: creando objetos - Parte 1 
  2. Java: creando objetos - Parte 2 

Aprovecharé esos conocimientos para explicar por qué algunas herramientas de desarrollo muestran una advertencia (warning) con el mensaje:

Constructor calls overridable method
(Constructor invoca un método redefinible)

Voy a destacar que es una advertencia y no un error, aunque es muy importante tenerla en cuenta, porque puede provocar que un programa muestre un comportamiente totalmente diferente al esperado por el programador.

Descripción del problema


En los artículos previamente citados explico el orden en el que se inicializan los atributos y el orden en el que se ejecutan las intrucciones de inicialización (constructores y bloques de inicialización). Todo eso junto con el hecho de que en Java siempre se invoca a la última redefinición de un método puede provocar que:

dentro de un constructor se acceda a atributos que aún no han sido inicializados y que por tanto serán inicializados tras acceder a ellos.

Y ahora te estarás preguntando: ¿en Java es posible acceder a un atributo no inicializado?
La respuesta depende de lo que se entienda por "inicializado".

Todo atributo es preinicializado a su valor por defecto (0, null o similar), así que nunca se podrá acceder a un atributo con valor desconocido.
Sin embargo, los valores con los que el programador inicializa explícitamente los atributos se establecen más tarde, por lo que cabe la posibilidad de acceder al atributo cuando "sólo ha sido preinicializado pero no inicializado explícitamente por el programador".

¿Por qué puede ocurrir el problema? Porque al redefinir un método puede ser ejecutado desde el constructor de una clase base. Si dentro del método redefinido, se accede a atributos de la clase derivada, se estará accediendo a dichos atributos cuando sólo han sido preinicializados, y por tanto, pueden no tener el valor que espera el programador en ese momento.

Ejemplo


Todo este rollo teórico no aclara mucho, así que veámos el problema con un ejemplo.
El pequeño programa OverridableMethodTest sirve para ilustrar el problema. De hecho, sólo sirve para eso, porque el código es bastante absurdo. Puedes ver y copiar el código fuente en el siguiente enlace:


En el código fuente se puede ver que el atributo Super#value nunca recibe explícitamente el valor 0, ya que es inicializado a 999 y posteriormente modificado en el constructor de la clase Super con el resultado del método abstracto getInitialValue().

En las subclases One y Two se define el método getInitialValue() para devolver el valor actual de los respectivos atributos initialValue, inicializados explícitamente a 1 en One y a 2 en Two.

Aparentemente es de esperar que si se instancian las clases One y Two y se imprime su atributo value se obtenga 1 y 2 respectivamente. Pero al ejecutar el programa, las salida es la siguiente:


Instance of One. Expected value 1 =>Class<One>: Value<0> InitialValue<1>
Instance of Two. Expected value 2 =>Class<Two>: Value<0> InitialValue<2>
After invoking setValue(...):
Instance of One. Expected value 10 =>Class<One>: Value<10> InitialValue<1>
Instance of Two. Expected value 20 =>Class<Two>: Value<20> InitialValue<2>

¿Qué? ¿Cómo?
¡El atributo value sale a 0 en las intancias de One y Two!
¿Pero si no hay ningún 0 en todo el código fuente?
¿Cómo es posible?

Lo que está pasando es que al instanciar One desde el constructor Super#Super() se invoca al método One#getInitialValue() que accede al atributo One#initialValue ¡qué aún no ha sido inicializado y por tanto aún tiene su valor por defecto (0 por ser int)! Al instanciar Two ocurre exactamente lo respectivo.

Obsérvese que el valor del atributo initialValue que se imprime siempre es correcto para One y Two, pero sin embargo, desde Super#Super() se accede a su valor por defecto.

Para liar aún más la cosa, te propongo que declares los atributos One#initialValue y Two#initialValue como final y veas los resultados. La cosa cambia...

Conclusiones


Invocar métodos redefinibles desde un constructor es peligroso, ya que puede provocar resultados inesperados, difíciles de entender y sobre todo, porque es un problema difícil de localizar si no disponemos de una herramienta que nos avise del riesgo. Por ejemplo, este riesgo es la razón de que el Netbeans IDE cuando encapsula atributos, dentro de los constructores nunca invoque a los métodos accesores get y set, sino que accede directamente a los atributos (a menos claro que los métodos get y set se hayan definido como final).

Sin embargo, como cualquier otra característica del lenguaje, puede resultar muy útil en algunos casos. Mientras los métodos invocados no dependan ni alteren el estado interno del objeto no hay peligro.

En conclusión, hay que tener claro el riesgo y usar esta característica con mucho cuidado o evitarla.

Enlaces


Enlaces de interés relacionados con este artículo:


(Actualizado 27/10/2013)

Source code: OverridableMethodTest

Código fuente del programa OverridableMethodTest

Ver la entrada correspondiente en el siguiente enlace:


Java: Overridable method call in constructor



package pruebas;

import java.io.PrintStream;

public class OverridableMethodTest {

    //Example.
    public static void main(String[] args) {
        final PrintStream out= System.out;

        Super one= new One();
        out.format("Instance of One. Expected value 1 =>%s%n", one);

        Super two= new Two();
        out.format("Instance of Two. Expected value 2 =>%s%n", two);

        out.format("After invoking setValue(...):%n");
        one.setValue(10);
        out.format("Instance of One. Expected value 10 =>%s%n", one);
        two.setValue(20);
        out.format("Instance of Two. Expected value 20 =>%s%n", two);
    }
    //--------------------------------------------------------------------------
    //--------------------------------------------------------------------------
    

abstract public static class Super {

    private int value= 999;
    //--------------------------------------------------------------------------

    public Super() {
        super();

        int aux= this.getInitialValue();
        this.setValue(aux);
    }
    //--------------------------------------------------------------------------

    public int getValue() {
        return this.value;
    }
    public void setValue(int aValue) {
        this.value= aValue;
    }
    //--------------------------------------------------------------------------

    abstract public int getInitialValue();
    //--------------------------------------------------------------------------

    @Override
    public String toString() {
        String str= String.format("Class<%s>: Value<%s> InitialValue<%s>"
            , this.getClass().getSimpleName()
            , this.getValue()
            , this.getInitialValue()
        );

        return str;
    }
    //--------------------------------------------------------------------------

}
    //--------------------------------------------------------------------------
    //--------------------------------------------------------------------------
    

public static class One extends Super {

    private int initialValue= 1;
    //--------------------------------------------------------------------------

    public One() {
        super();
    }
    //--------------------------------------------------------------------------

    @Override
    public int getInitialValue() {
        return this.initialValue;
    }
    //--------------------------------------------------------------------------

}
    //--------------------------------------------------------------------------
    //--------------------------------------------------------------------------


public static class Two extends Super {

    private int initialValue= 2;
    //--------------------------------------------------------------------------

    public Two() {
        super();
    }
    //--------------------------------------------------------------------------

    @Override
    public int getInitialValue() {
        return this.initialValue;
    }
    //--------------------------------------------------------------------------

}
    //--------------------------------------------------------------------------
    //--------------------------------------------------------------------------

}