Constructores e Instanciación en Java

Buenas tardes, en esta entrada vamos a desarrollar constructores para una o mas clases, explicar los efectos de modificadores en la herencia en lo que se refiere a constructores y desarrollar código que declare y/o invoque métodos sustituidos (Override) o sobrecargados (Overload).


Los objetos son cosntruidos. No podemos hacer un nuevo objeto sin antes invocar su constructor. En realidad no podemos hacer un nuevo objeto sin invocar no solo el constructor del tipo de clase del objeto actual, pero tambien el constructor de cada una de sus superclases. Los constructores son el código que se ejecuta allí donde usemos la palabra new.

  • Fundamentos del Constructor

    Cada clase, incluidas las clases abstractas, DEBEN tener un constructor. Grabemos esto en nuestra mente. Pero solo porque la clase debe tener uno, no significa que el programador no tenga que hacerlo. Un constructor es así:

    		class Foo{
    			Foo(){ } // Constructor para la clase Foo
    		

    Como podemos ver, no hay tipo de retorno. Dos closas que tenemos que tener claras a recordar sobre los constructores es que no tienen tipo de retorno y sus nombres deben coincidir exactamente con el nombre de la clase. Tipicamente, los contructores son usados para inicializar el estado de las variables de instancia, como el siguiente ejemplo:

    		public class Foo {
    		    int size;
    		    String name;
    		    Foo(String name, int size){
    		        this.name = name;
    		        this.size = size;
    		    }
    		}
    		

    En el ejemplo del código anterior, la calse Foo no tiene ningún constructor que no tenga argumentos. Esto significa que el siguiente código fallará al compilar:

    		Foo f = new Foo();
    		

    Pero lo siguiente compilará:

    		Foo f = new Foo("Fred", 43);
    		

    Por lo que es muy común para una clase tener un constructor el cual no tenga argumentos, sin importar cuandos de otros constructores sobrecargados existan en la clase(Los constructores tambien pueden ser sobrecargados). No podemos hacer que siempre funcione en nuestras clases, ocasionalmente tenemos una clase donde no vemos el sentido de crear una instancia sin pasar información al constructor. El objeto java.awt.Color, por ejemplo, no puede ser creado con una llamada sin argumentos al constructor, porque esto sería como decirle a la JVM, “Hazme un objeto Color nuevo, y realmente me trae sin cuidado que color sea…tu eliges”. Será mejor que no le dejáramos a al JVM escoger el estilo.

  • Encadenación de Constructores

    Sabemos que los constructores son invocados en tiempo de ejecución cuando escribimos la palabra new en alguna tipo de clase como el siguiente:

    		Horse h = new Horse();
    		

    Pero ¿Qué está realmente pasando cuando decimos new Horse()? (Asumimos que Horse hereda de Animal y Animal hereda de Object):

    1. El constructor de Horse es invocado. Cada constructor invoda al constructor de su superclase son una llamada implícita a super(), a menos que el constructor invoque un constructor sobrecargado de la misma clase.

    2. Se invoca el constructor de Animal.

    3. Se invoca el constructor de Object (Object es la superclase de todas las clases, por lo que la clase Animal hereda de Object aunque no hemos escrito “extends Object” en la declaración de la clase Animal. Es implícito). En este punto estamos en el punto mas alto del montón.

    4. Las variables de instancia de Object se dan sus valores explícitos. Por valores explícitos, nos referimos a valores que son asignados al mismo tiempo que las variables son declaradas.

    5. El constructor de Object se completa.

    6. Las variables de instancia de Animal son dadas sus valores explícitos (si hay alguno).

    7. Se termina el constructor de Animal.

    8. A las variables de instancia de Horse se les dán sus valores explícitos (si hay alguno).

    9. Se completa el constructor de Horse.

    4. Object()
    3. Animal () calls super()
    2. Horse() calls super()
    1. main() calls new Horse()
  • Reglas para los constructores

    La siguiente lista muestra las reglas que tenemos que conocer. DEBEMOS recordar esto siempre.

    • Los constructores pueden usar cualquier modificador de acceso, incluyendo private. (Un constructor privado significa que solo el código dentro de la clase por sí mismo puede instanciar un objeto de ese tipo, por lo que si el constructor privado de la clase quiere permitir a una clase instanciar lo para ser usado, la clase debe proveer un método estático o una variable que permita el acceso a la instancia a ser creada dentro de la clase).

    • El nombre del constructor debe siempre coincidir con el nombre de la clase.

    • El constructor no debe tener tipo de retorno.

    • Es legal (pero estúpido) tener un método con el mismo nombre que la clase, pero no hacerlo constructor. Si vemos un tipo de retorno, es un método en vez de un constructor. De hecho, podemos tener ambos, método y constructor con el mismo nombre en la misma clase y no ser un problema para Java.

    • Si no escribimos ningún constructor en nuestro código de la clase, un constructor por defecto será automáticamente generado por el compilador.

    • El constructor por defecto es SIEMPRE un constructor sin argumentos.

    • Si queremos un constructor sin argumentos y hemos escrito cualquier otro constructor en el código de neustra clase, el compilador no proveerá un constructor sin argumentos.

    • Cada constructor tiene, en su primera sentencia, una llamada a un constructor sobrecargado (this() o una llamada a su constructor de la superclase (super()).

    • Si no escribimos un constructor, y no queremos crear una llamada a super() o a this(), el compilador insertará una llamada sin argumentos a super() por nosotros, como la primera sentencia en el constructor.

    • Una llamada a super() puede ser sin argumentos o pueden incluir argumentos pasados al constructor de su superclase.

    • Un constructor sin argumentos no tiene porque ser necesariamente el constructor por defecto, aunque el constructor por defecto es siempre un constructor sin argumentos.

    • No podemos hacer una llamada a un método de instancia, o acceder a una variable de instancia hasta que el constructor de la superclase se ejecute.

    • Solo las variables estáticas y los métodos estáticos pueden ser accedidos como parte de la llamada a super() o this().

    • Las clases abstractas tienen constructores, y estos constructores son siempre llamados cuando una subclase concreta es instanciada.

    • Las Interfaces no tienen constructores. Las interfaces no forman parte del arbol de herencia de un objeto.

    • La única manera de invocar a un constructor puede ser desde dentro de otro constructor. En otras palabras, no podemos escribir código que actualmente llame a un costructor como lo siguiente:

      class Horse{
      Horse(){ } // Constructor
      void doStuff(){
      Horse(); // Llamada al constructor, ILEGAL
      }
      }

  • Determinar cuando un Constructor por defecto será creado

    El siguiente ejemplo muestra una clase Horse con 2 constructores:

    		class Horse{
    		    Horse(){ } 
    		    Horse(String name){ }
    		}
    		

    ¿Pondrá el compilador el constructor por defecto en la clase de arriba? NO! ¿Y en la siguiente variación de la clase?:

    		class Horse{
    			Horse(String name){ }
    		}
    		

    ¿Podrá el compilador insertar un constructor por defecto? NO!. ¿Y en esta clase?:

    		class Horse{ }
    		

    Ahora sí. El compilador generará un constructor por defecto para la clase anterior, porque esa clase no tiene ningún constructor definido. ¿Y en la siguiente clase?:

    		class Horse{
    			void Horse(){ }
    		}
    		

    Puede parecer que el compilador no creará uno, ya que hay un constructor en la clase Horse. Miremos de momento a la clase Horse anterior. ¿Qué está mal en el constructor de la clase Horse? No es un constructor!, es simplemente un método pero que tiene el mismo nombre que la clase. Recordemos, el tipo de retorno es un simbolo de que estamos trabajando en un método y no en un constructor.

    ¿Cuando sabemos seguro que un constructor por defecto será creado?

    Siempre y cuando no escribamos ningún constructor en nuestra clase.

    ¿Como sabremos a lo que se parecerá un constructor por defecto?

    • El constructor por defecto tiene el mismo modificador de acceso que la clase.
    • El constructor por defecto no tiene argumentos.
    • El constructor por defecto incluye una llamada sin argumentos al constructor de su superclase (super()).

    ¿Qué pasa si el constructor de super tiene argumentos?

    Los constructores pueden tener argumentos tanto como los métodos, y si intentamos invocar un método que coge, por ejemplo, un int, pero no queremos pasar nada al método, el compilador fallará como en el siguiente ejemplo:

    		class Bar{
    		    void takeInt(int x){ }
    		}
    		
    		class UseBar{
    		    public static void main (String[] args){
    		        Bar b = new Bar();
    		        b.takeInt();
    		    }
    		}
    		

    El compilador se quejará de que no podemos invocar el método takeInt() sin pasar un int como argumento.

    Código de la clase Código del constructor generado por el compilador(en negrita)
    class Foo{ } class Foo {
    Foo() {
    super();
    }

    }
    class Foo {
    Foo(){ }
    }
    class Foo{
    Foo() {
    super();
    }
    }
    public class Foo() public class Foo{
    public Foo(){
    super();
    }

    }
    class Foo() {
    Foo(String s){ }
    }
    class Foo{
    Foo(String s){
    super();
    }
    }
    class Foo{
    Foo(String s){
    super();
    }
    }
    Nada, el compilador no necesita insertar nada.
    class Foo{
    void Foo(){ }
    }
    class Foo{
    void Foo() { }
    Foo(){
    super();
    }

    }
    void Foo() es un método, no un constructor.

    Por lo que si nuestro super construtor (Que es el constructor de la superclase inmediata) tiene argumentos, debemos escribir la llamada a super(), añadiendo los argumentos apropiados. El punto crucial es: Si una superclase no tiene constructor sin argumentos, debemos escribir un constructor en nuestra clase (la subclase) ya que necesitamos donde poner la llamada a super con los argumentos apropiados.

    Lo siguiente es un ejemplo del problema:

    		class Animal {
    			Animal (String name){ }
    		}
    		
    		class Horse extends Animal {
    			Horse(){
    				super(); // PROBLEMA
    			}
    		}
    		

    Y una vez mas el compilador nos diría que no puede ser. El problemaes que estamos intentando invocar el constructor de la super clase con super() sin argumentos, cuando el constructor de la superclase tiene argumentos.

    Otra manera de poner esto es que si tenemos una superclase que no tiene constructor sin argumentos, entonces en nustra subclase no podremos usar un constructor por defecto proveído por el compilador. Es simple, ya que el compilador solo puede poner una llamada sin argumentos de super(), no podremos nunca compilar lo siguiente:

    		class Clothing{
    			Clothing(String s){ }
    		}
    		
    		class TShirt extends Clothing { }
    		

    Intentando compilar este código tendremos exactamente el mismo error que cuando pusimos un constructor en la subclase con una llamada a la versión sin argumentos de super().

    De hecho, el código anterior de Clothing y TShirt es implícitamente lo mismo que el siguiente que pondremos, hemos dado un constructor a TShirt que es idéntico al constructor por defecto que nos dá el compilador.

    		class Clothing {
    		    Clothing (String s){}
    		}
    		
    		class TShirt extends Clothing{
    		        // Constructor idéntico al dado por el compilador
    		    TShirt(){
    		        super(); // No funcionará
    		                 //Invoca un constructor Clothing() sin argumentos
    		                 // pero no hay ninguno
    		    }
    		}
    		

    Un último punto en el constructor por defecto ( y es probablemente muy obvio) Los constructores NUNCA son heredados.No son métodos, no pueden ser sustituidos. Por lo que el tipo de constructor de la superclase no tiene una manera de determinar el tipo de constructor que obtendremos. Alguna gentepor error creerán que el constructor por defecto de alguna manera coincide con el super constructor.

  • Constructores Sobrecargados (Overload)

    Sobrecargar un constructor significa escribir multiples versiones del constructor, cada uno teniendo una lista de argumentos diferentes, como los siguientes ejemplos:

    		class Foo {
    		    Foo() {}
    		    Foo (String s){ }
    		}
    		

    La clase anterior Foo tiene 2 constructores sobrecargados, uno que toma un String, y otro sin argumentos. Ya que no hay código en la versión sin argumentos, es actualmente idéntico al constructor por defecto que el compilador nos ofrece, pero recordemos que desde que hay un constructor en esta clase (el que coge el String), el compilador no nos ofrecerá el constructor por defecto. Si queremos un constructor sin argumentos para sobrecargarlo con una versión con argumentos que ya tenemos, vamos a tener que escribir uno por nosotros mismos, como en el anterior ejemplo de Foo.

    Sobrecargar un constructor es típicamente usado para proveer alternativas a los clientes para instanciar objetos de nustras clases. Si un cliente conoce el nombre de un animal por ejemplo, el puede pasar al constructor de la clase Animal que coge un String el nombre. Pero si no conoce el nombre, el cliente puede llamar a la versión sin argumentos y entonces ese constructor puede proveer un nombre por defecto. Se parecería a lo siguiente:

    		public class Animal {
    		    String name;
    		    Animal (String name){
    		        this.name = name;
    		    }
    		    
    		    Animal(){
    		        this(makeRandomName());
    		    }
    		    
    		    static String makeRandomName(){
    		        int x = (int) (Math.random() * 5);
    		        String name = new String[] {"Fluffy", "Fido", "Rover", "Spike", "Gigi"}[x];
    		        
    		        return name;
    		    }
    		    
    		    public static void main (String[] args){
    		        Animal a = new Animal();
    		        System.out.println(a.name);
    		        Animal b = new Animal("Zeus");
    		        System.out.println(b.name);
    		    }
    		}
    		

    Si ejecutamos esta clase obtendremos 2 nombres aleatorios por ejecución.

    El pubto clave que tenemos qeu coger de este código de ejemplo está en l alínea 8. En vez de llamar a super(), estamos llamando a this(), y this() siempre significa una llamada a otro constructor en la misma clase. Ok, bien, ¿Qué pasa despues de la llamada a this()? Tarde o temprano el constructor super() será llamado. Una llamada a this() significa que estamos retrasando lo inevitable. Algún constructor, en algún lugar, debe llamar a super().

    La primera línea en un constructor debe ser una llamada a super() o una llamada a this().

    No hay excepciones. Si no tenemos ningunas de estas llamadas en nuestro constructor, el compilador insertará la versión sin argumentos de super(). En otras palabras, si un constructor A() tiene una llamada a this(), el compilador sabe que el constructor A() no será el que invoque a super().

    La regla anterior significa que un constructor nunca puede tener ambas llamadas super() y this(). Porque cada una de estas llamadas debe ser la primera sentencia en el constructor, y no podemos usar ambos legalmente en el mismo constructor. Esto también significa que el compilador no pondrá una llamada a super() en cualquier constructor que ya tenga una llamada a this().

    Pensemos en lo siguiente: ¿Que crees que pasará si intentamos compilar el siguiente código?

    		class A{
    		        A(){
    		            this("foo");
    		        }
    		        A(String s){
    		            this();
    		        }
    		}
    		

    Nuestro compilador no cogerá el problema (aunque depende de nustro compilador, pero la mayoría no suele cogerlo). Asume que sabemos lo que estamos haciendo. Dado que el super constructor debe siempre ser lamado ¿Dónde iría la llamada a super()? Recordemos, el compilador no pondrá un constructor por defecto si ya tenemos actualmente uno o mas constructores en nuestra clase. Y cuando el compilador no pone un constructor por defecto, todabía inserta una llamada a super() en cualquier constructor que no tenga explícitamente una llamada al super constructor. Por lo que en el código anterior, ¿Dónde iria la llamada a super()? Ambos constructores en la clase tienen llamadas a this(), y de hecho obtendremos exactamente lo que queremos si escribimos el siguiente código:

    public void go(){
    doStuff();
    }

    public void doStuff(){
    go();
    }

    ¿Vemos el fallo ahora?. El stack explota!. La pila de llamadas va en aumento continuamente hasta que se llena y la aplicación acaba con un error de java.lang.StackOverflowError. Dos constructores sobrecargados los cuales llaman a this() son 2 constructores llamandose continuamente entre ellos.

    El beneficio de tener constructores sobrecargados es que podemos ofrecer diferentes maneras de instanciar objetos desde nuestra clase. El beneficio de tener un constructor que invoque a otro constructor sobrecargado es evitar la duplicación del código. En el ejemplo de Animal, no había código mas que el de establecer el nombre, pero imagina que pasaría si tras la línea 4 hubiera mas trabajo que hacer en el constructor. Poniendo todos los trabajos de todos los constructores en un constructor, y entonces tener a los otros constructores invocandolo, no tendremos que escribir y mantener multiples versiones de otro constructor importante. Básicamente, cada uno de los otros constructores que noe stán sobrecargados llamaría a otro constructor sobrecargado, pasando la información que necesitara.

    Los constructores y la instanciación se convierten en algo aún mas excitante, cuando usamos clases internas, que veremos mucho mas adelante.


Hasta aquí bastante contenido referente a los constructores de las clases, hemos visto que pueden existir varias formas de constructores, y tenemos que aprender bien cuando el compilador creará un constructor y cuando no lo hará.

Sin más, cualquier corrección o aporte es bienvenido.

Saludos!!!