5 de noviembre de 2011

Sun Certified Java Programmer 6, CX-310-065 - Parte 2: Orientación a Objetos

En este segundo post para la certificación de Java 6 hablaremos sobre conceptos de la orientación a objetos, que abarca temas como la herencia, el polimorfismo, la cohesión, bajo acoplamiento (loose coupling), etc.

Empezaremos hablando sobre la encapsulación:

Encapsulación


La encapsulación es un mecanismo que nos permite que, aunque nuestras clases utilicen muchas variables y métodos para su correcto funcionamiento, no todas sean visibles desde el exterior. O sea que solo exponemos lo que los clientes necesitan para poder hacer uso de los objetos de nuestra clase.

Para entender mejor este concepto imaginemos el siguiente escenario:

Tenemos la siguiente clase:


public class Examen
{
    public String pregunta1;

    public static void main(String... args)
    {
        Examen examen = new Examen();
        examen.pregunta1 = "¿Que es Java?"; // Legal pero no recomendable
    }
}


El ejemplo anterior compilará y se ejecutará de forma correcta. Sin embargo no es recomendable que los atributos de la clase (las variables de instancia) estén expuestas de forma que los clientes puedan leer y escribir sus valores directamente. De esta forma cualquiera podría colocar el valor que quisiera en la variable "pregunta1", aún valores que nuestra aplicación no puede o no está preparada para manejar.

Ahora hagamos una pregunta: ¿Cómo poder cambiar la clase cuando alguien cambia el valor de "pregunta1" por un valor incorrecto? La única forma es volver a la clase e implementar un método de sólo escritura (un método setter: "setPregunta1(String pregunta1)") y ocultar la variable "pregunta1" estableciéndola como privada, pero de esta manera al no establecerla desde un principio con los métodos "set" o "get" todas las personas que han utilizado este código anteriormente y de la manera en la que estaba implementada se encontraran perdidas.

La capacidad de realizar cambios en el código de aplicación sin romper el código de otras personas que utilizan este código es un beneficio clave de la encapsulación. Ocultando los detalles de la implementación a través de métodos de acceso nos brinda la ventaja de poder rehacer el código dentro de los métodos sin forzar a las demás personas a cambios, ya que ellas usan dichos métodos de acceso.

Con todo esto obtenemos las dos promesas/beneficios de la programación orientada a objetos: Flexibilidad y Mantenimiento, pero como vemos estos dos beneficios no llegan solos, nosotros tenemos que implementar nuestro código de manera que brinde y soporte estos beneficios.

Estas son algunas recomendaciones para lograr la encapsulación:

  • Mantener las variables de instancia protegidas (mayormente con el modificador de acceso private).
  • Mantener publicos los métodos de acceso a las variables de instancia (modificador de acceso public), así forzamos a llamar a las variables de instancia y a la implementación del mismo a través de estos métodos en lugar de ir directamente por las variables de instancia.
  • Para los métodos de acceso se recomienda usar las reglas de las convenciones de nombres de JavaBean: set<nombrePropiedad> y get<nombrePropiedad>, de las cuales se habló en el post anterior.

A continuación mostramos un ejemplo más práctico de lo que nos estamos refiriendo:



La clase "A" no puede acceder a las variables de instancia de la Clase "B" sin antes ir a los métodos de acceso ("set" y "get") de dichas variables, las variables de instancia son marcadas privadas y los métodos de acceso son públicos.

Debemos tener presente que el código de estos métodos no solo se utiliza para devolver o establecer los valores de las variables de instancia, también se puede establecer lógica o implementación de muchas más cosas o reglas que queramos definir, por ejemplo:


public void setTamanio(int tamanio)
{
    this.tamanio = tamanio * 0.10;
    this.color = "negro";
}



El encapsulamiento es uno de los mecanismos que nos proporciona la orientación a objetos para poder darle una funcionalidad rica a nuestras clases sin que las personas que las utilicen sepan los detalles exactos de cómo está implementada dicha funcionalidad.

Sin embargo, este no es el único mecanismo proporcionado por la orientación a objetos. A continuación veremos otro de ellos. La herencia.


Herencia, ES–UN, TIENE–UN (IS–A, HAS-A)


En Java, la herencia se encuentra en todos. Es seguro decir que en Java casi nada se puede hacer sin hacer uso de la herencia. Para dar un ejemplo a continuación usaremos del operador "instanceof" (por ahora no ahondaremos mucho en la explicación del uso de este operador ya que se tocará más adelante con mayor detalle, sólo cabe recordar ahora que este operador devuelve un "true" si la variable puesta al principio es del tipo de la variable con la que se está comparando):


public class Test
{
    public static void main(String... args)
    {
        Test t1 = new Test();
        Test t2 = new Test();
        
        if(!t1.equals(t2))
        {
            System.out.println("No son iguales")
        }
        if(t1 instanceof  Object)
        {
            System.out.println("t1 es un objeto");
        }
    }
}


¿De dónde saca "t1" el método "equals"? Si no hemos implementado ningún método dentro de la clase con ese nombre, ¿o sí? Por otro lado se pregunta si "t1" es una instancia de la clase "Object" y de ser así, la condición if será exitosa.

¿Cómo puede ser "t1" del tipo "Object" si solo lo declaramos que sea del tipo de la clase "Test"? Esto ocurre porque todas las clases en java (las que ya están escritas, las que escribimos y las que escribiremos) son una subclase de la clase "Object" (excepto por supuesto la clase "Object" misma) y siempre tendrán métodos como "equals", "clone", "notify", "wait" y otros más. Siempre que creamos una clase, esta hereda todos los métodos de la clase "Object".

El método "equals" por ejemplo: Los creadores de Java asumieron correctamente que nosotros comúnmente desearíamos comparar instancias de las clases para comprobar la igualdad; si la clase "Object" no tuviera un método "equals" tendríamos que implementar nosotros mismos un método para este propósito.

También debemos recordar que las 2 razones más importantes para el uso de la herencia son:

  • Reutilización de código
  • Uso del polimorfismo

Empecemos con la reutilización. Un enfoque de diseño común es crear una versión de una clase bastante genérica y después crear subclases muy especializadas que hereden de esta, por ejemplo:


public class Animal
{
    public void muevete()
    {
        System.out.println("Me estoy moviendo");
    }
}


public class Perro extends Animal
{
    public void ladra()
    {
        System.out.println("Estoy ladrando");
    }
}

public class PruebaAnimal
{
    public static void main(String... args)
    {
        Perro perro = new Perro();
        perro.muevete();
        perro.ladra();
    }
}


La salida del código anterior seria:


Me estoy moviendo
Estoy ladrando


Como podemos ver, la clase "Perro" está heredando el método "muevete" de la clase "Animal" y que también tiene su propio método agregado, en este caso es el método "ladra". Aquí se está haciendo uso de la reusabilidad al utilizar un método genérico de una clase padre que en este caso es el método "muevete", con esto podemos crear diferentes tipos de animales y todos van a poder utilizar el método "muevete" sin necesidad de implementarlo ellos mismos.

El segundo objetivo de la herencia es poder acceder a las clases polimórficamente. Imaginemos este escenario: digamos que tenemos una clase llamada "Entrenador" que quiere recorrer todos los diferentes tipos de animal e invocar al método "muévete" en cada uno de ellos, al momento de escribir la clase "Entrenador" no sabemos cuántas clases de "Animal" podría haber y seguro no vamos a querer cambiar el código sólo porque a alguien se le ocurrió crear un nuevo tipo de Animal.

Lo bonito del polimorfismo es que podemos tratar cualquier tipo de "Animal" como un "Animal", en otras palabras podemos decir lo siguiente: No me importa qué tipo de Animal se pueda crear, siempre y cuando extienda (herede) de Animal todos van a poder moverse (método "muevete").

Ahora miremos esto con un ejemplo:


public class Animal
{
    public void muevete()
    {
        System.out.println("Me estoy moviendo");
    }
}

public class Perro extends Animal
{
    public void ladra()
    {
        System.out.println("Estoy ladrando");
    }
}

public class Gato extends Animal
{
    public void ronronea()
    {
        System.out.println("Estoy ronroneando");
    }
}


Ahora imaginemos una clase llamada "Entrenador" que tiene un método que tiene como argumento un "Animal", esto significa que puede tomar cualquier tipo de "Animal", cualquier tipo de animal puede ser pasado a un método con un argumento del tipo "Animal", ejemplo:


public class Entrenador
{
    public static void main(String... args)
    {
        Gato gato = new Gato();
        Perro perro = new Perro();
        mueveteAnimal(gato);
        mueveteAnimal(perro);
    }

    public static void mueveteAnimal(Animal animal)
    {
        animal.muevete();
    } 
}


La salida del código anterior es:


Me estoy moviendo
Me estoy moviendo



El método "mueveteAnimal" está declarando un "Animal" como argumento pero se le puede pasar cualquier sub-clase o sub-tipo de esta clase "Animal", este método podría invocar a cualquier método dentro de la clase "Animal". Lo que si debemos de tener en cuenta es que sólo podemos llamar a los métodos declarados por "Animal", los métodos declarados dentro de las subclases de "Animal" son dependiente del tipo declarado, esto significa que no podríamos llamar al método "ladra" incluso si el "Animal" que se está pasando es del tipo "Perro".

Relaciones IS – A, HAS– A


IS – A


En Orientación a objetos el concepto de "IS-A" (es-un) esta basado en la herencia de una clase ("extends") o en la implementación de una interface ("implements"). IS-A es una forma de decir "Esta cosa es del tipo de esta cosa" (o estos dos tipos pueden ser equivalentes), por ejemplo un "Perro" es del tipo "Animal", en OO nosotros podemos decir: "Perro IS-A Animal", "Lechuga IS-A Vegetal", en java podemos expresar esta relación IS-A por medio de las palabras reservadas "extends" (para la herencia de clases) y de "implements" (para la implementación de interfaces), veamos un ejemplo:

public class Carro
{
    //cualquier código aquí
}

public class Toyota extends Carro
{
    /*Toyota está heredando de carro, no olvidemos que Toyota 
    hereda los miembros de Carro incluido métodos y variables*/
} 


"Carro" también es un vehículo así que se podría implementar un árbol de herencia de la siguiente manera:


public class Vehiculo{...}
public class Carro extends Vehiculo{...}
public class Toyota extends Carro{...}


En términos de OO podríamos decir lo siguiente:

  • Vehículo es la súper clase de Carro
  • Carro es la subclase de Vehículo
  • Carro es la súper clase de Toyota
  • Toyota es la subclase de Carro
  • Toyota hereda de Carro y de Vehiculo
  • Toyota deriva de Carro
  • Carro deriva de Vehículo
  • Toyota es subtipo de Carro y Vehiculo
Retornando a la relación IS-A, lo siguiente es correcto:

Toyota extends Carro significa Toyota IS-A Carro

Carro extends Vehiculo significa Carro IS-A Vehiculo

Ahora recordemos que al principio usamos el operador instanceof, bueno, si la expresión "Toyota instanceof Carro" es verdadera, entonces es lo mismo que decir "Toyota IS-A Carro", la expresión "Toyota instanceof Vehiculo" también es verdadera aunque explícitamente no dice esto ya que "Toyota" extiende de "Carro", pero por otro lado "Carro" si extiende de "Vehiculo" a esto se le llama herencia indirecta ya que una clase puede ser hijo, nieto, bisnieto, etc. de otra clase, una clase puede extender o heredar de otra directa o indirectamente ya que en el árbol de herencia pueden haber clases intermedias.

HAS–A


