domingo, 26 de mayo de 2013

¿Se pueden crear objetos en Java sin pasar por ningún constructor?

¡Atención pregunta!


¿Te has planteado alguna vez si es posible instanciar objetos en Java sin que se ejecute ninguno de los constructores de la clase correspondiente?
Digamos por ejemplo, para hacer la burrada de contar las veces que se instancia una clase.

Supongo que esta pregunta y su respuesta se pueden aplicar a otros lenguajes de programación, pero voy a centrarme en Java.

Insultando ligeramente tu inteligencia, ja ja ja, te recuerdo que para instanciar un objeto en Java se utiliza el operador new, que permite invocar uno de los constructores de la clase:


Existe otra forma de invocar uno de los constructores de una clase: mediante Reflection. Pero vamos, eso sigue siendo invocar a un constructor, aunque sea de un modo un tanto rebuscado:


Y también hay que decir que ciertos objetos, digamos especiales, se pueden instanciar usando literales:
  • Strings. Por ejemplo:
  • String aux= "¡Hola Mundo!";
    
    ¿Qué sería un post de programación sin que apareciera esa frase?, ja ja ja.
  • Arrays. Por ejemplo:
  • int[] aux= {0,1,2,3,4,5,6,7,8,9};
    
    Recordar que el constructor de un Array tiene una sintaxis distinta al resto.
  • Primitive Wrappers: Boolean, Character, Byte, Short, Integer, Float y Double.
    A partir de Java 5, el autoboxing permite poner literales al declarar variables de estos tipos. Por ejemplo:
  • Integer aux= 1234;
    
