jueves, 26 de diciembre de 2013

Java: static import

Introducción


Hace unos días, programando con el NetBeans 7.3, accidentalmente descubrí una característica de Java que no sabía ni que existía: import static.

Al utilizar la función fix imports del Netbeans 7.3 me dí cuenta, para mi sorpresa, que el NetBeans utilizaba esa directiva para "importar" una constante que tenía definida en otra clase.

¿Para qué sirve?


Pues para muy poca cosa la verdad: para escribir atributos static (normalmente serán constantes) sin tener que poner el nombre totalmente cualificado, es decir, para referenciarlos como si fuesen atributos static de la propia clase desde la que se referencian. Por ejemplo:

package pruebas;

import static java.lang.System.out;

public class StaticImportTest {

    public static void main(String[] args) {
        System.out.println("Using qualified name");
        out.println("Using import static feature");
    }

}

En el ejemplo, en la primera sentencia se referencia System.out con el nombre cualificado, sin embaro, en la segunda sentencia se referencia dicho atributo como si fuese un atributo más de la clase StaticImportTest.

Conclusiones


Llevo años programando en Java y jamás lo había usado, y en principio, seguiré sin hacerlo, porque prefiero escribir en qué clase está cada constante cuando la referencio desde otra, pero como muchas otras cosas, es cuestión de gustos.

Puede resultar útil cuando se hace un uso masivo de algún atributo, pero se pierde la información visual de que no se trata de un atributo propio ni heredado, sino que se está referenciando un atributo de otra clase. Con las constantes es aceptable, pero con atributos static no constantes (que no sea final), en mi humilde opinión, no es una buena idea.


Enlaces


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


(Actualizado 26/12/2013)

miércoles, 27 de noviembre de 2013

JTextField y saltos de línea

Introducción


Una vez leí la siguiente cita:
Any sufficiently advanced bug is indistinguishable from a feature.
- Rich Kulawiec
(Cualquier fallo suficientemente sofisticado es indistinguible de una característica). 
En la documentación de Java Swing se dice que el componente JTextField no acepta varias líneas y que para ese caso hay otras opciones como JTextArea o JTextPane.

Efectivamente, si se pulsa la tecla ENTER cuando el "focus" lo tiene un JTextField no se produce un salto de línea. Pero, ¿qué ocurre si se establece el texto del JTextField mediante el método JTextField#setText(String) con una String que sí contiene saltos de línea?

Pues he descubierto que depende, tal y como se explica en este "fallo documentado":

JDK-6427290: Possibility to put newlines into a JTextField

Esto ocurre al menos en la versiones 1.6.0_37, 1.7.0_25 y anteriores.
Ummm, ¿es un fallo o es una característica útil? Eso lo dejo a tu criterio.

Demostración


La clave está en instalar sobre el JTextField un DocumentFilter que no altere los saltos de línea presentes en la String (en realidad se instala sobre el Document del JTextField).

Por defecto, los JTextField no tienen ningún DocumentFilter instalado y en tal caso el propio JTextField sustituye los saltos de línea que detecta en la String por espacios en blanco (según JDK-6427290).

Sin embargo, al instalar un DocumentFilter, el JTextField no altera la String y delega completamente esa responsabilidad al DocumentFilter.

El pequeño programa JTextFieldLinesTest crea una ventana con un JTextArea, un JTextField y un JToggleButton:
  • El TextArea muestra información del sistema.
  • El TextField sirve para hacer pruebas con saltos de línea.
  • El botón permite conmutar el comportamiento del TextField respecto a los saltos de línea.
Puedes ver y copiar el código fuente completo en el siguiente enlace:


Las líneas que más interesan del programa están en el ItemListener del botón. Según se invoque a setDocumentFilter(DocumentFilter) con null o con un DocumentFilter válido variará el comportamiento del JTextField respecto a los saltos de línea.