La relación HAS-A esta basada en el uso en lugar de la herencia, por ejemplo "A HAS-A B" si la clase "A" tiene una referencia a una instancia de la clase "B", por ejemplo:


public class Animal{ }

public class Gato extends Animal
{
    CajaDeArena miCajaDeArena;
}


En el código anterior la clase "Gato" tiene una referencia una variable de instancia del tipo "CajaDeArena", entonces podemos decir "Gato HAS-A CajaDeArena", en otra palabras, "Gato" tiene una referencia a una "CajaDeArena", la clase "Gato" puede tener un método llamado "llenarCaja(Arena miArena)", de esta manera los usuarios de la clase "Gato" no podrán saber nunca que cuando se invoca al método "llenarCaja" este método delega toda la responsabilidad a la clase "CajaDeArena" llamando a su método "llenarCaja", veamos esto con un ejemplo:


public class Gato extends Animal
{
    private CajaDeArena miCajaDeArena;
    
    public void llenarCaja(Arena miArena)
    {
        miCajaDeArena.llenarCaja(miArena); /*delegando comportamiento al objeto CajaDeArena */
    }
}

public class CajaDeArena
{
    public void llenarCaja(Arena miArena)
    {
        System.out.println("Llenando la caja de arena");
    }
}


En OO muchas veces no queremos que la gente se preocupe por cual clase u objeto está haciendo realmente el trabajo, los usuarios de la clase "Gato" hacen llamado del método "llenarCaja" pero estos no saben si la misma clase hace el trabajo o no, sin embargo a ellos les parece que la propia clase "Gato" lo hace, no tienen ni idea de que existe algo como una clase llamada "CajaDeArena" que es quien realmente hace el trabajo.

Toda la explicación anterior nos servirá para entender mejor otro de los conceptos de la programación orientada a objetos, uno de los más útiles si sabemos cómo utilizarlo correctamente: el polimorfismo.


Polimorfismo

Cualquier clase que pase la prueba de tener la relación "IS-A" puede ser considerada polimórfica, todos los objetos en Java son polimórficos ya que pasan la prueba de la relación IS-A, tanto para su propio tipo como para con la clase "Object". Debemos recordar la única forma de a un objeto es a través de una variable de referencia, hay algunas cosas claves que debemos recordar de las variables de referencia:

  • Una variable de referencia puede ser sólo de un tipo y una vez declarado este tipo nunca podrá cambiar (aunque el objeto al que hace referencia puede cambiar).
  • Una referencia es una variable, por lo cual su valor puede ser reasignada a otros objetos a menos que esta sea declarada como final.
  • Un tipo de variable de referencia determina los métodos que se pueden invocar en el objeto que esta variable está referenciando.
  • Una variable de referencia puede referirse a cualquier objeto del mismo tipo que el que está declarando, o puede referirse a cualquier subtipo del tipo declarado.
  • Una variable de referencia puede ser declarado como un tipo de clase o un tipo de interfaz. Si la variable se declara como un tipo de interfaz, se puede hacer referencia a cualquier objeto de cualquier clase que implementa la interfaz.


Anteriormente creamos una clase "Animal" que era extendida por dos clases, "Perro" y "Gato", ahora imaginemos que tenemos una clase llamada "Gaviota", después queremos hacer que algunos tipos de Animal vuelen o se puedan elevar en el aire y otros no como el caso de "Gaviota" que si puede elevar, por el contrario "Perro" y "Gato" no pueden hacerlo, para esto podríamos crear una clase llamada "Elevable" con un método "elevar" y hacer que unos tipos de Animales puedan elevarse y otros no, pero también queremos que todos los tipos de Animal se muevan que es lo que nos permite el método "muevete" de la clase "Animal", pero esto no funcionaría ya que Java solo soporta la herencia simple, esto significa que una sub clase sólo puede tener una clase padre, es decir lo siguiente es incorrecto:


public class Gaviota extends Animal, Elevable // NO!!
{ 
 //Cualquier codigo aquí
}


Una clase NO puede extender de más que de sólo una clase, ante estos casos la solución sería crearse una interface llamada "Elevable" y solo las sub-clases de "Animal" que puedan volar implementen esta interfaz, dicha interfaz quedaría de la siguiente manera:


public interface Elevable
{
    public void volar();
}

public class Animal
{
    public void muevete()
    {
        System.out.println("Me estoymoviendo");
    }
}


A continuación mostramos la clase "Gaviota" que implementa esta interfaz:


public class Gaviota extends Animal implements Elevable
{
    public void volar()
    {
        System.out.println("¡Estoy volando!");
    }
}


Ahora tenemos a la clase "Gaviota" que pasa la prueba de la relación IS-A tanto para la clase "Animal" como para la interfaz "Elevable", esto significa que una "Gaviota" puede ser tratada polimórficamente como una de estas cuatro cosas en un momento dado, dependiendo del tipo declarado de la variable de referencia:

  • Como un "Object" (ya que todas las clases heredan de la clase Object).
  • Como un "Animal" (ya que está extendiendo de la clase Animal).
  • Como una "Gaviota" (ya que es lo que es).
  • Como un "Elevable" (ya que implementa de la interface Elevable).


Las siguientes declaraciones son legales:


Gaviota gaviota = new Gaviota();
Object object = gaviota;
Animal animal = gaviota;
Elevable elevable = gaviota;


Aquí solo hay un objeto Gaviota pero cuatro diferentes variables de referencia, hagamos una pregunta: ¿Cuál de las cuatro variables de referencia puede llamar al método "muevete"? Recuerden que las llamadas a métodos permitidos por el compilador se basan únicamente en el tipo declarado de la referencia, con independencia del tipo de objeto. Así que buscando en los cuatro tipos de referencia de nuevo, "Object", "Gaviota", "Animal" y "Elevable" ¿cuál de estos cuatro tipos puede llamar al método "muevete()"? La respuesta es la siguiente: El objeto "animal" como el objeto "gaviota" son conocidos por el compilador para poder llamar o invocar al método "muevete".

¿Qué métodos pueden ser invocados cuando el objeto "gaviota" está utilizando la referencia a la inteface "Elevable"? Sólo al método "volar()".

Un beneficio es que cualquier clase desde cualquier lugar en el árbol de herencia puede invocar a la interface "Elevable", que sucedería si tenemos un método que tiene como argumento declarado con tipo "Elevable", podrías pasar una instancia de objeto del tipo "gaviota" y cualquier otra instancia de clase que implemente a la interface "Elevable", podría usar el parámetro del tipo "Elevable" para invocar al método "volar()" pero no al método "muevete()" o cualquier otro objeto que se sabe que el compilador conocer basado en el tipo de referencia.

Otra cosa que debemos saber es que si bien el compilador solo conoce al tipo de referencia declarado, la JVM en tiempo de ejecución sabe lo que el objeto realmente es, y eso significa que incluso si el objeto "gaviota" hace una llamada al método "muevete" usando la variable de referencia "animal", si el objeto "gaviota" sobre-escribe el método "muevete", la JVM invocara a esa versión del método "muevete". La JVM mira al objeto real que se encuentra al otro extremo de la referencia, puede ver que se ha sobre escrito el método del tipo de variable de referencia declarado e invoca al método del objeto de la clase actual.

Siempre se puede hacer referencia a un objeto con un tipo de referencia variable más general (una superclase o interfaz), pero en tiempo de ejecución, las únicas cosas que son seleccionadas dinámicamente basándose en el objeto real (en lugar de tipo de referencia) son métodos de instancia (no los métodos estáticos, no las variables), sólo los métodos de instancia sobre-escritos se invocan de forma dinámica en función del tipo de objeto real.

Ejemplo:


public class Animal
{
    public void muevete()
    {
        System.out.println("Me estoy moviendo");
    }

    public static void respirar() //Método estático
    { 
        System.out.println("Estoy respirando");
    }
}

public class Gaviota extends Animal 
{
    public void muevete()
    {
        System.out.println("La gaviota se mueve");
    }

    public static void respirar() //Método estático
    {
        System.out.println("La gaviota está respirando");
    }
}

public class Prueba
{
    public static void main(String... args)
    {
        Animal a = new Gaviota();
        a.muevete();
        a.respirar();
    }
}


Salida:


La gaviota se mueve 
Estoy respirando


El segundo resultado que vemos se trata de la invocación de un método estático. El comportamiento que vemos pasa porque cuando el compilador ve que se está invocando un método estático, cambia la referencia al objeto por el tipo de la clase, o sea que al final queda de la siguiente manera:


Animal a = new Gaviota();
Animal.respirar();


En el primer caso el método que se invoca es el de "Gaviota" y no el de "Animal", ya que el método "muevete" no es estático sino un método de instancia; por eso dice se dice que sólo los métodos de instancia sobrecargados se invocan de forma dinámica en función del tipo de objeto real.


Sobrecarga y sobre escritura


Sobre escritura de métodos

En cualquier momento que se tenga un método que hereda de una superclase, se tendrá la oportunidad de realizar una sobre-escritura de métodos, a menos que el método este marcado con la palabra reservada final. El principal beneficio de la sobre-escritura de métodos es que se puede definir un comportamiento especial para un método de una subclase. En el siguiente ejemplo veremos cómo la clase "Gaviota" sobre-escribe el método "muevete" de la clase "Animal":


public class Animal
{
    public void muevete()
    {
        System.out.println("Me estoy moviendo");
    }
}

public class Gaviota extends Animal 
{
    public void muevete()
    {
        System.out.println("La gaviota se mueve");
    }
}


Para métodos abstractos que se heredan desde una superclase, no se tiene otra opción más que sobre escribir-dichos métodos. Se deben implementar los métodos a menos que la subclase que los herede también este marcada como abstracta. Los métodos abstractos deben ser implementados por una subclase concreta, esto quiere decir que la subclase concreta sobre-escribe el método abstracto de la superclase. Entonces debemos pensar que los métodos abstractos son métodos que forzosamente deben ser sobre-escritos.

El creador de la clase "Animal" podría haber decidido que, a efectos de polimorfismo, todos los subtipos de Animal deben implementar el método "moverse", de una manera única y especifica. Polimórficamente, cuando alguien tiene una referencia de Animal que no refiere a una instancia de Animal, sino a una instancia de una subclase de Animal, la persona que llama debe ser capaz de invocar al método "muevete" en la referencia a "Animal", pero el objeto en tiempo de ejecución real ejecutará su propio y especifico método "muevete". Marcando el método "muevete" como abstracto es la forma que el programador de la clase "Animal" dice a todos los desarrolladores de las demás subclases: "No tiene ningún sentido para el nuevo subtipo utilizar el método genérico "muevete", por lo que tú debes implementar tu propio método "muevete". A continuación mostraremos un ejemplo de clases no abstractas:


public class Prueba
{
    public static void main(String... args)
    {
        Animal a = new Animal();
       Animal b = new Gaviota(); //Referencia a Animal, pero objeto Gaviota
        a.muevete();
        b.muevete();
    }
}

public class Animal
{
    public void muevete()
    {
        System.out.println("Me estoy moviendo");
    }
}


public class Gaviota extends Animal 
{
    public void muevete()
    {
        System.out.println("La gaviota se mueve");
    }

    public void volar()
    {
        System.out.println("La gaviota vuela");
    }
}


En el código anterior la clase "Prueba" utiliza una referencia a "Animal" para invocar un método en el objeto "Gaviota", hay que recordar que el compilador solo permitirá que se invoquen métodos que se encuentran en la clase "Animal", cuando usas una referencia a un "Animal". Por lo tanto, el siguiente código no es legal:


