domingo, 29 de septiembre de 2013

Java: creando objetos - Parte 2


Introducción


Siguiendo con la explicación de algunas características no triviales de la creación de objetos en Java, ahora me voy a centrar en una cuestión un tanto confusa y que probablemente nadie domina del todo:

¿dónde, cuándo y en qué orden se inicializan los atributos?

Preinicialización de atributos


Al construir un objeto nuevo, lo primero que ocurre es que se reserva memoria para todos sus atributos y para todos los atributos de todas sus superclases.

Una vez reservada la memoria, cada atributo se inicializa a su valor por defecto:
  • byte: cero, concretamente (byte)0.
  • short: cero, concretamente (short)0.
  • int: cero, concretamente 0.
  • long: cero, concretamente 0L.
  • float: cero, concretamente 0.0f.
  • double: cero, concretamente 0.0d.
  • char: el carácter null, o sea '\u0000'.
  • boolean: false.
  • all reference types: null.


Para recordarlo fácilmente, basta con pensar que el área de memoria se rellena a 0, generando false para boolean, el carácter null para char, 0 para todos los números y null para las referencias a objetos.


Orden de inicialización de atributos


Recuérdese que los atributos ya se han preinicializado automáticamente a sus valores por defecto.

Al crear un objeto, se pueden inicializar sus atributos explícitamente en 3 sitios diferentes:

  1. Asignaciones: al declarar el atributo, se le puede asignar un valor.
    ...
    private int att= 1;
    ...
    
  2. Bloques de inicialización: son bloques anónimos a nivel de clase (código encerrado entre llaves) que el compilador copia literalmente en todos los constructores que invoquen al constructor de la clase base, justo tras dicha invocación (es decir, que si se invoca a otro constructor de la propia clase no se replica el código en dicho punto, sino que sólo se hace tras las invocaciones a un constructor de la clase base).
    ...
    {
        this.att= 1;
    }
    ...
    
  3. Cuerpo de los constructores: dentro de un constructor se pueden asignar valores a los atributos.
    ...
    public MyClass() {
        this.att= 1;
    }
    ...
    

En los 3 casos, el cálculo de los valores de los atributos ya puede hacer uso de referencias a this y super.

Las clases anónimas no pueden tener constructores explícitos, pero sí los otros 2 métodos de inicialización.

El orden en el que se ejecutan las acciones es el siguiente:
  • Preinicialización.
  • Tras invocar al constructor de la clase base, Asignaciones y Bloques de inicialización en el orden en que aparecen en el código fuente.
  • Cuerpo del constructor.
Destacar que las Asignaciones y los Bloques de inicialización tienen la misma precedencia, no se agrupan las asignaciones por un lado y los bloques de inicialización por otro ni nada por el estilo, sino que tal cual van a apareciendo unos y otros en el código fuente se van ejecutando.


Ejemplo


El pequeño programa InitializationOrder sirve para ilustrar y comprobar el orden en el que se inicializan los atributos de una clase. Puedes ver y copiar el código fuente en el siguiente enlace:


Todos los atributos que se crean son de tipo String, creando todas las Strings en el método next(String), que prefija cada String con el número de invocaciones al método, de modo que, el propio valor de la String indica cuando fue creada.

El método echo(String) sólo sirve para realizar una traza del programa.

Observando los resultados del programa y cambiando de orden algunas de sus instrucciones se puede comprobar lo que se explica en este artículo.

Conclusiones


Dado que el orden de inicialización depende del orden de aparición en el código fuente, yo recomiendo encarecidamente tener mucho cuidado al combinar los diferentes tipos de inicializaciones, ya que, mover bloques de código con posteridad (tal vez por otra persona que mantenga el código) podría provocar un comportamiento diferente y sería un error bastante difícil de encontrar.

No existe una solución perfecta, al fin y al cabo, los diferentes métodos de inicialización son herramientas que pueden resultar útiles en determinadas circunstancias. Sin embargo, en general, la solución que resulta más legible (y por tanto más mantenible) consiste en inicializar los atributos en un único constructor, que reciba todos los argumentos que sean necearios. Por comodidad, se pueden definir otros constructores más sencillos que invoquen al complejo con los valores iniciales. Por ejemplo:



public class Point {

    public int x;
    public int y;

    public Point(int aX, int aY) {
        super();

        this.x= aX;
        this.y= aY;
    }

    public Point() {
        this(0, 0);
    }

}


Recalco que esto es tan sólo una recomendación personal basada en la experiencia.De todas formas, no siempre es posible hacer esto. Por ejemplo, a veces cada constructor de una clase invoca a un constructor distinto de la superclase. En tal caso, puede resultar útil crear un bloque de inicialización.

Enlaces


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


(Actualizado 29/09/2013)

2 comentarios:

  1. Hey, Carlos, muy buena entrada del blog. Comparto.

    ResponderEliminar
    Respuestas
    1. Gracias por comentar Avelino.
      A veces al fijarse uno se da cuenta de que hay tantos detalles que no se pueden controlar todos.

      Eliminar