Document document= textField.getDocument();
AbstractDocument abstractDocument= (AbstractDocument) document;

DocumentFilter filter;
if (button.isSelected() == true) {
    filter= new DocumentFilter();
}
else {
    filter= null;
}
                
abstractDocument.setDocumentFilter(filter);

En estas capturas se puede ver el como cambia el comportamiento y el aspecto visual del JTextField.

Monolínea
Monolínea
Multilínea
Multilínea


En ningún caso se pueden teclear saltos de línea (tecla ENTER) sobre el JTextField, pero sí se pueden pegar desde el portapapeles textos que contengan saltos de línea.

Respecto al aspecto visual del JTextField, el que se estire o no a lo alto por la presencia de saltos de línea depende de diversos factores como el LayoutManager que se esté utilizando. Si se muestran saltos de línea sin estirar el JTextField a lo alto el contenido se verá horrible.

Comentar a este respecto que con los tabuladores pasa algo parecido. Al pulsar la tecla TAB normalmente se cede el "focus" al siguiente componente, por lo que no se pueden teclear tabuladores. Sin embargo, si la String indicada en JTextField#setText(String) o el texto pegado desde el portapapeles contienen tabuladores el JTextField los mostrará.

Conclusiones


En mi humilde opinión, mientras no se cambie este comportamiento, es una característica que resulta muy útil para detectar visualmente Strings que contengan saltos de línea en JTextField que se rellenen programáticamente, por ejemplo, con datos leídos desde una base de datos que supuestamente no deberían contener saltos de línea (por eso se visualizan con un JTextField).

Enlaces


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


(Actualizado 27/11/2013)



Source code: JTextFieldLinesTest

Código fuente del programa JTextFieldLinesTest

Ver la entrada correspondiente en el siguiente enlace:


JTextField y saltos de línea



package pruebas;

import java.awt.Component;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.SwingUtilities;
import javax.swing.text.AbstractDocument;
import javax.swing.text.Document;
import javax.swing.text.DocumentFilter;

/**
 * @see http://bugs.sun.com/view_bug.do?bug_id=6427290
 */
public class JTextFieldLinesTest {

    public static String makeInfo() {
        String info= String.format(""
            + "%njava.class.version= "            + System.getProperty("java.class.version")
            + "%njava.runtime.version= "          + System.getProperty("java.runtime.version")
            + "%njava.specification.name= "       + System.getProperty("java.specification.name")
            + "%njava.specification.vendor= "     + System.getProperty("java.specification.vendor")
            + "%njava.specification.version= "    + System.getProperty("java.specification.version")
            + "%njava.version= "                  + System.getProperty("java.version")
            + "%njava.vm.info= "                  + System.getProperty("java.vm.info")
            + "%njava.vm.name= "                  + System.getProperty("java.vm.name")
            + "%njava.vm.specification.name= "    + System.getProperty("java.vm.specification.name")
            + "%njava.vm.specification.vendor= "  + System.getProperty("java.vm.specification.vendor")
            + "%njava.vm.specification.version= " + System.getProperty("java.vm.specification.version")
            + "%njava.vm.vendor= "                + System.getProperty("java.vm.vendor")
            + "%njava.vm.version= "               + System.getProperty("java.vm.version")
            + "%nos.arch= "                       + System.getProperty("os.arch")
            + "%nos.name= "                       + System.getProperty("os.name")
            + "%nos.version= "                    + System.getProperty("os.version")
            + "%n"
        );

        return info;
    }
    //--------------------------------------------------------------------------