Animal c = new Gaviota();
c.volar();  // No puedes invocar a "volar()", Animal no tiene un método volar


Para reiterar: el compilar solo mira el tipo de referencia y no el tipo de instancia en tiempo de ejecución. El polimorfismo nos permite tener referencias a súper tipos o a tipos más abstractos (incluyendo las interfaces) para referir a uno de estos subtipos (incluyendo implementación de interfaces).

El método que sobre-escribe no debe tener un modificador de acceso más restringido que el del método a ser sobre-escrito. Por ejemplo no se puede sobre-escribir un método marcado con el modificador de acceso public y cambiarlo a protected. Pensemos en los siguiente: si la clase "Animal" tiene un método "comer()" marcado "public" y alguien tiene una referencia de "Animal", es decir, una referencia declarada del tipo "Animal", se asume que es seguro llamar al método "comer()" en la referencia de "Animal", independientemente de la instancia actual que la referencia a "Animal" está invocando. Si una subclase cambia el modificador de acceso del método sobre-escrito, entonces cuando la JVM invoque la verdadera versión del objeto "Gaviota" en lugar de la versión del tipo de referencia "Animal", el programa perecerá, a continuación un ejemplo:


public class Prueba
{
    public static void main(String... args)
    {
        Animal a = new Animal();
        Animal b = new Gaviota(); // Referencia a Animal, pero objeto Gaviota
        a.muevete();
        b.muevete();
    }
}

public class Animal
{
    public void muevete()
    {
        System.out.println("Me estoy moviendo");
    }
}

public class Gaviota extends Animal 
{
    private  void muevete() //Marcado como privado
    {   
        System.out.println("La gaviota se mueve");
    }
}


Si el código anterior compilara (cosa que no hará) obtendríamos el siguiente error en tiempo de ejecución:


Animal b = new Gaviota(); //Referencia a Animal, pero objeto Gaviota
a.muevete(); //Ocurre una crisis en tiempo de ejecución 


La variable "b" es del tipo "Animal", el cual tiene un método público llamado "muevete", pero hay que recordar que en tiempo de ejecución, Java utiliza la invocación de métodos virtuales para seleccionar dinámicamente la versión real del método que se ejecutará, basado en la del tipo del objeto en tiempo de ejecución. Una referencia de "Animal" puede referir siempre a una instancia de "Gaviota" porque "Gaviota" IS-A "Animal" ("Gaviota" ES UN "Animal"). Lo que hace posible que una instancia de una superclase referencie a una instancia de la subclase es que la subclase es capaz de hacer todo lo que la superclase puede hacer (ya que recibe el comportamiento de esta por herencia). Esto quiere decir que cualquiera con una referencia a "Gaviota" usando una instancia de Animal (Animal a = new Gaviota();) es libre de llamar a todos los métodos accesibles de "Animal", no importa si "Gaviota" sobre-escribe los métodos de "Animal" o simplemente los hereda. Un método-sobre escrito debe cumplir con el contrato de la superclase.

Las reglas para sobre escribir un método son las siguientes:

  • La lista de argumentos debe ser exactamente igual (del mismo tipo y en el mismo orden) que el método a sobre-escribir. Si esta lista no coincide en realidad lo que se está haciendo es una sobrecarga del método (que puede que no sea nuestra intención).
  • El tipo de retorno del método sobre escrito debe ser el mismo o un subtipo del declarado en el método de la súper clase.
  • El modificador de acceso no debe ser más restrictivo que del método a sobre-escribir. Por ejemplo, si en la clase base tenemos un método "public" NO podemos sobre-escribirlo poniéndole un modificador "protected".
  • El modificador de acceso puede ser menos restrictivo que el del método a sobre-escribir. Por ejemplo, si en la clase base tenemos un método "protected" SI podemos sobre-escribirlo poniéndole un modificador "public".
  • Los métodos de instancia solo pueden ser sobre-escritos si estos son heredados por la subclase. Una subclase dentro del mismo paquete que su superclase puede sobre escribir cualquier método de la súper clase que NO esté marcado como private o final. Una subclase en diferente paquete puede sobre-escribir solo los métodos no finales marcados public o protected (los métodos protected son heredados por la subclase).
  • Los métodos sobre-escritos no deben lanzar excepciones marcadas (en las que sean necesarias un try catch) que sean nuevas o más amplias que aquellas declaradas en el método que sobre-escribe. Por ejemplo un método que declara un "FileNotFoundException" no puede ser sobre-escrito por un método que declara un "SQLException", "Exception", "IOException" o cualquier otra excepción a menos que esta sea una subclase de "FileNotFoundException".
  • Los métodos sobre-escritos pueden lanzar excepciones más específicas (excepciones que extiendan de la excepción que se está declarando) o lanzar menos excepciones. Solo porque el método a sobre-escribir puede lanzar excepciones no quiere decir que el método sobre-escrito lanzara estas mismas excepciones, un método sobre-escrito no tiene que declarar una excepción que nunca lanzará. Independientemente de que el método a sobre-escribir las declare.
  • No se puede sobre-escribir un método marcado con final.
  • No se puede sobre-escribir un método marcado con static.
  • Si un método no puede ser heredado entonces no puede ser sobre-escrito. Hay que recordar que la sobre-escritura implica que se está re-implementando un método que está heredanddo. Por ejemplo el siguiente código no es legal:

public class Prueba
{
    public static void main(String... args)
    {
        Gaviota a = new Gaviota(); 
        a.muevete(); //No es legal porque Gaviota no hereda muevete(), muevete() es declarado private en Animal
    }    
}

public class Animal
{
    private void muevete()
    {
        System.out.println("Me estoy moviendo");
    }
}

public class Gaviota extends Animal {}



Invocando a la versión de la superclase de un método sobre escrito

Tal vez queramos tomar ventaja de una parte del código en la versión de la súper clase de un método y aun así sobre-escribirlo para proveer algún comportamiento especifico adicional. Esto es como decir: "Ejecuta la versión de la superclase de un método, después vuelve y termina con mi código para hacer un comportamiento adicional en el método de la subclase", esto es fácil de hacer utilizando la palabra reservada super como en el ejemplo siguiente:


public class Prueba
{
    public static void main(String... args)
    {
        Gaviota a = new Gaviota(); 
        a.muevete();  
    }
}

public class Animal
{
    public void muevete()
    {
        System.out.println("Me estoy moviendo");
    }
}

public class Gaviota extends Animal 
{
    public  void muevete()
    {  
        //podemos agregar alguna cosa mas
        super.muevete(); //invoca al código de la superclase (Animal)
    }
}


Usar la palabra reservada "super" para invocar a un método sobre-escrito sólo se aplica para métodos de instancia, hay que recordar que los métodos marcados con static no pueden ser sobre escritos.


Sobrecarga de métodos

La sobrecarga de métodos permite usar el mismo nombre de un método en una clase, pero con diferentes argumentos y, opcionalmente, un tipo de retorno diferente, el código asume la carga de hacer frente a diferentes tipos de argumentos en lugar de obligar a la persona que llama a hacer las conversiones antes de invocar el método. Las reglas a seguir para la sobrecarga son simples:

  • Para sobrecargar métodos se debe cambiar la lista de argumentos.
  • Para sobrecargar métodos se puede cambiar el tipo de datos de retorno.
  • Para sobrecargar métodos se puede cambiar el modificador de acceso.
  • Para sobrecargar métodos se pueden declarar excepciones que sean nuevas o más amplias.
  • El método puede ser sobrecargado en la misma clase o en una subclase.
  • Si la clase "A" define un método "hacerAlgo(int i)", la subclase "B" puede definir un método llamado "hacerAlgo(String s)", sin sobre-escribir el método "hacerAlgo(int i)" de la súper clase que toma como argumento un entero. Dos métodos con el mismo nombre pero en diferentes clases pueden ser consideradas sobrecargados, si la sub-clase hereda una versión del método y luego declara otra versión sobrecargada en la definición de la clase.



Formas legales de sobrecargar un método

El método que queremos sobrecargar es el siguiente:


public void unMetodo(String str, int t, double d){}


Los siguientes métodos son legales para sobrecargar el método anterior:


public void unMetodo(String str, int t){}

public int unMetodo(String str, double d){}

public void unMetodo(String str, int t)throws IOException{}



Invocando a métodos sobrecargados

Cuando un método es invocado, más de un método con el mismo nombre puede existir para el tipo de objeto que estamos invocando. Por ejemplo la clase "Gaviota" puede tener tres métodos con el mismo nombre pero con diferente lista de argumentos, estos serán métodos sobrecargados. Definir cuál de los diferentes métodos se desea invocar dependerá de la lista de argumentos que contenga. Si estamos invocando a un método que tiene un "String" como argumento, el método que tiene un "String" como argumento será llamado. A continuación veremos un ejemplo:


public class UnaClase
{
    public int unMetodo(int x, int y)
    {
        return x + y;
    }

    public double unMetodo(double x, double y)
    {
        return x + y;
    }
}

public class Prueba
{
    public static void main(String... args)
    {
        UnaClase a = new UnaClase();
        int x = 9;
        int y = 5;
        
        int resultado = a.unMetodo(x, y); //¿Qué versión de "unMetodo" es invocado?

        double otroResultado = a.unMetodo(5.2, 3.6); //¿Qué versión es invocada?
    }
}


En el primer caso se llama a la primera versión del método "unMetodo(x,y)" ya que se está pasando a dos enteros y en el segundo caso se llama a la versión del método "unMetodo" que recibe dos argumentos del tipo double.

La invocación de métodos sobrecargados que reciben objetos en lugar de tipos primitivos es un poco más interesante. Si tenemos un método sobrecargado, que en un método toma un objeto del tipo "Animal" y otro en el que toma un objeto del tipo "Gaviota" (subclase de "Animal"). Si pasamos un objeto del tipo Gaviota cuando invocamos al método, invocaremos a la versión sobrecargada que toma una "Gaviota". O al menos eso parece a primera vista:


class  Animal{}

class Gaviota extends Animal{}

public class UsaAnimales
{
    public void muevete(Animal a)
    {
        System.out.println("Estas en la versión de Animal");
    }

    public void muevete(Gaviota g)
    {
        System.out.println("Estas en la versión de Gaviota");
    }
 
    public static void main(String... args)
    {
        UsaAnimales ua = new UsaAnimales();
        Animal a = new Animal();
        Gaviora g = new Gaviota():
        ua.muevete(a);
        ua.muevete(g);
    }
}


La salida es la que esperamos:


Estas en la versión de Animal
Estas en la versión de Gaviota


¿Pero qué sucede si usamos una referencia de "Animal" para un objeto "Gaviota"?


Animal animalReferenciaGaviota = new Gaviota();
ua.muevete(animalReferenciaGaviota);


¿Cuál de las versiones del método "muévete" es invocada?, podríamos pensar que la respuesta es: "El que toma una "Gaviota", ya que es un objeto del tipo "Gaviota" el que en tiempo de ejecución está siendo pasado al método". Pero no es así como funciona. El código anterior imprime lo siguiente:


Estas en la versión de Animal


El objeto en tiempo de ejecución es una "Gaviota" y no un "Animal". La elección de cuál método sobrecargado se deberá llamar no es decidido dinámicamente en tiempo de ejecución. Sólo hay que recordar que el tipo de referencia, no el tipo de objeto, determina qué método sobrecargado es llamado.

