Sobrecarga y anulación de métodos en Java

1. Información general

La sobrecarga y anulación de métodos son conceptos clave del lenguaje de programación Java y, como tales, merecen una mirada en profundidad.

En este artículo, aprenderemos los conceptos básicos de estos conceptos y veremos en qué situaciones pueden resultar útiles.

2. Sobrecarga de métodos

La sobrecarga de métodos es un mecanismo poderoso que nos permite definir API de clases cohesivas. Para comprender mejor por qué la sobrecarga de métodos es una característica tan valiosa, veamos un ejemplo simple.

Supongamos que hemos escrito una clase de utilidad ingenua que implementa diferentes métodos para multiplicar dos números, tres números, etc.

Si le hemos dado a los métodos nombres engañosos o ambiguos, como multiplicar2 () , multiplicar3 () , multiplicar4 (), entonces esa sería una API de clase mal diseñada. Aquí es donde entra en juego la sobrecarga de métodos.

En pocas palabras, podemos implementar la sobrecarga de métodos de dos formas diferentes:

  • implementar dos o más métodos que tienen el mismo nombre pero que toman diferentes números de argumentos
  • implementar dos o más métodos que tienen el mismo nombre pero que toman argumentos de diferentes tipos

2.1. Diferentes números de argumentos

La clase Multiplier muestra, en pocas palabras, cómo sobrecargar el método multiply () simplemente definiendo dos implementaciones que toman diferentes números de argumentos:

public class Multiplier { public int multiply(int a, int b) { return a * b; } public int multiply(int a, int b, int c) { return a * b * c; } }

2.2. Argumentos de diferentes tipos

De manera similar, podemos sobrecargar el método multiply () haciéndolo aceptar argumentos de diferentes tipos:

public class Multiplier { public int multiply(int a, int b) { return a * b; } public double multiply(double a, double b) { return a * b; } } 

Además, es legítimo definir la clase Multiplicador con ambos tipos de sobrecarga de métodos:

public class Multiplier { public int multiply(int a, int b) { return a * b; } public int multiply(int a, int b, int c) { return a * b * c; } public double multiply(double a, double b) { return a * b; } } 

Sin embargo, vale la pena señalar que no es posible tener dos implementaciones de métodos que solo difieran en sus tipos de retorno .

Para entender por qué, consideremos el siguiente ejemplo:

public int multiply(int a, int b) { return a * b; } public double multiply(int a, int b) { return a * b; }

En este caso, el código simplemente no se compilaría debido a la ambigüedad de la llamada al método : el compilador no sabría qué implementación de multiply () llamar.

2.3. Promoción de tipo

Una característica interesante proporcionada por la sobrecarga de métodos es la llamada promoción de tipo, también conocida como conversión primitiva de ampliación .

En términos simples, un tipo dado se promueve implícitamente a otro cuando no hay coincidencia entre los tipos de argumentos pasados ​​al método sobrecargado y una implementación de método específica.

Para comprender más claramente cómo funciona la promoción de tipos, considere las siguientes implementaciones del método multiplicar () :

public double multiply(int a, long b) { return a * b; } public int multiply(int a, int b, int c) { return a * b * c; } 

Ahora, llamar al método con dos argumentos int dará como resultado que el segundo argumento sea promovido a long , ya que en este caso no hay una implementación coincidente del método con dos argumentos int .

Veamos una prueba unitaria rápida para demostrar la promoción de tipos:

@Test public void whenCalledMultiplyAndNoMatching_thenTypePromotion() { assertThat(multiplier.multiply(10, 10)).isEqualTo(100.0); }

Por el contrario, si llamamos al método con una implementación coincidente, la promoción de tipos simplemente no se lleva a cabo:

@Test public void whenCalledMultiplyAndMatching_thenNoTypePromotion() { assertThat(multiplier.multiply(10, 10, 10)).isEqualTo(1000); }

A continuación, se muestra un resumen de las reglas de promoción de tipos que se aplican a la sobrecarga de métodos:

  • el byte se puede promover a corto, int, largo, flotante o doble
  • short se puede promover a int, long, float o double
  • char se puede promover a int, long, float o double
  • int se puede promover a largo, flotante o doble
  • largo se puede promover a flotar o duplicar
  • el flotador se puede promover al doble

2.4. Enlace estático

La capacidad de asociar una llamada de método específica al cuerpo del método se conoce como enlace.

En el caso de la sobrecarga del método, el enlace se realiza de forma estática en el momento de la compilación, por lo que se denomina enlace estático.

El compilador puede establecer efectivamente el enlace en tiempo de compilación simplemente verificando las firmas de los métodos.

3. Anulación de método

La anulación de métodos nos permite proporcionar implementaciones detalladas en subclases para métodos definidos en una clase base.