    public static void selectAll(final JTextField aTextField) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                aTextField.selectAll();
                aTextField.requestFocusInWindow();
            }
        });
    }
    //--------------------------------------------------------------------------

    public static void example() {
        final String DEFAULT_TEXT= String.format("Line1%nLine2%nLine3%nLine4%nLine5");

        JTextArea textArea= new JTextArea();
        textArea.setText( makeInfo() );
        textArea.setEditable(false);

        final JTextField textField= new JTextField();
        textField.setEditable(true);

        String text= String.format(DEFAULT_TEXT);
        textField.setText(text);
        selectAll(textField);

        final JToggleButton button= new JToggleButton("Toogle new lines mode");

        final JPanel panel= new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
        textArea.setAlignmentX(Component.CENTER_ALIGNMENT);
        textField.setAlignmentX(Component.CENTER_ALIGNMENT);
        button.setAlignmentX(Component.CENTER_ALIGNMENT);
        panel.add(textArea);
        panel.add(new JSeparator(JSeparator.HORIZONTAL));
        panel.add(textField);
        panel.add(new JSeparator(JSeparator.HORIZONTAL));
        panel.add(button);

        final JFrame frame= new JFrame("JTextField test");
        frame.setContentPane(panel);
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        button.addItemListener(new ItemListener() {
            @Override
            public void itemStateChanged(ItemEvent e) {
                Document document= textField.getDocument();
                AbstractDocument abstractDocument= (AbstractDocument) document;

                DocumentFilter filter;
                if (button.isSelected() == true) {
                    filter= new DocumentFilter();
                }
                else {
                    filter= null;
                }
                abstractDocument.setDocumentFilter(filter);

                String text= DEFAULT_TEXT;
                textField.setText(text);
                selectAll(textField);

                panel.revalidate();
                panel.repaint();
                frame.pack();
            }
        });

        frame.setVisible(true);
    }
    //--------------------------------------------------------------------------

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                example();
            }
        });
    }
    //--------------------------------------------------------------------------

}

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;
    }
    //--------------------------------------------------------------------------

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

}

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)

Source code: InitializationOrder

Código fuente del programa InitializationOrder

Ver la entrada correspondiente en el siguiente enlace:


Java: creando objetos - Parte 2



import java.io.PrintStream;

public class InitializationOrder {

    //static attribute for counting invocations.
    private static int nextValue= 0;
    //--------------------------------------------------------------------------