Para resumir lo que hemos visto hasta ahora de sobrecarga y sobre-escritura: cuál versión de un método sobre-escrito es llamado (en otras palabas, desde cuál clase en el árbol de herencia) es decidido en tiempo de ejecución por el tipo de objeto; pero cuál versión del método sobrecargado se llamará está basado en el tipo de referencia del argumento que se pasa en tiempo de compilación. Si invocamos un método pasando una referencia de "Animal" para un objeto "Gaviota", el compilador solo sabe que está recibiendo un "Animal", por lo que elige la versión sobrecargada que toma un "Animal". No importa que en tiempo de ejecución realmente se pase una "Gaviota".


Polimorfismo en métodos sobrecargados y sobre-escritos

¿Cómo trabaja el polimorfismo en métodos sobrecargados? Si pasamos una referencia de "Animal", el método que toma un "Animal" será llamado, incluso si el objeto actual pasado es una "Gaviota". Una vez que la "Gaviota" disfrazada de "Animal" entra en el método, sin embargo, el objeto "Gaviota" sigue siendo una "Gaviota" a pesar de ser pasado a un método que recibe un "Animal". Entonces es verdad que el polimorfismo no determina que versión del método sobrecargado será llamado. El polimorfismo entra en juego cuando se decide cuál versión de un método sobre-escrito es llamado. Pero algunas veces un método puede ser ambos, sobre-escrito y sobrecargado. Imaginemos lo siguiente:


public class Animal
{
    public void comer()
    {
        System.out.println("El Animal genérico está comiendo");
    }
}

public class Gaviota extends Animal
{
    public void comer()
    {
         System.out.println("La gaviota está comiendo");
    }

    public void comer(String s)
    {
        System.out.println("La gaviota está comiendo esto: " + s);
    }
}


Noten que la clase "Gaviota" tiene sobrecargado y sobre-escrito el método "comer" (el método sobrecargado es el que recibe un String y el sobre-escrito el que no recibe nada).

La siguiente tabla muestra cuál de las versiones del método "comer" será llamado dependiendo de cómo sea invocado:

Código de Invocación al métodoResultado
Animal a = new Animal();
a.comer();
"el Animal genérico está comiendo"
Gaviota g = new Gaviota();
g.comer();
"La gaviota está comiendo"
Animal ag = new Gaviota();
ag.comer();
"La gaviota está comiendo"
Polimorfismo trabajando: El objeto actual ("Gaviota"), no el tipo de referencia ("Animal"), es usado para determinar cuál versión del método "comer" es llamado.
Gaviota gp = new Gaviota();
gp.comer("peces")
"La gaviota está comiendo esto: peces"
El método sobre cargado "comer(String s)" es llamado.
Animal a2 = new Animal();
a2.comer("manzanas");
¡¡Error de compilación!!
El compilador mira la clase Animal y ve que no tiene un método comer que reciba un String.
Animal ag2 = new Gaviota();
ag2.comer("peras");
¡¡Error de compilación!!
El compilador ve solo la referencia y sabe que "Animal" no tiene un método "comer" que reciba un String. Al compilador no le importa que el objeto actual pueda ser una "Gaviota" en tiempo de ejecución.



Diferencias entre métodos sobre cargados y sobre escritos:

Método sobrecargado Método sobre-escrito
Argumento(s)Debe cambiar.No debe cambiar.
Tipo de retornoPuede cambiar.No puede cambiar, excepto para retornos covariantes.
ExcepcionesPuede cambiar.Pueden no declarar una excepción del método que sobre-escribe, o declarar una subclase de esta excepción. No debe lanzar nuevas excepciones o más "amplias" que aquellas declaradas en el método que sobre-escribe.
Nivel de AccesoPuede cambiar.No pueden tener un nivel de acceso más restrictivo, pero si uno igual o menos restrictivo.
InvocaciónEl tipo de referencia determina cuál método sobrecargado es seleccionado, basado en el tipo de argumentos declarados. Sucede en tiempo de compilación.El método real que se invoca es todavía una invocación de método virtual que siempre sucede en tiempo de ejecución, pero el compilador ya sabe la firma del método que se invoca. El tipo de objeto determina cuál método es seleccionado. Sucede en tiempo de ejecución.



Casting de variables de referencia

Hasta el momento hemos visto como es posible y común el uso de variables de referencia genéricas para referirse a tipos de objetos más específicos, eso es el corazón del polimorfismo, como ejemplo veamos la siguiente línea de código:


Animal animal = new Gaviota();


¿Pero qué sucede si queremos usar una variable de referencia "Animal" para invocar un método que solo la clase "Perro" tiene? Sabemos que nos estamos refiriendo a un Perro y queremos hacer una cosa específica de Perro.

En la siguiente línea de código tendremos un arreglo de Animales y cada vez que encontremos una Perro en el arreglo, haremos algo especial de Perro. Asumamos por el momento que todo el código escrito a continuación esté correcto, solo que no estamos seguros de la línea de código que invocara al método "hacerRuido".


public class Animal
{
    void hacerRuido()
    {
        System.out.println("Ruido Generico");
    }
}

public class Perro extends Animal
{
    void hacerRuido()
    {
        System.out.println("Ladrando");
    }

    void hacerseElMuerto()
    {
        System.out.println("Muriendo");
    }
}

public class Prueba
{
    public static void main(String... args)
    {
        Animal[] a = {new Animal(), new Perro(), new Animal()};

        for(Animal animal: a)
        {
            animal.hacerRuido();

            if(animal instanceof Perro)
            {
                animal.hacerseElMuerto(); //Se intenta invocar un metodo de Perro?
            }
        }
    }
}


Cuando compilemos el código anterior, el compilador nos enviara un mensaje como:


Cannot find Symbol


El compilador nos está diciendo: "Oye, la clase "Animal" no tiene un método "hacerseElMuerto"".

Ahora modifiquemos el bloque de la condicional if:


if(animal instanceof Perro)
{
    Perro perro = (Perro) animal; // Casteando la variable de referencia
    perro.hacerseElMuerto();
}


El nuevo y mejorado bloque de código contiene un "cast", el cual en algunos casos es llamado down casting, porque estamos bajando en el árbol de herencia a una categoría mas especifica.

Ahora el compilador no nos dará problemas, casteamos la variable "animal" a un tipo "Perro". El compilador nos está diciendo lo siguiente: "Sabemos que nos estamos refiriendo a un objeto del tipo Perro, está bien hacer una nueva variable de referencia de tipo Perro, para referirnos a este objeto", en este caso estamos bien, porque antes de intentar el "cast" hicimos una prueba de instanceof para asegurarnos.

Es importante saber que el compilador confía en nosotros cuando hacemos un down casting aun cuando pudiéramos hacer algo como lo siguiente:


public class Animal{}

public class Perro extends Animal{}

public class Prueba
{
    public static void main(String... args)
    {
        Animal animal = new Animal();
        Perro perro = (Perro)animal; //Compila pero fallara después 
    }
}


Este código compilara correctamente pero cuando intentemos ejecutarlo vamos a obtener una excepción como la siguiente:


java.lang.ClassCastException


¿Por qué no podemos confiar en el compilador para que nos ayude en esto? ¿No puede ver que animal es del tipo "Animal"? Todo lo que el compilador puede hacer es comprobar que los dos tipos pertenezcan al mismo árbol de herencia, por lo que dependiendo de lo que el código puede hacer antes del down casting, es posible de que "animal" sea del tipo "Perro". El compilador puede permitir cosas que posiblemente puedan funcionar en tiempo de ejecución. Sin embargo si el compilador sabe con certeza que el cast no puede funcionar, entonces la compilación fallara. El siguiente bloque de código NO compilará:


Animal animal = new Animal();
Perro p = (Perro) animal;
String s = (String) animal; //animal nunca puede ser un String


En este caso obtendremos un error como el siguiente:


inconvertible types


A diferencia del down-casting, el up-casting (convertir a un tipo más general en el árbol de herencia) trabaja implícitamente porque cuando estmos haciendo up-casting estamos implícitamente restringiendo el numero de métodos que podemos invocar, lo que impide que más adelante podamos invocar a un método más especifico, por ejemplo:


class Animal{}

class Perro extends Animal{}

class Prueba
{
    public static void main(String … args)
    {
        Perro p = new Perro();
        Animal a1 = p;  //upCasting, se puede sin conversión explicita
        Animal a2 = (Animal)p; // upCasting, se puede con conversión explicita
    }
}


Ambos up-castings anteriores compilaran y se ejecutaran sin excepción, porque un "Perro" ES-UN "Animal", lo que significa que lo que cualquier "Animal" pueda hacer, el "Perro" lo hará. Un "Perro" puede hacer más, pero en este punto, cualquiera con una referencia de "Animal" puede llamar con seguridad a los métodos de "Animal" en una instancia de "Perro". Los métodos de "Animal" pueden haber sido sobre-escritos en la clase "Perro", pero todo lo que importa ahora es saber que un "Perro" puede hacer todo lo que un "Animal" puede hacer. El compilador y la JVM saben esto también, entonces el up-casting implícito es siempre legar para la asignación de un objeto de un subtipo para una referencia a su súper tipo o interface. Si "Perro" implementa la interface "Mascota", y "Mascota" define el método "seAmigable()", un "Perro" puede implícitamente hacer casting a una "Mascota", pero el único método de "Perro" que se podrá invocar es "seAmigable()", el cual "Perro" fue forzado a implementar porque "Perro" implementó a la interface "Mascota".

Una cosa más, si "Perro" implementa a la interface "Mascota", y si "Sabueso" extiende a "Perro", pero "Sabueso" no declara que este implementando a "Mascota", "Sabueso" sigue siendo una "Mascota", "Sabueso" es una "Mascota" simplemente porque este está extendiendo a "Perro". La clase "Sabueso" puede siempre sobre-escribir cualquier método que este heredando desde "Perro", incluyendo los métodos que "Perro" implementa para cumplir con el contrato de la interfaz.

Por último, si "Sabueso" declara que implementa a "Mascota", es solamente para que los que busquen en "Sabueso" puedan ver fácilmente que "Sabueso" ES-UNA "Mascota", sin tener que mirar a la superclase de "Sabueso". "Sabueso" no tiene la necesidad de implementar el método "seAmigable()" si la clase "Perro" (súper clase de "Sabueso") ya se ha ocupado de eso. En otras palabras, si "Sabueso" ES-UN "Perro", y "Perro" ES-UNA "Mascota", entonces "Sabueso" ES-UNA "Mascota", y ya ha cumplido con los métodos de "Mascota" para la aplicación del método "seAmigable()", ya que hereda el método "seAmigable()". El compilador es suficientemente inteligente para decir: "Sé que Sabueso es un Perro, pero está bien para que sea más obvio".

Así que no hay que dejarse engañar por el código que muestra una clase concreta que declara que implementa una interfaz, pero no implementa el método de la interface, antes de poder decir si el código es legal, debemos mirar cuál es la superclase de la clase que esta implementado esta interface. Si alguna clase en el árbol de herencia ya ha implementado métodos concretos y ha declarado que ella (la superclase) implementa la interfaz, entonces la subclase no tiene ninguna obligación de volver a implementar (sobre-escribir) los métodos.


Implementando una interface

Cuando implementamos una interface estamos aceptando el contrato definido por la interfaz. Eso quiere decir que estamos obligados a implementar todos los métodos definidos por la interfaz y que cualquiera que conozca los métodos de la interfaz (no como lo implementa pero si como se llaman y que retornan) puede invocar estos métodos en una clase que implementa la interfaz.

