Introducción
Contenido
Java define un método como una unidad de las tareas que puede realizar una clase. Y la práctica de programación adecuada nos anima a garantizar que un método haga una cosa y solo una cosa.
También es normal que un método llame a otro método al realizar una rutina. Aún asÃ, espera que estos métodos tengan diferentes identificadores para diferenciarlos. O, al menos, sugerir qué hacen sus componentes internos.
Por lo tanto, es interesante cuando las clases comienzan a ofrecer métodos con nombres idénticos, o más bien, cuando sobrecargan los métodos y, por lo tanto, violan los estándares de código limpio como el principio de no repetir usted mismo (DRY).
Sin embargo, como mostrará este artÃculo, los métodos con nombres similares o iguales a veces son útiles. Pueden mejorar la intuición de las llamadas a la API y, con un uso inteligente y sobrio, pueden incluso mejorar la legibilidad del código.
¿Qué es la sobrecarga de métodos?
La sobrecarga es el acto de definir varios métodos con nombres idénticos en la misma clase.
Aún asÃ, para evitar la ambigüedad, Java exige que dichos métodos tengan firmas diferentes para poder diferenciarlos.
Es importante recordarnos cómo declarar un método, para tener una idea precisa de cómo se produce la sobrecarga.
Mira, Java espera que los métodos cuentan con hasta seis partes:
- Modificadores: por ejemplo,
public
yprivate
- Tipo de retorno: por ejemplo,
void
,int
yString
- Nombre / identificador de método válido
- Parámetros (opcional)
- Throwables (opcional): p. Ej.,
IllegalArgumentException
yIOException
- Cuerpo del método
Por tanto, un método tÃpico puede verse asÃ:
public void setDetails(String details) throws IllegalArgumentException {
// Verify whether supplied details string is legal
// Throw an exception if it's not
// Otherwise, use that details string
}
El identificador y los parámetros forman la firma del método o declaración.
Por ejemplo, la firma del método del método anterior es: setDetails(String details)
.
Dado que Java puede diferenciar las firmas de métodos, puede permitirse una sobrecarga de métodos.
Definamos una clase con un método sobrecargado:
public class Address {
public void setDetails(String details) {
//...
}
public void setDetails(String street, String city) {
//...
}
public void setDetails(String street, String city, int zipCode) {
//...
}
public void setDetails(String street, String city, String zip) {
//...
}
public void setDetails(String street, String city, String state, String zip) {
//...
}
}
AquÃ, hay un método llamado setDetails()
en varias formas diferentes. Algunos requieren solo una String details
, mientras que algunos requieren un street
, city
, state
, zip
etc.
Llamando al setDetails()
método con un cierto conjunto de argumentos determinará qué método se llamará. Si ninguna firma corresponde a su conjunto de argumentos, se producirá un error del compilador.
¿Por qué necesitamos la sobrecarga de métodos?
La sobrecarga de métodos es útil en dos escenarios principales. Cuando necesitas una clase para:
- Crear valores predeterminados
- Capturar tipos de argumentos alternativos
Toma el Address
clase a continuación, por ejemplo:
public class Address {
private String details;
public Address() {
this.details = String.format(
"%s, %s n%s, %s", // Address display format
new Object[] { // Address details
"[Unknown Street]",
"[Unknown City]",
"[Unknown State]",
"[Unknown Zip]"});
}
// Getters and other setters omitted
public void setDetails(String street, String city) {
setDetails(street, city, "[Unknown Zip]");
}
public void setDetails(String street, String city, int zipCode) {
// Convert the int zipcode to a string
setDetails(street, city, Integer.toString(zipCode));
}
public void setDetails(String street, String city, String zip) {
setDetails(street, city, "[Unknown State]", zip);
}
public void setDetails(String street, String city, String state, String zip) {
setDetails(String.format(
"%s n%s, %s, %s",
new Object[]{street, city, state, zip}));
}
public void setDetails(String details) {
this.details = details;
}
@Override
public String toString() {
return details;
}
}
Valores predeterminados
Di que solo conoces una dirección street
y city
, por ejemplo. LlamarÃas al método setDetails()
con dos String
parámetros:
var address = new Address();
address.setDetails("400 Croft Road", "Sacramento");
Y a pesar de recibir algunos detalles, la clase seguirá generando una apariencia de dirección completa. Completará los detalles faltantes con valores predeterminados.
Entonces, en efecto, los métodos sobrecargados han reducido las demandas impuestas a los clientes. Los usuarios no necesitan conocer una dirección en su totalidad para usar la clase.
Los métodos también crean una forma estándar de representar los detalles de la clase en una forma legible. Esto es especialmente conveniente cuando uno llama a la clase toString()
:
400 Croft Road
Sacramento, [Unknown State], [Unknown Zip]
Como muestra el resultado anterior, un toString()
La llamada siempre producirá un valor que sea fácil de interpretar, sin valores nulos.
Tipos de argumentos alternativos
los Address
class no limita a los clientes a proporcionar el código postal en un solo tipo de datos. Además de aceptar códigos postales en String
, también maneja aquellos en int
.
Entonces, uno puede establecer Address
detalles llamando a:
address.setDetails("400 Croft Road", "Sacramento", "95800");
o:
address.setDetails("400 Croft Road", "Sacramento", 95800);
Sin embargo, en ambos casos, un toString
llamar a la clase dará como resultado lo siguiente:
400 Croft Road
Sacramento, [Unknown State], 95800
Sobrecarga de métodos frente al principio DRY
Por supuesto, la sobrecarga de métodos introduce repeticiones en una clase. Y va en contra de la esencia misma del principio DRY.
los Address
class, por ejemplo, tiene cinco métodos que hacen algo parecido. Sin embargo, en una inspección más cercana, se dará cuenta de que puede que ese no sea el caso. Vea, cada uno de estos métodos maneja un escenario especÃfico.
public void setDetails(String details) {}
public void setDetails(String street, String city) {}
public void setDetails(String street, String city, int zipCode) {}
public void setDetails(String street, String city, String zip) {}
public void setDetails(String street, String city, String state, String zip) {}
Mientras 1 permite a un cliente proporcionar una dirección sin limitación al formato, 5 es bastante estricto.
En total, los cinco métodos hacen que la API sea más amigable. Permiten a los usuarios proporcionar algunos de los detalles de una dirección. O todos. Lo que el cliente considere conveniente.
Entonces, a expensas de la sequedad, Address
resulta ser más legible que cuando tiene setters con nombres distintos.
Sobrecarga de métodos en Java 8+
Antes de Java 8, no tenÃamos lambdas, referencias de métodos y demás, por lo que la sobrecarga de métodos era un asunto sencillo en algunos casos.
Digamos que tenemos una clase AddressRepository
, que gestiona una base de datos de direcciones:
public class AddressRepository {
// We declare any empty observable list that
// will contain objects of type Address
private final ObservableList<Address> addresses
= FXCollections.observableArrayList();
// Return an unmodifiable collection of addresses
public Collection<Address> getAddresses() {
return FXCollections.unmodifiableObservableList(addresses);
}
// Delegate the addition of both list change and
// invalidation listeners to this class
public void addListener(ListChangeListener<? super Address> listener) {
addresses.addListener(listener);
}
public void addListener(InvalidationListener listener) {
addresses.addListener(listener);
}
// Listener removal, code omitted
}
Si deseamos escuchar los cambios en la lista de direcciones, adjuntaremos un oyente al ObservableList
, aunque en este ejemplo hemos delegado esta rutina a AddressRepository
.
Como resultado, hemos eliminado el acceso directo a los modificables ObservableList
. Vea, tal mitigación protege la lista de direcciones de operaciones externas no autorizadas.
No obstante, debemos realizar un seguimiento de la adición y eliminación de direcciones. Entonces, en una clase de cliente, podrÃamos agregar un oyente declarando:
var repository = new AddressRepository();
repository.addListener(listener -> {
// Listener code omitted
});
Sin embargo, si hace esto y compila, su compilador arrojará el error:
reference to addListener is ambiguous
both method addListener(ListChangeListener<? super Address>) in AddressRepository and method addListener(InvalidationListener) in AddressRepository match
Como resultado, tenemos que incluir declaraciones explÃcitas en las lambdas. Tenemos que señalar el método sobrecargado exacto al que nos referimos. Por lo tanto, la forma recomendada de agregar tales oyentes en Java 8 y más allá es:
// We remove the Address element type from the
// change object for clarity
repository.addListener((Change<?> change) -> {
// Listener code omitted
});
repository.addListener((Observable observable) -> {
// Listener code omitted
});
Por el contrario, antes de Java 8, el uso de métodos sobrecargados no habrÃa sido ambiguo. Al agregar un InvalidationListener
, por ejemplo, habrÃamos utilizado una clase anónima.
repository.addListener(new InvalidationListener() {
@Override
public void invalidated(Observable observable) {
// Listener handling code omitted
}
});
Mejores prácticas
El uso excesivo de la sobrecarga de métodos es un olor a código.
Tomemos un caso en el que un diseñador de API ha tomado malas decisiones en los tipos de parámetros durante la sobrecarga. Este enfoque expondrÃa a los usuarios de la API a confusión.
Esto, a su vez, puede hacer que su código sea susceptible a errores. Además, la práctica coloca cargas de trabajo excesivas en las JVM. Se esfuerzan por resolver los tipos exactos a los que se refieren las sobrecargas de métodos mal diseñados.
Sin embargo, uno de los usos más controvertidos de la sobrecarga de métodos es cuando presenta varargs
, o para ser formal, aridad variable métodos.
Recuerde, la sobrecarga generalmente eleva el número de parámetros que un cliente puede suministrar varargs
introducir una capa extra de complejidad. Eso se debe a que se adaptan a diferentes recuentos de parámetros, más sobre eso en un segundo.
Limitar el uso de varargs en métodos sobrecargados
Hay muchas decisiones de diseño que giran en torno a la mejor forma de capturar direcciones. Los diseñadores de UI, por ejemplo, lidian con orden y número de campos utilizar para capturar esos detalles.
Los programadores también enfrentan un enigma: tienen que considerar la cantidad de variables fijas que necesita un objeto de dirección, por ejemplo.
Una definición completa de un objeto de dirección podrÃa, por ejemplo, tener hasta ocho campos:
- Casa
- Entrada
- Departamento
- Calle
- Ciudad
- Estado
- Código Postal
- PaÃs
Sin embargo, algunos diseñadores de UI insisten en que capturar estos detalles en campos separados no es ideal. Afirman que aumenta la carga cognitiva de los usuarios. Por lo tanto, generalmente sugieren combinar todos los detalles de la dirección en una sola área de texto.
Como resultado, el Address
clase en nuestro caso contiene un setter que acepta uno String
parámetro – details
. Aún asÃ, eso por sà solo no ayuda a la claridad del código. Es por eso que sobrecargamos ese método para cubrir varios campos de direcciones.
Pero reString, varargs
también es una forma excelente de atender a diferentes recuentos de parámetros. Por lo tanto, podrÃamos simplificar el código en gran medida al incluir un método de establecimiento como:
// Sets a String[]{} of details
public void setDetails(String... details) {
// ...
}
Por lo tanto, habrÃamos permitido que el cliente de la clase hiciera algo como:
// Set the house, entrance, apartment, and street
address.setDetails("18T", "3", "4C", "North Cromwell");
Sin embargo, esto plantea un problema. ¿El código anterior llamó a este método?
public void setDetails(String line1, String line2, String state, String zip){
// ...
}
O se refirió a:
public void setDetails(String... details) {
// ...
}
En resumen, ¿cómo deberÃa tratar el código esos detalles? ¿Te gustan los campos de dirección especÃficos o los detalles generalizados?
El compilador no se quejará. No elegirá el método de aridad variable. En cambio, lo que sucede es que el diseñador de API crea ambigüedad y este es un error que está esperando suceder. Como esto:
address.setDetails();
La llamada anterior pasa una matriz String vacÃa (new String[]{}
). Si bien no es técnicamente erróneo, no resuelve ninguna parte del problema del dominio. AsÃ, a través de varargs
, el código ahora se ha vuelto propenso a errores.
Sin embargo, existe un truco para contrarrestar este problema. Implica crear un método a partir del método con el mayor número de parámetros.
En este caso, usando el método:
public void setDetails(String line1, String line2, String state, String zip) {
// ...
}
Crear:
public void setDetails(String line1, String line2, String state, String zip, String... other) {
// ...
}
Aún asÃ, el enfoque anterior no es elegante. Aunque está libre de errores, solo aumenta la verbosidad de la API.
Tenga cuidado con el autoboxing y el ensanchamiento
Ahora supongamos que tenemos una clase, Phone
, además Address
:
public class Phone {
public static void setNumber(Integer number) {
System.out.println("Set number of type Integer");
}
public static void setNumber(int number) {
System.out.println("Set number of type int");
}
public static void setNumber(long number) {
System.out.println("Set number of type long");
}
public static void setNumber(Object number) {
System.out.println("Set number of type Object");
}
}
Si llamamos al método:
Phone.setNumber(123);
Obtendremos la salida:
Set number of type int
Eso es porque el compilador elige el método sobrecargado setNumber(int)
primero.
Pero que si Phone
no tenia el metodo setNumber(int)
? Y ponemos 123
¿otra vez? Obtenemos la salida:
Set number of type long
setNumber(long)
es la segunda opción del compilador. En ausencia de un método con el primitivo int
, la JVM renuncia al autoboxing por ensanchamiento. Recuerde, Oracle define autoboxing como:
… la conversión automática que hace el compilador de Java entre los tipos primitivos y sus correspondientes clases contenedoras de objetos.
Y ensanchamiento como:
Una conversión especÃfica de tipo S
digitar T
permite una expresión de tipo S
para ser tratado en tiempo de compilación como si tuviera tipo T
en lugar.
A continuación, eliminemos el método setNumber(long)
y establecer 123
. Phone
salidas:
Set number of type Integer
Eso es porque las cajas automáticas JVM 123
en una Integer
desde int
.
Con la eliminación de setNumber(Integer)
la clase imprime:
Set number of type Object
En esencia, la JVM se autoencuadra y luego amplÃa la int
123
en un eventual Object
.
Conclusión
La sobrecarga de métodos puede mejorar la legibilidad del código cuando lo usa con cuidado. En algunos casos, incluso hace que el manejo de problemas de dominio sea intuitivo.
No obstante, la sobrecarga es una táctica difÃcil de dominar. Aunque parece algo trivial de usar, es todo lo contrario. Obliga a los programadores a considerar la jerarquÃa de los tipos de parámetros, por ejemplo: ingrese a las instalaciones de autoboxing y ampliación de Java, y la sobrecarga de métodos se convierte en un entorno complejo para trabajar.
Además, Java 8 introdujo nuevas caracterÃsticas en el lenguaje, lo que agravó las sobrecargas de métodos. El uso de interfaces funcionales en métodos sobrecargados, por ejemplo, reduce la legibilidad de una API.
Obligan a los usuarios a declarar los tipos de parámetros en un método de cliente. Por lo tanto, esto anula todo el propósito de la sobrecarga de métodos: simplicidad e intuición.
Puede encontrar el código utilizado en este artÃculo en GitHub.