    //Prefixes the argument with the current value of nextValue and increases it.
    public static String next(String aString) {
        String str= String.format("%02d%s"
            , InitializationOrder.nextValue++
            , aString
        );

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

    //Helper method.
    public static String makeString(String[] aFormats) {
        StringBuilder buffer= new StringBuilder();
        for (int i= 0; i < aFormats.length; i++) {
            buffer.append( aFormats[i] );
        }

        String str= buffer.toString();
        str= String.format(str);

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

    public static String echo(String aMessage) {
        //This sentence is useful only for tracing purposes. It can be commented.
        System.out.format("echo: %s%n", aMessage);

        return aMessage;
    }
    //--------------------------------------------------------------------------
    //--------------------------------------------------------------------------

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

        aux= new Super();
        out.format("Using Super implicit (automatic) constructor:%n%s%n", aux);
        out.format("..........%n");

        aux= new Base("FIFTH: Initialized at the end of Base(String).");
        out.format("Using Base(String):%n%s%n", aux);
        out.format("..........%n");

        aux= new Base();
        out.format("Using Base():%n%s%n", aux);
        out.format("..........%n");

        aux= new Base() {

            //Local anonymous class attributes.
            private String localFirst= echo(next("Local class instance attribute assignment."));
            private String localSecond;
            
            //Local anonymous Initialization block.
            {
                this.localSecond= echo(next("Local class initialization block."));
            }
            
            @Override
            public String toString() {
                String str= makeString(new String[]{
                    "\tlocalFirst=" + this.localFirst + "%n",
                    "\tlocalSecond=" + this.localSecond + "%n",
                });
                str= String.format("%s%s", super.toString(), str);

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

        };
        out.format("Using Anonymous Local Class extending Base():%n%s%n", aux);
        out.format("..........%n");

        out.format("%n");
    }
    //--------------------------------------------------------------------------


public static class Super {

    private String superFirst=  echo(next("FIRST: Super instance assignment."));
    private String superSecond= echo(next("SECOND: Super Instance assignment: this one is changed later."));
    
    //Initialization block 1.
    {
        this.superSecond= echo(next("SECOND: Super initialization block 1."));
    }
    //--------------------------------------------------------------------------

    //This class has no explicit constructor.

    @Override
    public String toString() {
        String str= makeString(new String[]{
            "\tsuperFirst=" + this.superFirst + "%n",
            "\tsuperSecond=" + this.superSecond + "%n",
        });
        str= String.format("%s: atributes:%n%s", this.getClass(), str);

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

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


public static class Base extends Super {

    //First of all, all the attributes are initialized with their default value.
    //0 for int, false for boolean, null for object referencies, etc.
    private String basePreinitialized;//In this case, the default value is null.
    private String baseFirst=  echo(next("FIRST: Base instance assignment."));
    private String baseSecond;

    //Initialization block 1.
    {
        this.baseSecond= echo(next("SECOND: Base initialization block 1."));
        this.baseThird= echo(next("SECOND: Base initialization block 1. This one is changed later."));
    }

    //Initialization block 2.
    {
        this.baseThird= echo(next("THIRD: Base initialization block 2."));
    }

    //Initialization block 3.
    {
        this.baseFourth= echo(next("FOURTH: Base initialization block 3. This one is changed later."));
    }

    private String baseThird;
    private String baseFourth= echo(next("FOURTH: Base instance assignment."));

    private String baseFifth= echo(next("FIFTH: Base instance assignment. This one is changed later."));
    //--------------------------------------------------------------------------

    public Base(String aFifth) {
        super();
        
        this.baseFifth= aFifth;
        echo("End of Base(String)");
    }
    //--------------------------------------------------------------------------

    public Base() {
        this( next("Modified at the end of Base().") );

        this.baseFifth= next("FIFTH: " + this.baseFifth);
        echo("End of Base()");
    }
    //--------------------------------------------------------------------------
    //--------------------------------------------------------------------------

    @Override
    public String toString() {
        String str= makeString(new String[]{
            "\tbasePreinitialized=" + this.basePreinitialized + "%n",
            "\tbaseFirst=" + this.baseFirst + "%n",
            "\tbaseSecond=" + this.baseSecond + "%n",
            "\tbaseThird=" + this.baseThird + "%n",
            "\tbaseFourth=" + this.baseFourth + "%n",
            "\tbaseFifth=" + this.baseFifth + "%n",
        });
        str= String.format("%s%s", super.toString(), str);

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

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

}//End of InitializationOrder.

sábado, 31 de agosto de 2013

Java: creando objetos - Parte 1

Introducción


Los constructores en Java tienen ciertas características en las que no solemos reparar y que conviene tener en cuenta. No hacerlo, puede dar resultados inesperados e incluso llevar al programador a cometer un error que es muy difícil de encontrar (y posiblemente solucionar) del que espero hablar en un artículo venidero:

¿Has visto alguna vez el mensaje "Constructor calls overridable method"?

Pero vayamos poco a poco. Entendamos primero como funcionan los constructores.

La especificación técnica de todo lo que explico aquí está en el siguiente enlace:


Para no espantar al lector con un artículo inmenso, ja ja ja, he decido separar este artículo en varios artículos pequeños, tratando de centrar cada uno de ellos en explicar una característica de la creación de objetos.

Algoritmo general de creación de objetos


Al crear un nuevo objeto, una vez que se ha reservado memoria suficiente para todos los atributos de la clase (y todos los atributos de sus superclases, incluidos los campos ocultados), se realizan los siguientes pasos:

  1. Preinicialización de todos los atributos de la clase y todas sus superclases a sus valores por defecto.
  2. Evaluación de los argumentos, si los hay, que se vayan a pasar al otro constructor.
  3. Invocación al otro constructor (hasta llegar a Object): un constructor de la clase base u otro de la propia clase.
  4. Asignaciones a atributos y bloques de inicialización en estricto orden de aparición (Left - Right).
  5. Resto de instrucciones del cuerpo del constructor.
Esos pasos se ejecutan recursivamente hasta llegar a la clase Object, clase cuyo único constructor no tiene otro constructor al que invocar.

Recursión de constructores


El programador invoca explícitamente uno de los constructores de la clase.

La primera instrucción de un constructor (al menos a la vista del programador, puesto que la preinicialización ya se ha ejecutado) es siempre una invocación a otro constructor:

  • Un constructor de la clase base (salvo en el caso de Object, que no hereda de nadie). Muchas veces, esta sentencia la pone el compilador por nosotros, invocando implícitamente al constructor sin argumentos de la clase base (si existe, si no, se producirá un error de compilación).
  • Otro constructor de la propia clase.

Lo primero que ocurre, es que si la invocación al otro constructor lleva argumentos, se evalúan dichos argumentos. En este punto, aún no se pueden usar referencias a this ni a super. Es decir, que el cálculo de los argumentos no puede depender del objeto en construcción.

Al retornar del otro constructor, se continúa ejecutando el cuerpo del constructor actual. En este punto, ya se pueden utilizar referencias a this y a super.

Ejemplo


El pequeño programa ConstructorRecursion sirve para ilustrar la recursión de constructores. Además, como comentarios, hay código alternativo que fallaría por no ser válido en ese punto. Puedes ver y copiar el código fuente en el siguiente enlace:


La salida del programa es la siguiente:

echoAndPrint: No arguments
echoAndPrint: aValue= No arguments
echoAndPrint: Root constructor
echoAndPrint: Initial value: null
echoAndPrint: aValue= No arguments
A derived object: pruebas.ConstructorRecursion$Derived@a62fc3 variable<aValue= No arguments> constant=<Constant attribute.>

Se invoca la siguiente cadena de constructores:

Derived()
Derived(String)
Base(String)
Root()
Object()

Obsérvese como curiosidad que al inicializar el atributo variable uso su propio valor como parte de la inicialización. Esto es posible porque en ese punto ya se puede referenciar a this y super y además el atributo ha sido preinicializado a null.

Enlaces


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


(Actualizado 02/09/2013)

Source code: ConstructorRecursion

Código fuente del programa ConstructorRecursion

Ver la entrada correspondiente en el siguiente enlace:


Java: creando objetos - Parte1



package pruebas;

import java.io.PrintStream;

public class ConstructorRecursion {
    
    public static String echoAndPrint(String aMessage) {
        System.out.format("echoAndPrint: %s%n", aMessage);

        return aMessage;
    }

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

        Derived aux= new Derived();
        out.format("A derived object: %s%n", aux);
    }


public static class Root {

    final protected String constant= "Constant attribute.";

    public Root() {
        //Compiler calls super() automatically.
        echoAndPrint("Root constructor");
    }
}

    
public static class Base extends Root {

    //Yes, I'm using the own attribute value to initialize it.
    private String variable= echoAndPrint("Initial value: " + this.variable);
    
    public Base(String aValue) {
        //Compiler calls super() automatically.
        this.variable= echoAndPrint(aValue);
    }
    
    @Override
    public String toString() {
        String str= String.format("%s variable<%s> constant=<%s>"
            , super.toString()
            , this.variable
            , this.constant
        );

        return str;
    }
}


public static class Derived extends Base {

    private String extra;

    public Derived(String aValue) {
        super( echoAndPrint("aValue= " + aValue) );
    }
    
    public Derived() {
        this( echoAndPrint("No arguments") );
        //this( super.constant );//Error: cannot reference this before supertype constructor has been called
        //this( this.extra );//Error: cannot reference this before supertype constructor has been called
    }
}

}//End ConstructorRecursion