Por ejemplo si creamos una clase que implemente a la interfaz "Runnable" (para que el código pueda ejecutarse en otro un hilo), debemos implementar el método "public void run()", de lo contrario el hilo verá que no tenemos implementado el método "run" de la interface "Runnable" y provocará un error. Afortunadamente, Java impide que esta crisis se produzca mediante la ejecución de un proceso de comprobación, en el compilador, de cualquier clase que pretende implementar una interfaz.

Si la clase está implementando una interface, deberíamos tener una implementación para cada método de la interface, con unas pocas excepciones que veremos en un momento.

Por ejemplo, imaginemos que tenemos la interfaz "Rebotable", con dos métodos "rebotar()" y "setFactorRebote", la siguiente clase compilará:


public class Pelota implements Rebotable  //palabra reservada implements
{ 
    public void rebotar(){}
    public void setFactorRebote(int fr){}
}


El contrato garantiza que una clase tenga todos los métodos de una interface pero no garantiza que tenga una buena o correcta implementación en el cuerpo del método. El compilador nunca se fijará que haya algo entre las llaves del método, nunca dirá que es un método y que debería hacer algo, solo se fija en que tenga los métodos que se describen en la interface, nada más.

Las clases que implementan una interface deben de seguir las mismas reglas para una clase que extiende a una clase abstracta.

Las reglas para que una clase NO abstracta implemente correctamente una interface son las siguientes:

  • Proveer implementación para todos los métodos declarados en la interface.
  • Seguir todas las reglas para una sobre-escritura legal (overrides).
  • No declarar excepciones, en la implementación del método, que no estén en el método definido en la interface. Se pueden declarar subclases de las declaradas por el método de la interfaz.
  • Mantener el mismo nombre del método de la interface y el mismo tipo de dato de retorno (o un subtipo).


Una clase que implementa una interface puede ser abstracta. Por ejemplo:


abstract class Pelota implements Rebotable{}


¿Notan que algo falta?, pues no estamos implementando los métodos de la interfaz "Rebotable", y no tenemos ningún error. Si la clase que implementa una interface es una clase abstracta esta puede simplemente pasar la implementación de los métodos a la primera clase concreta que se implemente.

Por ejemplo si tenemos la clase "PelotaPlaya" y esta extiende de la clase "Pelota", entonces la clase "PelotaPlaya" debe de implementar todos los métodos de la interfaz "Rebotable":


public class PelotaPlaya extends Pelota
{
    /*A pesar que no se dice en la declaración anterior (no hay implements a Rebotable)
      PelotaPlaya tiene que implementar a la interface Rebotable ya que la super clase
      abstracta de PelotaPlaya (Pelota) implementa a Rebotable */
      
    public void rebotar(){}
    
    public void setFactorRebote(int fr){} 
    
    /*Si la clase Pelota hubiera declarado algun método abstracto, entonces
     ese método debería estar implementado aquí también*/
}


A menos que la implementación sea de una clase abstracta, la implementación debe tener todos los métodos definidos por la interface.

Hay dos reglas más que debemos saber:

  • 1. Una clase puede implementar más de una interface, por ejemplo:
  • 
    public class Pelota implements Rebotable, Runnable, Serializable{}
    
    Podemos extender solo a una clase, pero implementar a muchas interfaces. Pero recuerden que la sub-clasificación (extends) define quién y qué es nuestra clase, mientras que la implementación (implements) define una función que puede desempeñar o un sombrero que nuestra clase puede usar, a pesar de lo diferente que podría ser de la otra clase que implemente la misma interface (pero de un árbol de herencia diferente). Por ejemplo: "Persona extends SerHumano", pero una "Persona" también puede ser (implements) "Programador", "Empleado", "Pariente".
  • 2. Una interface puede también extender a otra interface, pero nunca implementar nada, por ejemplo:
  • 
    public interface Rebotable extends Movible{}
    
¿Qué significa esto? La primera clase concreta que implemente "Rebotable" debe implementar todos los métodos de esta interface, pero también debe implementar todos los métodos de la interface "Movible". La sub-interface simplemente está agregando más requerimientos al contrato de la super-interface.

Una interface puede extender a más de una interface, pero sabemos que cuando hablamos de clases esto es ilegal, por ejemplo:


public class Programador extends Empleado, Geek{} //Esto es ilegal!!


Como mencionamos anteriormente una clase no permite extender a más de una clase en Java, una interface, sin embargo, puede implementar a más de una interface, por ejemplo:


public interface Rebotable extends Movible, Inflable //OK
{ 
    void rebotar();
    void setFactorRebote(int fr);
}

interface Movible
{
    void muevete();
}

interface Inflable
{
    void inflate();
}


En el próximo ejemplo, "Pelota" requiere implementar la interface "Rebotable", pero también debe implementar todos los métodos de las interfaces que "Rebotable" ha extendido, incluyendo cualquier interface que aquellas interfaces extienden, y así sucesivamente hasta llegar a la parte superior de la pila. "Pelota" debe tener lo siguiente:


public class Pelota implements Rebotable
{
    public void rebotar(){}               //implementando metodos de la 
    public void setFactorRebote(int fr){} //interface Rebotable

    public void muevete(){} //implementando de Movible

    public void inflate(){} //implementando de Inflable
}


Si la clase "Pelota" no implementa cualquiera de los métodos de "Rebotable", "Movible" o "Inflable", el compilador encontrará errores, al menos que "Pelota" sea una clase abstracta. En el caso de que "Pelota" sea una clase abstracta esta puede implementar todos, algunos o ningún método de las interfaces, puede dejar la implementación a una sub clase concreta de "Pelota", por ejemplo:


abstract class Pelota implements Rebotable
{
    public void rebotar(){}               //define comportamiento de la
    public void setFactorRebote(int fr){} //interface Rebotable

    /*no implementa el resto de los métodos, deja el resto para una sub-clase concreta*/
}

class PelotaFutbol extends Pelota
{
    /*implementando métodos que pelota no hizo*/
    public void muevete(){} //implementando de Movible

    public void inflate(){} //implementando de Inflable

    /*PelotaFutbol puede elegir sobre escribir el método rebotar implementado en pelota*/
    public void rebotar(){} 
}


Comparando ejemplos de abstracto y concreto de extender e implementar:



Ya que "PelotaFutbol" es la primera clase concreta que Implementa "Rebotable", esta debe implementar todos los métodos de la interface "Rebotable", excepto aquellos definidos en la clase abstracta "Pelota". Ya que "Pelota" no provee implementación de los métodos de la interface "Rebotable", es necesario que "PelotaFutbol" implemente todos aquellos métodos.


Tipos Correctos de Retorno

El objetivo de esta sección es cubrir dos aspectos de los tipos de retorno:

  • 1. Qué se puede declarar como un tipo de retorno, y
  • 2. Qué se puede retornar como un valor.
Qué se puede y no se puede declarar es muy sencillo, pero esto depende si estamos sobre-escribiendo un método, heredado o simplemente declarando un método nuevo (el cual incluye sobrecarga de métodos). Daremos solo una pequeña mirada a las diferencias entre las reglas para los tipos de retorno para métodos sobre cargados y sobre-escritos (overloaded y overriding).

Declaración de tipos de retorno

Veremos qué está permitido para declarar como un tipo de retorno, lo cual depende primero en que si estamos sobre-escribiendo, sobrecargando o declarando un nuevo método.

Tipos de retorno en métodos sobre cargados

Recuerden que un método sobrecargado no es más que una forma para la reutilización del mismo nombre de un método pero con diferentes argumentos. Un método sobrecargado es un método completamente diferente de cualquier otro método con el mismo nombre. Si heredamos un método pero lo sobrecargamos este en una subclase, este no está sujeto a las restricciones de sobre-escritura, lo cual implica que podemos declarar cualquier tipo de retorno. Lo que no podemos hacer es cambiar solo el tipo de retorno. Para sobre cargar un método solo debemos cambiar la lista de argumentos, por ejemplo:


public class Animal
{
    void muevete(){};
}

public class Perro extends Animal
{
    String muevete(int numeroPasos)
    {
        return null;
    }
}


Noten que el método de "Animal" tiene un tipo de retorno diferente que el método de "Perro". Esto está bien. En el momento que se ha cambiado la lista de argumentos, hemos sobrecargado el método, entonces el tipo de retorno no tiene que coincidir con el del método de la súper clase, pero por ejemplo, lo siguiente no está permitido:


public class Animal
{
    void muevete(){};
}

public class Perro extends Animal
{
    String muevete() //Error, no se puede cambiar solo el tipo de retorno
    {  
        return null;
    }
}


Sobre escritura y tipos de retorno, retornos covariantes

Cuando una clase quiere cambiar la implementación del método de un método heredado (sobre-escrito), la subclase debe definir un método que debe coincidir exactamente con el método heredado. O, a partir de Java 5, se le permite cambiar el tipo de retorno en el método sobre-escrito siempre y cuando el nuevo tipo de retorno es un subtipo del tipo de retorno declarado en el método a sobre-escribir (superclase).


class Alpha 
{
    Alpha hacerAlgo(char c)
    {
        return new Alpha(); 
    }
}

class Beta extends Alpha
{
    Beta hacerAlgo(char c) //sobre escritura legal en Java 1.5
    { 
        return new Beta();
    }
}


En Java 5 este código compilara sin problemas, pero si intentamos compilar este código con Java 1.4, mostrará el siguiente error:


attempting to use incompatible return type


Otras reglas aplicadas a la sobre-escritura, incluyen aquellas para modificadores de acceso y declaración de excepciones.

Retornando un valor

Tenemos que recordar solo 6 reglas para retornar un valor:

  • 1. Podemos retornar null a los métodos que tengan como tipo de retorno un objeto, ejemplo:
  • 
    public Button hacerAlgo
    {
        return null;
    }
    
  • 2. Podemos declarar un arreglo como tipo de retorno sin problemas, ejemplo:
  • 
    public String[] go()
    {
        return new String[]  {"Alan", "Alex", "Diego", "y Peke", "Henry", "Cesar", "Pedroww"};
    }
    
  • 3. En un método con un tipo de retorno primitivo, podemos retornar cualquier valor o variable que pueda ser implícitamente convertido al tipo de retorno declarado, ejemplo:
  • 
    public int go()
    {
        char c = ‘c’;
        return c; //char es compatible con int
    }
    
  • 4. En un método con un tipo de retorno primitivo, podemos retornar cualquier valor o variable que pueda ser explícitamente convertido al tipo de retorno declarado, ejemplo:
  • 
    public int go()
    {
        float f = 32.5f;
        return (int) f
    }
    
  • 5. No debemos retornar nada desde un método que tiene como tipo de retorno "void", ejemplo:
  • 
    public void go()
    {
        return "Esto es todo"; //Error, no es legal
    }
    
  • 6. En un método con un tipo de retorno con referencia a un objeto, podemos retornar cualquier tipo de objeto que pueda ser implícitamente convertido al tipo de retorno declarado, ejemplo:
  • 
    public Animal getAnimal()
    {
        return new Perro(); //Asumiendo que Perro extiende de Animal
    }
    
    public Object getObject()
    {
        int[] nums =  {1, 2, 3};
        return nums; //retorna un arreglo de enteros, el cual sigue siendo un //objeto
    }
    
    public interface Rebotable{}
    
    public class Pelota implements Rebotable{}
    
    public class TestPelota
    {
        //Método con una interface como tipo de retorno 
        public Rebotable getRebotable()
        {
            return new Pelota(); //Retorna un implementador de la interface
        }
    }
    
Y con eso terminamos todo lo concerniente con las reglas para el retorno de tipos de valor. Ahora veremos algunos detalles interesantes sobre constructores e instanciación de objetos.

