Override/Overload en Java

Buenas tardes, en esta entrada vamos a ver lo que es hacer Override y Overload en Java, vamos a aprender a determinar si un método ha sido correctamente sustituido o sobrecargado, e identificar los valores de retorno legales para los métodos. Tambien veremos como declarar y/o invocar métodos sustituidos o sobrecargados y el código que declara y/o invoca los constructores de la superclase, sustituidos o sobrecargados.


  • Métodos Sustituidos

    Cada vez que tengamos una clase que ha heredado un método de la superclase, tenemos la oportunidad de sustituir el método. El beneficio de sustituir el método es tener la habilidad de definir el comportamiento específico de una subclase. En el siguiente ejemplo mostramos una clase Horse, sustituyendo el método eat() de su superclase Animal:

    				public class Animal {
    					public void eat(){
    						System.out.println("Un Animal generico comiendo genericamente");
    					}
    				}
    				
    				class Horse extends Animal{
    					public void eat(){
    						System.out.println("Caballo comiendo paja");
    					}
    				}
    				

    Para métodos abstractos que heredamos de una superclase, no tenemos otra opción. Debemos implementar el método en la subclase aunque la subclase sea tambien abstracta.

    El creador de la clase Animal puede haber decidido esto para propósitos del polimorfismo, todos los subtipos de Animal deberían tener un método eat() definido de una manera única y específica. Polimorficamente, cuando alguien tiene una referencia a la clase Animal que no se refiere a una instancia de Animal, pero si a una instancia de una subclase de Animal, aquel que lo llame podría invocar el método eat() en la referencia de Animal, pero el objeto en tiempo de ejecución arrancará su propia versión del método eat(). Un ejemplo del uso del polimorfismo podría ser el siguiente:

    				public class TestAnimals{
    					public static void main (String [] args){
    						Animal a = new Animal();
    						Animal b = new Horse(); // Referencia a Animal, pero objeto Horse
    						a.eat(); // Ejecuta la versión Animal de eat()
    						b.eat(); // Ejecuta la versión Horse de eat()
    							
    					}
    				}
    				class Animal {
    					public void eat(){
    						System.out.println("Un Animal generico comiendo genericamente");
    					}
    				}
    				
    				class Horse extends Animal{
    					public void eat(){
    						System.out.println("Caballo comiendo paja");
    					}
    					
    					public void buck(){
    						
    					}
    				}
    				

    En el código anterior, la clase Test usa una referencia a un Animal para invocar un método del objeto Horse. El compilador permitirá solo métodos de la clase Animal a ser invocados cuando se use una referencia a un Animal. Lo siguiente no sería legal dando el código que le precede:

    				Animal c = new Horse();
    				c.buck(); 	// No se puede invocar a buck()
    							// La clase Animal no tiene ese método
    				

    Para reiterar, el compilador solo mira el tipo de referencia, no el tipo de instancia. El polimorfismo te permite usar un supertipo mas abstracto para referirse a uno de sus subtipos.

    El método sustituido no puede tener un modificador mas restrictivo que el metódo que está siendo sustituido, por ejemplo, no podemos sustituid un método marcado como public y hacerlo protected. Pensemos en ello: si una clase Animal tiene el método eat() como público y alguien tiene una referencia a Animal (En otras palabras, uan referencia declarada del tipo Animal), ese alguien asume que es seguro llamar al método eat() en la referencia de Animal sin importarle la actual isntancia a la que Animal está refiriendo. Vamos a modificar el ejemplo del polimorfismo que vimos antes:

    				public class TestAnimals{
    					public static void main (String [] args){
    						Animal a = new Animal();
    						Animal b = new Horse(); // Referencia a Animal, pero objeto Horse
    						a.eat(); // Ejecuta la versión Animal de eat()
    						b.eat(); // Ejecuta la versión Horse de eat()
    							
    						Animal c = new Horse();
    						c.buck(); 	// No se puede invocar a buck()
    									// La clase Animal no tiene ese método
    								
    					}
    				}
    				class Animal {
    					public void eat(){
    						System.out.println("Un Animal generico comiendo genericamente");
    					}
    				}
    				
    				class Horse extends Animal{
    					private void eat(){ //Es privado!
    						System.out.println("Caballo comiendo paja");
    					}
    					
    					public void buck(){
    						
    					}
    				}
    				

    En este código compilado (que no lo hará), podremos ver que no se puede reducir la visibilidad del método eat().

    La variable b es del tipo Animal, que tiene un método public eath(). Pero recordemos que en tiempo de ejecución, Java usa la invocación virtual del método para seleccionar dinámicamente la versión actual del método que será ejecutado, basado en la instancia actual. Una referencia a Animal puede siempre referirse a una isntancia de Horse, porque Horse ES-UN Animal. Lo que hace que la referencia de la superclase a la instancia de la subclase sea posible es que la subclase tiene garantizado a hacer todo lo que la superclase puede hacer.

    Las reglas para sustituir un método son las siguientes:

    • La lista de argumentos debe coincidir exactamente con la del método sustituido. Si no coinciden, podemos estar sobrecargando el método, cosa que no queremos hacer.

    • El tipo de retorno debe de ser el mismo, o un subtipo de el, el tipo de retorno declarado en el método sustituido de la superclase.

    • El nivel de acceso no puede ser mas restrictivo que el del método sustituido.

    • El nivel de acceso PUEDE ser menos restrictivo que el método que esta sustituyendo.

    • Los métodos de Instancia pueden ser sustituidos solo si lo han heredado por la subclase. Una subclase dentro del mismo paquete en el que se encuentra la instancia a la superclase puede ser sustiuido por cualquier método de la superclase que no esté marcado como private o final. Una subclase en un paquete diferente puede sustituir solo los métodos no finales marcados como public, protected.

    • El método sustituido PUEDE lanzar cualquier excepción sin marcar, sin contar donde el método sustituido declare la excepción.

    • El método sustituido NO debe lanzar excepciones marcadas que son nuevas o mas amplias que las declaradas por el método sustituido. Por ejemplo, un método que declara un FileNotFoundException no puede ser sustituido por un método que declare un SQLException, Exception, o cualquier otra excepcion a no ser que sea subclase de la subclase FileNotFoundException.

    • El método sustituido puede lanzar excepciones mas reducidas.

    • No se puede sustituir un método marcado como final.

    • No se puede sustituir un método marcado como static.

    • Si un método no puede ser heredado, no se puede sustituir. Recordemos que sustituir un método implica que estamos reimplementando ese método que hemos heredado, por ejemplo, el siguiente código no sería legal:

      						public class TestAnimals{
      							public static void main (String [] args){
      								Horse h = new Horse();
      								h.eat(); // No es legal porque Horse no puede heredar eat();
      								
      							}
      						}
      						class Animal {
      							private void eat(){
      								System.out.println("Un Animal generico comiendo genericamente");
      							}
      						}
      						
      						class Horse extends Animal{
      							
      						}
      						

    Invocando la versión de la Superclase de un Método Sustituido

    A veces, querremos tener ventajas de algó del código de al versión del método de la superclase, aunque lo sustituyamos para proveer un comportamiento adicional específico. es como decir “Vamos a la versión del método que está en la superclase, entonces volvemos aquí abajo y lo terminamos con el código del método adicional de la subclase”. Es facil de hacer en código usando la palabra clave super como vemos a continuación:

    				public class Animal{
    					public void eat(){
    						
    					}
    					
    					public void printYourself(){
    						// Código útil aquí
    					}
    				}
    				
    				class Horse extends Animal{
    					public void printYourself(){
    						// Cogemos las ventajas del código de ANimal, y entonces añadimos mas
    						super.printYourself();
    						
    						// Añadimos mas cosas
    					}
    				}
    				

    Recordemos, los métodos marcados como static no pueden ser sustituidos.

    Ejemplos legales e ilegales de Métodos sustituidos

    Tomando como referencia el método eat() siguiente veremos unos ejemplos que no son legales:

    				public class Animal{
    					public void eat(){ }
    				}	
    				
    Código de Sustitución Ilegal Problema con el código
    private void eat(){ } El modificador de acceso es mas restrictivo
    public void eat() throws IOException { } Declara una excepción marcada que no está definida en al versión de la superclase
    public void eat(String food){ } Una sobrecarga legal, pero no una sustitución, porque la lista de argumentos ha cambiado
    public String eat() { } No es una sustitución porque el tipo de retorno es diferente, no es una sobrecarga tampoco porque no ha cambiado la lista d eargumentos.
  • Métodos Sobrecargados

    Los métodos sobrecargados permiten reusar el mismo nombre del método en una clase, pero con diferentes argumentos (Y opcionalmente, un tipo de valor de retorno diferente). Sobrecargar un método a veces implica que somos algo mas agradables a quien llama a nuestros métodos, porque nuestro código coge diferentes tipos de argumentos en vez de forzar la conversión al invocar los métodos. Las reglas son simples:

    • Los métodos sobrecargados DEBEN cambiar la lista de argumentos.

    • Los métodos sobrecargados PUEDEN cambiar el tipo de retorno.

    • Los métodos sobrecargados PUEDEN cambiar el modificador de acceso.

    • Los métodos sobrecargados PUEDEN declarar nuevas o mas amplias excepciones.

    • Un método puede ser sobrecargado en la misma clase o en una subclase. En otras palabras, si una clase A define un método doStuff(int i), la subclase B podría definir un doStuff(String s) sin tener que sustituir la versión de la superclase que coge un tipo de dato int. Por lo que 2 métodos con el mismo nombre pero en diferentes clases, pueden mantenerse considerados como sobrecargados, si la subclase hereda una versión del método y entonces declara otra version sobrecargada en su propia definición de la clase.

    Sobrecargas legales

    Vamos a echar un vistazo a un método que queremos sobrecargar:

    				public void changeSize(int size, String name, float pattern){ }
    				

    Los siguientes métodos son sobrecargas legales del método changeSize() :

    				public void changeSize(int size, String name){}
    				public int changeSize(int Size, float pattern){}
    				public void changeSize(float pattern, String name) throws IOException{}
    				

    Invocando métodos sobrecargados

    Cuando un método es invocado, mas de un método con el mimo nombre puede exister para el tipo de objeto que estamos invocando en el método. Por ejemplo, la clase Horse puede tner 3 métodos con el mismo nombre pero con diferente lista de argumentos, lo cual significa que el método está sobrecargado.

    Decidir cual de los métodos se va a invocar se basa en los argumentos. Si invocamos un método con un argumento de tipo String, la versión sobrecargada que coge el String es la que es llamada. Si invocamos un método con el mismo nombre pero pasamos un float, la version sobrecargada que contiene el float será ejecutada. Si invocamos el método con el mismo nombre pero que acepta un objeto Foo, y no hay una versión sobrecargada que coja el objeto Foo, entonces el compilador nos dirá que no ha podido encontrar una versión compatible con ese método. Lo siguiente son ejemplos de invocar métodos sobrecargados:

    				class Adder{
    					public int addThem(int x, int y){
    						return x+y;
    					}
    					
    					//Sobrecarga del método addThem para añadir double en vez de ints
    					public double addThem(double x, double y){
    						return x+y;
    					}
    				
    				}
    				
    				//Desde otra clase, invocamos el método addThem()
    				public class TestAdder{
    					public static void main (String [] args){
    						Adder a = new Adder();
    						int b = 27;
    						int c = 3;
    						int result = a.addThem(b, c); // ¿Que addThem es invocado?
    						double doubleResult = a.addThem(22.5, 9.3); // ¿Que addThem?
    					}
    				}
    				

    En el ejemplo anterior, en el código de TestAdder, la primera llamada a a.addThem(b,c) pasa 2 enteros al método, por lo cual se llama a la primera versión de addThem(). La segunda llamada a a.addThem(22.5, 9.3) pasa 2 double al método, por lo que una segunda versión de addThem() es invocado.

    Invocar métodos sobrecargados que cogen referencias a objetos en vez de tipos de dato primitivos son un poco mas interesantes. Vamos a decir que tenemos un método sobrecargado tal que una versión coge un Animal y otro coge un Horse (subclase de Animal). Si pasamos un objeto Horse en la invocación al método, estaremos invocando la versión que coge un Horse. o eso parece a primera vista:

    				class Animal{ }
    				class Horse extends Animal{ }
    				class UseAnimals{
    					public void doStuff(Animal a){
    						System.out.println("En la versión de Animal");
    					}
    					
    					public void doStuff(Horse h){
    						System.out.println("En la versión de Horse");
    					}
    					
    					public static void main (String [] args){
    						UseAnimals ua = new UseAnimals();
    						Animal animalObj = new Animal();
    						Horse horseObj = new Horse();
    						ua.doStuff(animalObj);
    						ua.doStuff(horseObj);
    					}
    				}
    				

    La salida será:

    				En la versión de Animal
    				En la versión de Horse
    				

    Pero que pasa si usamos una referencia a Animal en un objeto Horse:

    				Animal animalRefToHorse = new Horse();
    				ua.doStuff(animalRefToHorse);
    				

    ¿Cuál de las versiones sobrecargadas es invocada? Podríamos decir: “El que coge Horse, ya que es un objeto Horse en tiempo de eejcución que está siendo pasado al método”. Pero no es así como funciona. El código anterior actualmente imprimiría en pantalla:

    				En la versión de Animal
    				

    Aunque el objeto actual en tiempo de ejecución es un Horse y no un Animal, la elección de cual método sobrecargado a llamar (en otras palabras, la firma del método) NO es decidido dinámicamente decidido en tiempo de ejecución. Recordemos, el tipo de referencia (no el tipo del objeto) determina cual de los métodos sobrecargados es el invocado. Para resumir, que versión sustituida del método se llama (en otras palabras, desde que clase en el árbol de herencia) es decidido en tiempo de ejecución basandose en el tipo de objeto, pero cual versión sobrecargada del método a llamar está basado en el tipo de referencia del argumento pasado en tiempo de compilación.

    Polimorfismo en Métodos Sobrecargados y Sustituidos

    ¿Como trabaja el polimorfismo con los métodos sobrecargados? Por lo que hemos visto, no parece que el polimorfismo importe cuando un método es sobrecargado. Si pasamos una referencia a un Animal, el método sobrecargado que coge el argumento de Animal será invocado, aunque el objeto actual pasado sea un Horse. Una vez que Horse se enmascara como un Animal coge esté método, sin embargo, el objeto Horse se mantiene como Horse a pesar de estar siendo pasado en un método que espera un Animal. Por lo que es verdad que el polimorfismo no determina cual versión sobrecargada es llamada, el polimorfismo entra en juego cuando la decisión de que versión sustituida de un método es llamada. Pero a veces, un método está sobrecargado y sustituido. Imagina una clase Animal y una clase Horse que son así:

    				class Animal{ 
    					public void eat(){
    						System.out.println("Animal genérico comiendo");
    					}
    				}
    				
    				public class Horse extends Animal{ 
    					public void eat(){
    						System.out.println("Horse comiendo paja");
    					}
    					
    					public void eat(String s){
    						System.out.println("Hose comiendo " + s);
    					}
    				}
    				
    Código de Invocación del método Resultado
    Animal a = new Animal();
    a.eat();
    Animal genérico comiendo
    Horse h = new Horse();
    h.eat();
    Horse comiendo paja
    Animal ah = new Horse();
    ah.eat();
    Horse comiendo paja
    Polimorficamente funciona–El tipo de objeto (Horse), no el tipo de referencia (Animal) es usado para determinar que método eat() se llama.
    Horse he = new Horse();
    he.eat(“Apples”);
    Horse comiendo Apples
    El método sobrecargado eat(String s) es invocado
    Animal a2 = new Animal();
    a2.eat(“Treats”);
    Error de compilación. El compilador ve que la clase Animal no tiene un método eat() que coja un String.
    Animal ah2 = new Horse();
    ah2.eat(“Carrots”);
    Error de Compilación. El compilador se mantiene mirando solo la referencia, y ve que Animal no tiene ningúin método eat() que coja un String.
    Método Sobrecargado Método Sustituido
    Argumentos Deben cambiar No deben cambiar
    Tipo de retorno Puede cambiar No puede cambiar excepto retorno covariante
    Excepciones Puede cambiar Puede reducir o eliminar. No puede ser lanzada ninguna nueva o mas amplia
    Acceso Puede cambiar No debe hacerlo mas restrictivo (Pero si ser menos restrictivo)
    Invocación El tipo de referencia determina cual versión sobrecargada es seleccionada. Ocurre en tiempo de compilación. El método actual que es invocado se mantiene como invocación virtual del método que ocurre en tiempo de ejecución. El tipo de objeto determina cual de los métodos es llamado

Hasta aquí lo referente un poco al tema de la sobrecarga y la sustitución de métodos, o Override/Overload, que es como lo veremos con bastante mas frecuencia por internet, y de hecho son los nombres que deberíamos usar.

Sin mas, cualquier corrección o aporte es bienvenido!.

Saludos!!!