En estos casos también se invoca un constructor, pero el código correspondiente lo genera el compilador. Eso sí, no tiene por qué invocarse un constructor para cada literal (ver por ejemplo String#intern() y Integer#valueOf(int)).

Ahora, antes de seguir leyendo, pregúntate a tí mismo si crees que existe alguna manera de crear objetos sin pasar por un constructor.

Y la respuesta es...


Sí, sí se pueden crear objetos sin invocar ningún constructor. ¿Cómo?

Pues existen al menos 2 formas de crear objetos sin pasar por uno de los constructores de la clase:

  1. Clonando un objeto mediante el método Object#clone() sin romper el contrato de redefinición que requiere invocar al método de la clase base. En este caso, el método Object#clone() creará una copia superficial (Shallow copy) del objeto sin pasar por un constructor.
    MyClass instance1= new MyClass();
    MyClass instance2= (MyClass) instance1.clone();
    

  2. Deserializando un objeto utilizando la interface java.io.Serializable (The Java tutorial: Serializable objects). En este caso, el objeto deserializado se creará en memoria sin pasar por un constructor.
    ...
    ObjectInputStream ois= ...;
    Object deserialized= (Serializable) ois.readObject();
    

    Bueno, en realidad esto no es del todo cierto, ya que depende de que todas las superclases de la clase a deserializar implementen o no Serializable (a excepción de la clase raíz Object), como se indica en su documentación:
(...) To allow subtypes of non-serializable classes to be serialized, the subtype may assume responsibility for saving and restoring the state of the supertype's public, protected, and (if accessible) package fields. The subtype may assume this responsibility only if the class it extends has an accessible no-arg constructor to initialize the class's state. It is an error to declare a class Serializable if this is not the case. The error will be detected at runtime. (...)

Sí sí sí, muy bonito, pero demuéstramelo


He escrito el pequeño programa ObjectCreationTest con el código necesario para demostrar el rollo teórico que acabas de leer. Más abajo hay un enlace al código fuente completo. Aquí me limito a describir lo que contiene el código fuente y lo que hace el programa:

  • Se define la clase Base, en la que cambiando una línea se puede hacer que implemente java.io.Serializable o no.
  • Se define la clase MySerializable, que deriva de Base y que sí implementa java.io.Serializable. Se trata de una clase Cloneable. Sólo tiene un constructor que requiere un argumento de tipo String, de tal forma que no tiene ningún constructor sin argumentos. Además, en el constructor se incrementa el atributo static INSTANCE_COUNT, para tratar de contabilizar las veces que se instancia la clase.
  • Se define el método serializeAndDeserialize(Serializable), que serializa un objeto sobre un byte array y devuelve el resultado de deserializar dicho byte array.
  • El método main(String[]) crea objetos con los diversos métodos explicados y demuestra que se trata de objetos diferentes imprimiendo su IdentityHashCode y comparando algunas de las referencias a los objetos usando el operador ==.

Por simplicidad, he declarado Base y MySerializable como static nested classes, de modo que sólo hace falta un fichero '.java', en vez de necesitar uno para cada clase.

Las líneas más reseñables del programa son las siguientes:


public class ObjectCreationTest {

    public static Serializable serializeAndDeserialize(Serializable aSerializable) throws IOException, ClassNotFoundException {
        ...
    }
    //--------------------------------------------------------------------------
    //--------------------------------------------------------------------------
    
    public static void main(String[] args) {
        try {
            OUT.println("Start");
            long antes= System.currentTimeMillis();
            
            OUT.println("...new object...");
            MySerializable normal= new MySerializable("I was created at main(...) with new.");
            OUT.println("...clone()...");
            MySerializable cloned= (MySerializable) normal.clone();
            OUT.println("...serialize and deserialize...");
            MySerializable serialized= (MySerializable) serializeAndDeserialize(normal);
            OUT.println("...reflection...");
            //In this case Class.newInstance() cannot be used because MySerializable doesn't have a zero-argument constructor.
            Constructor<MySerializable> constructor= MySerializable.class.getConstructor(String.class);
            MySerializable reflect= constructor.newInstance("I was created at main(...) with Reflection.");
            OUT.println();
            
            OUT.println("...results...");
            OUT.format("normal=     %s%n", normal);
            OUT.format("cloned=     %s%n", cloned);
            OUT.format("serialized= %s%n", serialized);
            OUT.format("reflect=    %s%n", reflect);
            OUT.format("There are 4 diferent instances.%n");

            OUT.println();
            OUT.format("(normal == cloned)?     %s%n", (normal == cloned));
            OUT.format("(normal == serialized)? %s%n", (normal == cloned));
            OUT.format("(cloned == serialized)? %s%n", (normal == cloned));
            
            OUT.println();
            OUT.format("MySerializable.INSTANCE_COUNT= %s%n", MySerializable.INSTANCE_COUNT);
            
            OUT.println();
            OUT.format("End. %sms%n", System.currentTimeMillis()-antes);
        }
        catch (Exception ex) {
            ex.printStackTrace();
            System.exit(1);
        }
    }
    //--------------------------------------------------------------------------
    //--------------------------------------------------------------------------
    
public static class Base {                  //Se invoca al constructor sin argumentos al deserializar. El atributo <name> será establecido en dicho constructor.
///public static class Base implements Serializable {//No se invoca ningún constructor al deserializar.

    /** Sólo se puede, y se debe establecer desde un constructor/inicializador. */
    final private String name;
    //--------------------------------------------------------------------------

    public Base(String aName) {
        ...
        this.name= aName;
    }
    //--------------------------------------------------------------------------

    public Base() {
        this("Default constructor required for deserialization.");
    }
    //--------------------------------------------------------------------------
    //--------------------------------------------------------------------------

    ...

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

public static class MySerializable extends Base implements Serializable, Cloneable {

    private static final long serialVersionUID= 1L;
    public static int INSTANCE_COUNT= 0;
    //--------------------------------------------------------------------------

    public MySerializable(String aName) {
        super(aName);

        INSTANCE_COUNT++;
        ...
    }
    //--------------------------------------------------------------------------
    //--------------------------------------------------------------------------

    @Override
    public Object clone()throws CloneNotSupportedException {
        return super.clone();
    }
    //--------------------------------------------------------------------------

    ...

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

Atendiendo a los mensajes de texto que se imprimen, se puede comprobar que se generan instancias diferentes de la clase MySerializable (basta con comprobar que los objetos tienen diferente IdentityHashCode).

También, los mensajes de texto imprimidos permiten saber por qué constructores se pasa y por cuales no.

Haciendo que la clase Base implemente java.io.Serializable se puede comprobar que al deserializar no se invoca a ninguno de los constructores de la clase Base ni de la clase MySerializable.

Sin embargo, haciendo que la clase Base no implemente java.io.Serializable se puede comprobar que se invoca al constructor sin argumentos de la clase Base. De hecho, se verá que el atributo Base#name de la instancia deserializada tendrá el valor establecido por dicho constructor sin argumentos.

Comentando el constructor sin argumentos (o poniéndole un argumento de cualquier tipo que no sea String) se puede comprobar que al deserializar se lanza una excepción del tipo java.io.InvalidClassException indicando algo parecido a:

java.io.InvalidClassException: pruebas.ObjectCreationTest$MySerializable; pruebas.ObjectCreationTest$MySerializable; no valid constructor


Código fuente


Puedes ver y copiar el código fuente completo del programa ObjectCreationTest en el siguiente enlace:


No olvides corregir el package para evitar problemas en caso de abrir el fichero desde un IDE.

Conclusiones


Hace tiempo, descubrí esta curiosidad buscando información sobre algo que ya no recuerdo. Recientemente otras cuestiones me lo recordaron y me ha dado por compartir estos conocimientos. Además, no sé si estará bien explicado por ahí en castellano, porque la verdad, no me he molestado en buscar.

Quien sabe, tal vez un día te resulte útil saberlo.

Muchas gracias por leerlo y espero que te haya parecido interesante, y a ser posible, también entretenido.

Cualquier comentario será bienvenido, bueno, al menos aquellos que sean constructivos, ja ja ja.

Enlaces de interés


Enlaces a artículos, documentación y herramientas en los que me he basado para escribir este post, y también referencias a algunos proyectos interesantes relacionados con el tema.

Dar las gracias desde aquí a los autores de las citadas referencias por compartir su conocimiento con el resto del mundo.

(Actualizado 31/08/2013)

Source Code: ObjectCreationTest

Código fuente del programa ObjectCreationTest

Ver la entrada correspondiente en el siguiente enlace:


¿Se pueden crear objetos en Java sin pasar por ningún constructor?


import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;

/**
 * @see http://docs.oracle.com/javase/6/docs/api/java/io/Serializable.html
 * @see http://docs.oracle.com/javase/6/docs/api/java/lang/Object.html#clone%28%29
 * 
 * @see http://stackoverflow.com/questions/3488097/is-it-possible-to-create-an-instance-of-an-object-in-java-without-calling-the-co
 * @see http://www.javaspecialists.eu/archive/Issue175.html
 * 
 * @see http://objenesis.googlecode.com/svn/docs/index.html
 */
public class ObjectCreationTest {

    /** Puesto por comodidad. */
    private static final PrintStream OUT= System.out;
    //--------------------------------------------------------------------------
    
    /**
     * Serializa y deserializa un objeto, devolviendo el objeto resultante
     * de la deserialización.
     * 
     * @param aSerializable
     * 
     * @return
     * 
     * @throws IOException
     * @throws ClassNotFoundException 
     */
    public static Serializable serializeAndDeserialize(Serializable aSerializable) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos= null;
        ObjectOutputStream oos= null;
        ByteArrayInputStream bis= null;
        ObjectInputStream ois= null;
        
        try {
            if (aSerializable == null) {
                throw new IllegalArgumentException("Cannot Serialize/Deserialize a null object.");
            }

            //Se serializa el objeto en memoria sobr e un byte[].
            bos= new ByteArrayOutputStream();
            oos= new ObjectOutputStream(bos);
            oos.writeObject(aSerializable);

            byte[] byte_s= bos.toByteArray();

            //Se deserializa el byte[] sobre un nuevo objeto.
            bis= new ByteArrayInputStream(byte_s);
            ois= new ObjectInputStream(bis);
            Serializable deserialized= (Serializable) ois.readObject();

            return deserialized;
        }
        finally {
            if (bos != null) {
                bos.close();
            }
            if (oos != null) {
                oos.close();
            }
            if (bis != null) {
                bis.close();
            }
            if (ois != null) {
                ois.close();
            }
        }
    }
    //--------------------------------------------------------------------------
    //--------------------------------------------------------------------------
    
    /**
     * Programa de prueba.
     * 
     * @param args 
     */
    public static void main(String[] args) {
        try {
            OUT.println("Start");
            long antes= System.currentTimeMillis();
            
            OUT.println("...new object...");
            MySerializable normal= new MySerializable("I was created at main(...) with new.");
            OUT.println("...clone()...");
            MySerializable cloned= (MySerializable) normal.clone();
            OUT.println("...serialize and deserialize...");
            MySerializable serialized= (MySerializable) serializeAndDeserialize(normal);
            OUT.println("...reflection...");
            //In this case Class.newInstance() cannot be used because MySerializable doesn't have a zero-argument constructor.
            Constructor<MySerializable> constructor= MySerializable.class.getConstructor(String.class);
            MySerializable reflect= constructor.newInstance("I was created at main(...) with Reflection.");
            OUT.println();
            
            OUT.println("...results...");
            OUT.format("normal=     %s%n", normal);
            OUT.format("cloned=     %s%n", cloned);
            OUT.format("serialized= %s%n", serialized);
            OUT.format("reflect=    %s%n", reflect);
            OUT.format("There are 4 diferent instances.%n");

            OUT.println();
            OUT.format("(normal == cloned)?     %s%n", (normal == cloned));
            OUT.format("(normal == serialized)? %s%n", (normal == cloned));
            OUT.format("(cloned == serialized)? %s%n", (normal == cloned));
            
            OUT.println();
            OUT.format("MySerializable.INSTANCE_COUNT= %s%n", MySerializable.INSTANCE_COUNT);
            
            OUT.println();
            OUT.format("End. %sms%n", System.currentTimeMillis()-antes);
        }
        catch (Exception ex) {
            ex.printStackTrace();
            System.exit(1);
        }
    }
    //--------------------------------------------------------------------------
    //--------------------------------------------------------------------------
    
/**
 * Superclase de la clase a serializar/deserializar.
 */
public static class Base {                  //Se invoca al constructor sin argumentos al deserializar. El atributo <name> será establecido en dicho constructor.
///public static class Base implements Serializable {//No se invoca ningún constructor al deserializar.

    /** Sólo se puede, y se debe establecer desde un constructor/inicializador. */
    final private String name;
    //--------------------------------------------------------------------------

    public Base(String aName) {
        super();

        OUT.format("Base(String)#Creating a %s. aName<%s>%n"
            , this.getClass()
            , aName
        );

        this.name= aName;
    }
    //--------------------------------------------------------------------------

    /**
     * Este es requerido si la clase no implementa Serialized pero la subclase sí.
     */
    public Base() {
        this("Default constructor required for deserialization.");

        OUT.format("Base()#Creating a %s.%n"
            , this.getClass()
        );
    }
    //--------------------------------------------------------------------------
    //--------------------------------------------------------------------------

    @Override
    public String toString() {
        String str= String.format("Name<%s>."
            , this.name
        );

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

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

/**
 * Clase que se va a Serializar/Deserializar en el programa de prueba.
 */
public static class MySerializable extends Base implements Serializable, Cloneable {

    /** Recomendación de la interface java.io.Serializable */
    private static final long serialVersionUID= 1L;

    /** Número de instancias construídas de esta clase. */
    public static int INSTANCE_COUNT= 0;
    //--------------------------------------------------------------------------

    public MySerializable(String aName) {
        super(aName);

        INSTANCE_COUNT++;
        OUT.format("MySerializable(String)#Creating a %s. INSTANCE_COUNT=%s%n"
            , this.getClass()
            , INSTANCE_COUNT
        );
    }
    //--------------------------------------------------------------------------
    //--------------------------------------------------------------------------

    /**
     * Hay que convertir el método clone de protected a public. 
     */
    @Override
    public Object clone()throws CloneNotSupportedException {
        return super.clone();
    }
    //--------------------------------------------------------------------------

    @Override
    public String toString() {
        String str= String.format("Instance of %s with IdentityHashCode<%s> %s"
            , this.getClass()
            , System.identityHashCode(this)
            , super.toString()
        );

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

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