Constructores e Instanciación

Los objetos son construidos. No podemos crear un nuevo objeto sin invocar a un constructor. No podemos crear un nuevo objeto sin invocar al constructor del objeto actual y a los constructores de sus superclases.

Los constructores son código que se ejecuta cuando usamos la palabra reservada "new". También pueden ser bloques de inicialización que se ejecutan al escribir "new" (son como constructores pero un poco distintos), pero vamos a cubrir estos (bloques de inicialización), y sus homólogos de la inicialización estática, más adelante. Vamos a ver cómo se codifican los constructores y como trabajan en tiempo de ejecución.

Constructores Básicos

Todas las clases, incluyendo las clases abstractas, deben tener un constructor, pero solo porque una clase deba de tener un constructor no quiere decir que necesitamos escribirlo explícitamente. Un constructor es algo como esto:


public class Rectangulo
{
    Rectangulo(){} //Constructor para la clase Rectangulo
}


Un constructor no retorna ningún tipo de valor. Hay dos cosas que siempre debemos recordar acerca de los constructores:

  1. 1. No retornan ningún tipo de valor y
  2. 2. Llevan el mismo nombre que la clase.
Típicamente los constructores son utilizados para inicializar el estado de las variables de instancia, por ejemplo:


public class Rectangulo
{
    private int ancho;
    private int altura;

    public Rectangulo (int ancho, int altura) 
    {
        this.ancho = ancho;
        this.altura = altura;
    }
}


En el caso anterior, la clase "Rectangulo" no tienen un constructor sin argumentos, lo que significa que el siguiente código fallará en tiempo de compilación:


Rectangulo r = new Rectangulo(); //No compila, no coinciden el constructor


Pero el siguiente código, compilará correctamente:


Rectangulo r = new Rectangulo(4,2); //No hay problema, los argumentos coinciden con el constructor


Es muy común (y deseable) tener un constructor sin argumentos, independientemente del número de constructores sobrecargados que nuestra clase pueda tener (sí, los constructores pueden ser sobrecargados). De vez en cuando se tiene una clase en la que no tiene sentido crear una instancia sin necesidad de suministrar información al constructor. Un "java.awt.Color", por ejemplo, no puede ser llamado mediante una llamada a un constructor sin argumentos, porque es como decirle a la JVM: "Hazme un nuevo color, y no me importa qué tipo de color sea este... tu decide", ¿realmente creen que la JVM pueda tomar decisiones de ese tipo?

Encadenamiento de constructores

Sabemos que los constructores son invocados en tiempo de ejecución cuando usamos la palabra reservada "new" de la siguiente manera:


Perro p = new Perro();


Pero ¿qué es lo que realmente sucede cuando escribimos "new Rectangulo()"?

Asumamos que "Perro" extiende (hereda) de "Animal" y "Animal" extiende de "Object"

  1. 1. El constructor de "Perro" es invocado. Cada constructor invoca al constructor de su superclase con una llamada implícita a "super()", a menos que el constructor invoque a un constructor sobrecargado de la misma clase, pero esto lo veremos más adelante.
  2. 2. El constructor de "Animal" es invocado ("Animal" es la superclase de "Perro")
  3. 3. El constructor de "Object" es invocado ("Object" es la última superclase de todas las clases, entonces "Animal" extiende de "Object" a pesar de que no escribimos explícitamente "extends Object" en la declaración de la clase "Animal". Esto es Implícito), en este punto estamos en el cima de la pila.
  4. 4. Se asignan los valores explícitos a las variables de instancia. Nos referimos a las variables que declaramos como "int x = 24", donde 24 es el valor explicito (en comparación con el valor por defecto) de la variable de instancia.
  5. 5. El constructor de Object se completa.
  6. 6. Se asignan los valores explícitos a las variables de instancia de "Animal" (si tuviera).
  7. 7. El constructor de "Animal" completa.
  8. 8. Se asignan los valores explícitos a las variables de instancia de "Perro" (si tuviera).
  9. 9. El constructor de "Perro" completa.


A continuación mostramos como trabajan los constructores en la pila de llamadas:

  1. 4.- Object()
  2. 3.- Animal() llamada a super()
  3. 2.- Perro() llamada a super()
  4. 1.- main() llamada a new Perro()


Reglas para los constructores:

A continuación mostramos lo que debemos recordar acerca de los constructores:

  • Un constructor puede tener cualquier modificador de acceso, incluso "private" (un constructor privado quiere decir que solo el código dentro de la clase misma puede instanciar un objeto de ese tipo, así que si la clase con constructor privado quiere permitir que una clase pueda instanciarla, la clase debe proveer un método estático o variable que permitan acceder a una instancia creada dentro de la clase).
  • El constructor debe tener el mismo nombre que la clase.
  • El constructor no debe retornar ningún tipo de valor.
  • Es legal (pero tonto) tener un método con el mismo nombre de la clase, pero esto no hace que sea un constructor. Si ven que tiene un tipo de retorno, entonces es un método en vez de un constructor. Podemos tener ambos, un método y un constructor con el mismo nombre (el nombre de la clase) en una misma clase y esto no es problema para Java.
  • Si no escribimos ningún constructor dentro de la clase, un constructor por defecto será automáticamente generado por el compilador.
  • El constructor por defecto SIEMPRE es un constructor sin argumentos.
  • Si deseamos tener un constructor sin argumentos y ya hemos escrito algún otro constructor dentro de la clase, el compilador NO proporcionara un constructor sin argumentos (o cualquier otro constructor). Es decir si hemos escrito algún constructor con argumentos no vamos a tener un constructor sin argumentos a menos que nosotros mismos lo declaremos.
  • Todos los constructores tienen como primera declaración ya sea una llamada a un constructor sobre cargado "this()" o una llamada al constructor de la superclase "super()", aunque recuerden que esta llamada puede ser insertada por el compilador.
  • Si escribimos un constructor (en lugar de confiar en el constructor por defecto generado por el compilador), y no escribimos una llamada a this() o a super(), el compilador insertará una llamada sin argumentos a super() por nosotros, como una primera declaración en el constructor.
  • Una llamada a super() puede ser una llamada sin argumentos o podemos incluir argumentos en él.
  • Un constructor sin argumentos no es necesariamente el constructor por defecto, aunque el constructor por defecto siempre es uno sin argumentos. El constructor por defecto es el único que el compilador provee, pero podemos insertar nuestro propio constructor sin argumentos.
  • No podemos hacer una llamada a un método de instancia o acceder a una variable de instancia hasta que se ejecute el constructor super.
  • Solo variables y métodos estáticos pueden ser accedidos como parte de la llamada a super() o this(). Por ejemplo: super(Animal.NOMBRE) está bien ya que NOMBRE está declarada como variable estática.
  • Las clases abstractas tienen constructores, y estos constructores son siempre llamados cuando una clase concreta es instanciada.
  • Las interfaces no tienen constructores, las interfaces no son parte del árbol de herencia de un objeto.
  • La única forma de que un constructor pueda ser invocado es dentro de otro constructor, en decir, no podemos invocar a un constructor de la siguiente manera:

public class Perro
{
    public Perro(){} //Contructor
    
    public void hacerAlgo()
    {
        Perro(); //llamada al constructor. Error!!
    }
}


Determinar si un constructor por defecto será creado

El siguiente código muestra a la clase "Perro" con dos constructores:


public class Perro
{
    public Perro(){}
    public Perro(String nombre){}
}


¿El compilador insertará un constructor por defecto para la clase anterior? ¡¡NO!!

¿Y para la siguiente modificación en la clase?


public class Perro
{
    public Perro(String nombre){}
}


¿Ahora el compilador insertará un constructor por defecto? ¡¡NO!!

¿Y qué hay de esta clase?


public class Perro
{  
}


El compilador SI generará un constructor por defecto para la clase anterior, porque la clase no tiene ningún constructor definido. Y ahora ¿Qué hay de esta siguiente clase?:


public class Perro
{ 
    void Perro(){}
} 


Podría parecer que el compilador no crea un constructor ya que ya hay un constructor en la clase "Perro". ¿Pero realmente hay un constructor? Revisemos nuevamente la clase anterior, pues eso no es un constructor, es solo un método que tiene el mismo nombre que la clase. Recordemos que el tipo de retorno es un claro indicador de que eso es un método y no un constructor.

¿Cómo se sabe con seguridad que un constructor por defecto será creado?

Porque no declaramos NINGUN constructor en nuestra clase.

¿Cómo se verá el constructor por defecto creado por el compilador?

  • 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 súper constructor (super()).


¿Qué sucede si el súper constructor tiene argumentos?

Los constructores pueden tener argumentos así como los métodos, si tratamos de invocar a un método que toma por ejemplo un int y no le pasamos nada al método, el compilador se comportará de la siguiente manera:


class Ejemplo
{
    void tomaUnInt(int valor){}
}

class Test
{
    public static void main(String … args)
    {
        Ejemplo e = new Ejemplo();
        e.tomaUnInt();  //Se está tratando de invocar al metodo "tomaUnInt()" sin argumentos
    }
}


El compilador nos dirá que estamos tratando de invocar a "tomaUnInt()" sin pasarle ningún un int. El compilador, según la versión de la JVM, responderá más o menos de la siguiente manera:


Test.java:7: tomaUnInt(int) in Ejemplo cannot be applied to ()e. tomaUnInt();


Esto quiere decir que debemos pasar los mismos valores o variables que el método acepta y en el mismo orden en el que esté declarado en el método, a lo que queremos llegar es que este mecanismo funciona exactamente igual en los constructores.

A continuación mostramos el código que genera el compilador con respecto a los constructores:

Código de clase (lo que nosotros escribimos)Código de constructor generado por el compilador
class Perro { }

    class Perro
    {
        Perro()
        {
           super();
        }
    }
    

    class Perro 
    {
       Perro(){}
    }

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

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

    class Perro 
    {
        Perro(String nombre){}
    }

    class Perro
    {
        Perro(String nombre)
        {
           super();
        }
    }

    class Perro
    {
        Perro(String nombre)
        {
           super();
        }
    }
Nada, el compilador no necesita insertar nada

    class Perro 
    {
        void Perro(){}
    }
 
    class Perro  
    {
        void Perro(){}
    
        Perro()
        {
            super();
        }
    }


("void Perro()", es un método no un constructor)


Si el súper constructor (el constructor de su inmediata súper clase o clase padre) tiene argumentos, debemos escribir en la llamada a super() los argumentos adecuados.

Un punto crucial: si nuestra superclase no tiene un constructor sin argumentos, debemos escribir un constructor en la clase (subclase), porque necesitamos un lugar donde insertar, en la llamada a super, los argumentos adecuados.

El siguiente código da un ejemplo del problema:


class Animal
{
    Animal(String nombre){}
}

class Perro extends Animal
{
    Perro()
    {
        super(); //He aquí el problema
    }
}


El compilador nos arrojará algo como esto:


Perro.java:7: cannot resolve symbol
symbol  : constructor Animal  ()
location: class Animal
       super();  // Problema! 
       ^  


Otra forma de explicar es la siguiente: Si nuestra superclase no tiene un constructor sin argumentos entonces la subclase no será capaz de usar el constructor por defecto que te provee el compilador, es decir, el compilador puede solo insertar una llamada a "super()" sin argumentos, no será capaz de compilar algo como esto:


class Animal
{
    Animal(String nombre){}   
}

class Perro extends Animal{}


El compilador nos arrojará algo como esto:


Animal.java:4: cannot resolve symbol
symbol  : constructor Animal ()  
location: class Animal
class Perro extends Animal { }
^