Si bien la anulación de métodos es una característica poderosa, considerando que es una consecuencia lógica del uso de la herencia, uno de los pilares más importantes de OOP, cuándo y dónde utilizarlo debe analizarse cuidadosamente, por caso de uso .

Veamos ahora cómo utilizar la sustitución de métodos creando una relación simple basada en herencia ("es-a").

Aquí está la clase base:

public class Vehicle { public String accelerate(long mph) { return "The vehicle accelerates at : " + mph + " MPH."; } public String stop() { return "The vehicle has stopped."; } public String run() { return "The vehicle is running."; } }

Y aquí hay una subclase artificial:

public class Car extends Vehicle { @Override public String accelerate(long mph) { return "The car accelerates at : " + mph + " MPH."; } }

En la jerarquía anterior, simplemente hemos anulado el método accelerate () para proporcionar una implementación más refinada para el subtipo Car.

En este caso, es claro ver que si una aplicación utiliza las instancias del vehículo de clase, entonces se puede trabajar con las instancias de coches , así , ya que ambas implementaciones de la aceleración () método tienen la misma firma y el mismo tipo de retorno.

Escribamos algunas pruebas unitarias para verificar las clases de vehículos y automóviles :

@Test public void whenCalledAccelerate_thenOneAssertion() { assertThat(vehicle.accelerate(100)) .isEqualTo("The vehicle accelerates at : 100 MPH."); } @Test public void whenCalledRun_thenOneAssertion() { assertThat(vehicle.run()) .isEqualTo("The vehicle is running."); } @Test public void whenCalledStop_thenOneAssertion() { assertThat(vehicle.stop()) .isEqualTo("The vehicle has stopped."); } @Test public void whenCalledAccelerate_thenOneAssertion() { assertThat(car.accelerate(80)) .isEqualTo("The car accelerates at : 80 MPH."); } @Test public void whenCalledRun_thenOneAssertion() { assertThat(car.run()) .isEqualTo("The vehicle is running."); } @Test public void whenCalledStop_thenOneAssertion() { assertThat(car.stop()) .isEqualTo("The vehicle has stopped."); } 

Ahora, veamos algunas pruebas unitarias que muestran cómo los métodos run () y stop () , que no se anulan, devuelven valores iguales para Car y Vehicle :

@Test public void givenVehicleCarInstances_whenCalledRun_thenEqual() { assertThat(vehicle.run()).isEqualTo(car.run()); } @Test public void givenVehicleCarInstances_whenCalledStop_thenEqual() { assertThat(vehicle.stop()).isEqualTo(car.stop()); }

En nuestro caso, tenemos acceso al código fuente para ambas clases, por lo que podemos ver claramente que llamar al método accelerate () en una instancia de vehículo base y llamar a accelerate () en una instancia de Car devolverá valores diferentes para el mismo argumento.

Por lo tanto, la siguiente prueba demuestra que se invoca el método anulado para una instancia de Car :

@Test public void whenCalledAccelerateWithSameArgument_thenNotEqual() { assertThat(vehicle.accelerate(100)) .isNotEqualTo(car.accelerate(100)); }

3.1. Tipo de sustituibilidad

A core principle in OOP is that of type substitutability, which is closely associated with the Liskov Substitution Principle (LSP).

Simply put, the LSP states that if an application works with a given base type, then it should also work with any of its subtypes. That way, type substitutability is properly preserved.

The biggest problem with method overriding is that some specific method implementations in the derived classes might not fully adhere to the LSP and therefore fail to preserve type substitutability.

Of course, it's valid to make an overridden method to accept arguments of different types and return a different type as well, but with full adherence to these rules:

  • If a method in the base class takes argument(s) of a given type, the overridden method should take the same type or a supertype (a.k.a. contravariant method arguments)
  • If a method in the base class returns void, the overridden method should return void
  • If a method in the base class returns a primitive, the overridden method should return the same primitive
  • If a method in the base class returns a certain type, the overridden method should return the same type or a subtype (a.k.a. covariant return type)
  • If a method in the base class throws an exception, the overridden method must throw the same exception or a subtype of the base class exception

3.2. Dynamic Binding

Teniendo en cuenta que la anulación del método solo se puede implementar con herencia, donde hay una jerarquía de un tipo base y subtipo (s), el compilador no puede determinar en el momento de la compilación a qué método llamar, ya que tanto la clase base como las subclases definen el mismos métodos.

Como consecuencia, el compilador debe verificar el tipo de objeto para saber qué método debe invocarse.

Como esta verificación ocurre en tiempo de ejecución, la anulación de métodos es un ejemplo típico de enlace dinámico.

4. Conclusión

En este tutorial, aprendimos cómo implementar la sobrecarga de métodos y la anulación de métodos, y exploramos algunas situaciones típicas en las que son útiles.

Como de costumbre, todos los ejemplos de código que se muestran en este artículo están disponibles en GitHub.