El compilador explícitamente está haciendo el código que se muestra a continuación, donde vamos a proveer a "Perro" el mismo constructor que el compilador le proveerá:


class Animal
{
    Animal(String nombre){}
}

class Perro extends Animal
{
    //el constructor a continuación es idéntico al que el compilador proveerá
    Perro()
    {
        super(); //¡¡Se invoca al constructor sin argumentos de "Animal" el cual no existe!!
    }
}


Una última cosa que debemos recordar es que los constructores no se heredan, estos no son métodos los cuales si se heredan. Estos no pueden ser sobre-escritos pero, como hemos visto a lo largo de esta parte de constructores, estos si se pueden sobrecargar.


Sobrecarga de constructores

Sobrecargar un constructor quiere decir que escribimos diferentes versiones del constructor, cada uno tiene diferente número de argumentos, como por ejemplo:


class Perro
{
    Perro(){} 
    
    Perro(String nombre){}
}


La clase "Perro" anterior muestra dos constructores sobrecargados, uno de ellos toma un String como argumento y el otro no tiene argumentos; este es exactamente igual al constructor que el compilador provee, pero recordemos que una vez que nosotros escribamos un constructor en la clase, como por ejemplo el que toma el String, el compilador ya no nos proveerá un constructor por defecto. Si queremos un constructor sin argumentos para sobrecargar el que tiene argumentos nosotros mismo tendremos que escribirlo como en el ejemplo anterior.

Sobrecargando un constructor proveemos diferentes maneras de que se pueda instanciar un objeto de nuestra clase, por ejemplo si sabemos el nombre del Perro, podemos pasarselo a un constructor de "Perro" que toma una cadena. Pero si no sabemos el nombre podemos llamar al constructor sin argumentos para que nos provea un nombre por defecto, a continuación mostramos un ejemplo de lo que estamos hablando:


1. public class Perro {
2.      String nombre;
3.      Perro (String nombre){
4.              this.nombre = nombre;
5.      }
6. 
7.      Perro(){
8.              this(tomaNombreAleatorio());
9.      }
10. 
11.      static  String tomaNombreAleatorio(){
12.               int x = (int) (Math.random() * 5);
13.               String nombre = new String[]{"fido", "tango", "aguao", "rex" , "lassy"}[x];
14.               return nombre;
15.       }
16. 
17.      public static void main(String … args){
18.               Perro a = new Perro();
19.               System.out.println(a.nombre);
20.               Perro b = new Perro("flafy");
21.               System.out.println(b.nombre);
22.       }
23. }


Ejecutando el código unas cuantas veces tendremos un resultado como el siguiente:


% java Perro 
tango  
flafy
% java Perro 
lassy  
flafy
% java Perro 
rex  
flafy
% java Perro 
aguao
flafy


A continuación mostramos la pila de llamadas para la invocación del constructor cuando un constructor es sobrecargado.

  • 4. Object
  • 3. Perro(String nombre) llamada a super()
  • 2. Perro() llamada a this(tomaNombreAleatorio())
  • 1. main() llamada a new Perro()


Ahora vamos a describir el código desde la parte superior:

  • Línea 2: declaramos una variable de instancia nombre de tipo String
  • Línea 3 – 5: El constructor toma un String y lo asigna a la variable de instancia nombre
  • Línea 7: Aquí viene lo interesante. Asumiendo que cada Animal necesita de un nombre, pero la persona que lo invoca no siempre debe saber que nombre será (código de llamada), entonces nosotros establecemos un nombre aleatorio. El constructor sin argumentos genera un nombre aleatorio invocando al método "tomaNombreAleatorio()".
  • Línea 8: El constructor sin argumentos invoca a su propio constructor sobrecargado que toma un String llamandolo de la misma manera que sería llamado si se hace una nueva instancia del objeto, pasándole un String para el nombre. La invocación al constructor sobrecargado se hace mediante la palabra reservada "this", pero la utiliza como si fuese un nombre de método, this(). Entonces en la línea 8 simplemente se está llamando al constructor sin parámetros que está en la línea 3, pasándole un nombre aleatorio en lugar nosotros establecer el nombre.
  • Línea 11: Notemos que el método "tomaNombreAleatorio()" está marcado como estático (static), eso es porque no podemos invocar a un método de instancia (en otras palabras, no estático) o acceder a una variable de instancia hasta después de que el super constructor haya sido ejecutado, y hasta que el súper constructor sea invocado desde el constructor en la línea 3, en lugar que de la línea 7, la línea 8 puede usar solo un método estático para generar un nombre. Si nosotros quisiéramos especificar algún nombre en específico en vez de que se genere aleatoriamente, por ejemplo, "Thor", entonces en la línea 8 simplemente pondríamos this("Thor") en lugar de llamar al método "tomaNombreAleatorio()" para generar un nombre aleatorio.
  • Línea 12: Esto no tiene nada que ver con el constructor pero es bueno aprenderlo, esto genera un numero entero aleatorio entre 0 y 4;

  • Línea 13: Estamos creando un nuevo String, pero queremos que este String sea seleccionado aleatoriamente desde una lista, entonces necesitamos hacer esto. Para explicarlo con mejor detalle, en esta sola línea de código hacemos lo siguiente:
    1. 1. Declaramos una variable del tipo String.
    2. 2. Creamos un arreglo de Strings (anónimo ya que no asignamos el arreglo)
    3. 3. Obtenemos el String en index [x] (x contiene el numero aleatorio generado en la línea 12) del recientemente creado arreglo de Strings.
    4. 4. Asignamos el String obtenido del arreglo a la variable de instancia "nombre". Esto hubiera sido más fácil de leer si hubiéramos escrito esto:
    
    String[] lista  = {"fido", "tango", "aguao", "rex" , "lassy"};
    String nombre = lista[x];
    


  • Línea 18: Invocamos al constructor sin argumentos (generando un nombre aleatorio que después será enviado al constructor que recibe un String).
  • Línea 20: Invocamos al constructor sobrecargado que toma un String que representa el nombre.


El punto cable en el código antes escrito está en la línea 8, en lugar de llamar a super(), estamos llamando a "this()", y "this()" siempre significa llamar a otro constructor en la misma clase. ¿Pero qué sucede al momento de llamar a "this()"? Tarde o temprano el constructor "super()" será llamado, una llamada a "this()" solo significa que estamos retrasando lo inevitable, ya que algún constructor en algún lugar debe hacer una llamada a "super()".

La regla clave: la primera línea de un constructor debe ser una llamada a "super()" o una llamada a "this()".

Sin excepciones. Si el constructor "A()" tiene una llamada a "this()", el compilador sabe que el constructor "A()" no será e que invoque a "super()".

La regla anterior significa que un constructor nunca podrá tener ambas llamadas, es decir no podrá llamar a la vez a "this()" y a "super()". Porque alguna de estas llamadas debe ser la primera sentencia en un constructor, no podemos usar ambas en un mismo constructor. El compilador tampoco insertará una llamada a "super()" si el constructor tiene una llamada a "this()".

¿Qué sucederá si nosotros tratamos de compilar el siguiente código?


class A
{
    A()
    {
        this("test");
    }

    A(String s)
    {
        this();
    }
}


El compilador puede que no capte el problema (esto depende del compilador). Este asume que sabemos lo que estamos haciendo. ¿Captan el problema? Sabemos que el súper constructor siempre debe de ser llamado, entonces ¿Dónde podría ir la llamada a "super()"? Recordemos que el compilador no inserta ningún constructor por defecto si nosotros ya insertamos uno o más constructores en nuestra clase, y cuando el compilador no inserta un constructor por defecto todavía puede insertar una llamada a super() en algún constructor que no tenga explícitamente una llamada a super(), a menos que, el constructor ya tenga una llamada a this() y recordemos que un constructor no puede tener ambas llamadas (this() y super()). Entonces en el código anterior ¿Dónde va la llamada a super()? Si los únicos dos constructores en la clase tienen una llamada a this(), en efecto nosotros tenemos el mismo problema que tendríamos si escribimos los siguientes métodos:


public void ir()
{
    hacerAlgo();
}

public void hacerAlgo()
{
 ir();
}


¿Ahora pueden ver el problema? Podemos ver que el stack explota. Ya que se llamaran uno a otro indefinidamente hasta que la maquina o la JVM estalle (esperamos que lo segundo suceda primero… ¿o lo primero?). Regresando al ejemplo de los constructores podemos decir que dos constructores sobrecargados y ambos con una llamada a this() son dos constructores llamándose uno a otro una y otra y otra vez, resultando en:


% java A
Exception in thread "main" java.lang.StackOverflowError


El beneficio de tener constructores sobrecargados es que ofrecen formas flexibles de poder instanciar un objeto de nuestra clase. El beneficio de que un constructor invoque a otro constructor sobrecargado es evitar duplicación de código, en el ejemplo anterior no hubo algún otro código para establecer el nombre, pero imaginen que después de la línea 4 aún hay muchas cosas que podríamos hacer.


Variables y métodos estáticos

El modificador "static" tiene un impacto tan profundo en el comportamiento de un método o una variable que debemos tratando como un concepto totalmente separado de los demás modificadores. Para entender la manera en que un miembro estático trabaja, primero veremos las razones por las cuales utilizaríamos alguno.

Imaginen que tenemos una clase de utilidad la cual siempre se ejecuta de la misma manera, su única función es devolver, digamos, un número al azar. No nos importa en cuál instancia de la clase se ejecuta el método, ya que siempre se comporta de la misma manera. En otras palabras, el comportamiento del método no tiene ninguna dependencia en el estado (valores de variable de instancia) de un objeto. Entonces ¿Porque es necesario un objeto cuando el método nunca será instanciado? ¿Por qué no solo pedimos a la clase en si misma que ejecute el método?

Ahora imaginemos otro escenario: supongamos que queremos mantener un contador corriendo para todas las instancias de una clase en particular. ¿En dónde incluiremos la variable? No va a trabajar si la incluimos como variable de instancia dentro de las clases en las cuales se quiere hacer el seguimiento, ya que el contador siempre se iniciara a un nuevo valor por defecto cada que nosotros creamos una nueva instancia.

La respuesta a los dos escenarios anteriores (el método de utilidad que se ejecuta siempre de la misma manera, y el contador para mantener el total de instancias actualizado) es utilizar el modificador static.

Las variables y métodos marcados con static pertenecen a la clase y no a una instancia en particular. Podemos usar un método o variable static sin tener instancias de esa clase, solo necesitamos que la clase esté disponible para invocar un método static o acceder a una variable static.

Una variable estática de una clase será compartida por todas las instancias de esa clase, sólo hay una copia.

El siguiente código utiliza una variable static que será utilizada como contador:


class Oveja
{
    static int contadorOvejas = 0; //Declaramos e inicializamos la variable estática 
 
    public Oveja()
    {
        contadorOveja += 1; //Modificamos el valor en el constructor
    }

    public static void main(String … args)
    {
        new Oveja();
        new Oveja();
        new Oveja();

        System.out.println("El contador de ovejas está ahora en: " + contadorOveja);
    }
}


En el código anterior, la variable estática "contadorOveja" es establecida en cero cuando la clase "Oveja" es cargada por primera vez por la JVM, antes de que cualquier instancia de "Oveja" sea creada (en realidad no se necesita inicializar la variable estática en cero, las variable estáticas obtienen los mismos valores por defecto que las variables de instancia obtienen). Cada vez que una instancia de la clase "Oveja" es creada el constructor de "Oveja" es ejecutado y la variable "contadorOveja" es incrementada. Cuando este código es ejecutado, tres instancias de "Oveja" son creadas en el "main()", y el resultado es el siguiente:


El contador de ovejas está ahora en: 3


Ahora imaginemos que sucedería si la variable "contadorOveja" fuera una variable de instancia (es decir, una variable no estática):


class Oveja
{
    int contadorOvejas = 0; //Declaramos e inicializamos la variable de instancia  
 
    public Oveja()
    {
        contadorOveja += 1; //Modificamos el valor en el constructor
    }

    public static void main(String... args)
    {
        new Oveja();
        new Oveja();
        new Oveja();

        System.out.println("El contador de ovejas está ahora en: " + contadorOveja);
    }
}


Cuando este código es ejecutado, este puede todavía crear tres instancias de la clase "Oveja" en el "main()", pero el resultado es un error de compilación. No podemos compilar este código, mucho menos ejecutarlo, porque obtenemos el siguiente error:


Exception in thread "main" java.lang.RuntimeException: Uncompilable source code - non-static variable contadorOveja cannot be referenced from a static context
System.out.println("El contador de ovejas está ahora en: " + contadorOveja);
^


La JVM no sabe a cuál objeto "contadorOveja" de "Oveja" estamos intentando de acceder. El problema está en que el método "main()" en sí mismo es un método estático. Un método estático no puede acceder a una variable no estática (variable de instancia) porque hay que recordar que para acceder a una variable estática no se necesita una instancia de la clase, ya que la variable pertenece a la clase misma.

Eso no quiere decir que no hay instancias de la clase con vida en la heap, si las hay, pero el método estático no sabe nada de ellas.

Lo mismo se aplica a los métodos de instancia, un método estático no pueden invocar directamente un método no estático.

Piensen que static=clase, no-estático=instancia. Haciendo que el método llamado por la JVM ("main()") sea un método estático la JVM no tiene que crear una instancia de la clase sólo para iniciar la ejecución de código.


Accediendo a métodos y variables estáticos

Puesto que no es necesario tener una instancia para invocar un método estático o acceder a una variable estática, entonces ¿cómo invocar o utilizar un miembro estático? ¿Cuál es la sintaxis? Sabemos que con un método de instancia regular, se utiliza el operador punto "." en una referencia, por ejemplo:

class Oveja
{
    static int contadorOvejas = 0; //Declaramos e inicializamos la variable estática  
 
    public Oveja()
    {
        contadorOveja += 1; //Modificamos el valor en el constructor
    }

    public static void main(String... args)
    {
        new Oveja();
        new Oveja();
        new Oveja();
        System.out.println("El contador de ovejas está en: " + Oveja.contadorOveja);
    }
}


Pero para que sea realmente confuso, el lenguaje Java también permite el uso de una variable de referencia de objeto para acceder a un miembro estático:


    Oveja ov = new Oveja();
    ov.contadorOveja; //Accediendo a la variable estática contadorOveja usando o


En el código anterior, hemos instanciado una "Oveja" asignando "new Oveja" a la variable de referencia "ov", y después usamos la referencia "ov" para invocar al método estático. Pero a pesar de que se está utilizando una instancia específica para acceder al método estático, las reglas no han cambiado. Esto no es más que un truco de sintaxis que nos permite utilizar una variable de referencia a objeto (pero no el objeto que se refiere) para llegar a un método o variable estática, pero el miembro estático sigue ignorando la instancia utiliza para invocar al miembro estático. En el ejemplo de la Oveja, el compilador sabe que la variable de referencia "ov" es del tipo de Oveja, por lo que el método estático de la clase Oveja se ejecuta sin conocimiento o interés para la instancia de la Oveja del otro extremo de la referencia "ov". En otras palabras al compilador le importa solo que la variable de referencia "ov" sea del tipo Oveja.

El compilador en realidad hace una transformación a la llamada que acabamos de hacer. Cuando ve que estamos tratando de invocar a un miembro estático, reemplaza la variable de instancia que estamos usando por la clase en la que está dicho miembro. Esto quiere decir que reemplazara esta llamada:


    Oveja ov = new Oveja();
    ov.contadorOveja;


Por esta otra:


    Oveja ov = new Oveja();
    Oveja.contadorOveja;


Así que como podemos ver, finalmente quedamos con una llamada al miembro estático, a través de la clase a la que pertenece dicho miembro.

La siguiente imagen describe el efecto del modificador static en métodos y variables:



Finalmente, recuerden que los métodos estáticos no pueden ser sobrescritos. Pero esto no significa que no puedan ser redefinidos en una subclase. Redefinir y sobrescribir no son la misma cosa. A continuación mostramos un ejemplo sobre redefinir (no sobrescribir) un método marcado como static:


class Animal
{
    static void hacerAlgo()
    {
        System.out.println("a");
    }
}

class Perro extends Animal
{
    static void hacerAlgo()
    {
        System.out.println("b"); //Esto es redefinir, no sobrescribir
    }

    public static void main(String... args)
    {
        Animal[] a = {new Animal(), new Perro(), new Animal()};

        for(int i = 0; i < a.length; i++ )
        {
            a[i].hacerAlgo(); //Invoca al método estático 
        }
    }
}


Ejecutando el código anterior se produce la siguiente salida:


a   a   a


Recuerden, la sintaxis "a[i].hacerAlgo()" es solo un atajo (un truco de sintaxis). El compilador lo sustituye con algo como "Animal.hacerAlgo()", como vimos hace un momento.


Cohesión y acoplamiento (Coupling and Cohesion):

Estos dos temas, la cohesión y el acoplamiento, tienen que ver con la calidad de un diseño orientado a objetos. En general un buen diseño pide un acoplamiento bajo y evita un acoplamiento estrecho y un buen diseño orientado a objetos pide una alta cohesión y evita la baja cohesión. Como con la mayoría de las discusiones sobre diseño de orientación a objetos, las metas de una aplicación son:

  • Facilidad de creación.
  • Facilidad de mantenimiento.
  • Facilidad de mejora.



Acoplamiento

Vamos a empezar haciendo un intento sobre la definición de acoplamiento. Acoplamiento es el grado en el cual una clase sabe acerca de otra clase. Si el único conocimiento que la clase "A" tiene acerca de la clase "B", es lo que la clase "B" ha puesto de manifiesto a través de su interface, se dice que la clase "A" y "B" están débilmente acopladas, lo cual es algo bueno. Si por otro lado, la clase "A" se basa en una parte de la clase "B", que no es parte de la interface de la clase "B", entonces el acoplamiento entre estas dos clases es más estrecho, y esto no es algo bueno. En otras palabras, si la clase "A" sabe más de lo que debería de la forma en que se implementó "B", entonces "A" y "B" están estrechamente acopladas.

Usando este segundo escenario, imaginemos lo que sucede cuando la clase "B" sea mejorada. Es muy posible que el desarrollador que mejore a "B" no tenga conocimiento acerca de "A" ¿Y porque debería de tenerlo? El desarrollador de la clase "B" debe pensar (y es correcto) que todas las mejoras que no quiebren o rompan la interfaz de la clase deben ser seguras, por lo que podría cambiar alguna parte que no tenga que ver con la interface en la clase. Si las dos clases están estrechamente acopladas, este cambio provocaría que la clase "A" se quiebre.

Veamos un ejemplo obvio de acoplamiento fuerte, que ha sido posible gracias a una pobre encapsulación:


class Impuestos
{
    float ratio;
    
    float calculaImpuestoVentaPeru()
    {
        RatioImpuestoVentas riv = new RatioImpuestoVentas(); 
        ratio = riv.ratioVentas;  /*Mal, esto debería ser una llamada a un 
                                    método get para obtener la variable, por ejemplo:
                                    ratio = riv.getRatioVentas("PE");  */
    }
}

class RatiosImpuestoVentas
{
    public float ratioVentas;       //Debería ser privado
    public float ajusteRatioVentas; //Debería ser privado
 
    public float getRatioVentas(String pais)
    {
        ratioVentas = new Impuestos().calculaImpuestoVentaPeru(); //Mal otra vez hacer basado en los cálculos de la región.
        
        return ajusteRatioVentas;
    }
}


Todas las aplicaciones orientadas a objetos que no son triviales son una mezcla de muchas clases e interfaces trabajando juntas. Idealmente, todas las interacciones entre los objetos en un sistema orientado a objetos deben utilizar sus APIs, en otras palabras, los contratos de las clases de los objetos respectivos. Teóricamente, si todas las clases en una aplicación tienen bien diseñada sus APIs, entonces debería ser posible para todas las interacciones entre las clases el uso de las APIs de forma exclusiva. Como hemos comentado anteriormente en este capítulo, un aspecto de buen diseño de una clase y el diseño de la API es que las clases deben estar bien encapsuladas.


Cohesión

Mientras que el acoplamiento tiene que ver con cómo las clases interactúan con las otras clases, la cohesión es acerca de cómo una simple clase es diseñada. El termino cohesión es usado para indicar el grado para el cual una clase tiene un propósito simple bien enfocado . Hay que mantener en mente que la cohesión es un concepto subjetivo al igual que el acoplamiento. Cuanto más enfocada sea la clase en una sola tarea, mayor su cohesión, lo cual es bueno. El beneficio clave de la alta cohesión, es que estas clases son mucho más fáciles de mantener (y menos frecuentes a ser cambiadas) que las clases con baja cohesión. Otro beneficio de la alta cohesión es que las clases con un propósito bien enfocado tienden a ser más reutilizables que otras clases. Veamos un ejemplo:


class ReportePresupuesto
{
    void conectarBaseDatos(){}
    void generarReportePresupuesto(){}
    void guardarArchivo(){}
    void imprimir(){}
}


Ahora imaginen que su jefe llega y dice, "Eh, ¿sabes de la aplicación de contabilidad que estamos trabajando? Los clientes decidieron que también van a querer generar un informe de proyección de ingresos, y que quieren hacer algunos informes de inventario también".

También les dice que se aseguren de que todos estos informes les permitan elegir una base de datos, seleccione una impresora, y guardar los informes generados a ficheros de datos ... ¡Ouch!

En lugar de poner todo el código de impresión en una clase de informe, probablemente hubiera sido mejor usar el siguiente diseño desde el principio:


class ReportePresupuesto
{
    Opciones getOpcionesReporte() {}
    void generarReportePresupuesto(Opciones o){} 
}

class ConexionBaseDatos
{
    ConexionBD getConexion(){}
}

class Impresion
{
    OpcionesImpresion getOpcionesImpresion(){}
}

class AlmacenArchivos
{
    OpcionesGuardado getOpcionesGuardado(){}
}


Este diseño es mucho más cohesivo. En lugar de una clase que hace todo, hemos roto el sistema en cuatro clases principales, cada uno con un rol muy específico (cohesión).

Como hemos construido estas clases especializadas, reutilizables, y va a ser mucho más fácil escribir un nuevo informe, dado que ya tenemos la clase de conexión de bases de datos, la clase de impresión, y la clase para guardar archivos, y eso significa pueden ser reutilizados por otras clases pueden desear imprimir un informe.

Este es el fin del segundo tutorial, donde hemos aprendido un poco sobre algunos de los conceptos más importantes de la orientación a objetos que son necesarios para el examen de certificación, y para nuestra vida como programadores en general.El la próxima entrega seguiremos aprendiendo más temas importantes para nosotros como programadores Java y en particular para los que deseen certificarse como programadores Java.

Muchas gracias a Alan Cabrera Avanto, de Trujillo Perú por este tutorial.

Saludos.

Entradas Relacionadas: