Guía para sobrecargar métodos en Java

La sobrecarga se refiere a la definición de múltiples métodos con los mismos nombres pero diferentes firmas en la misma clase. Hablaremos sobre la sintaxis y las mejores prácticas al sobrecargar.

Introducción

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 asegurarnos de que un método haga una cosa y solo una.

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 distinguirlos. O, al menos, para sugerir lo que 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 [no te repitas](/principios-de-diseno-orientado-a-objetos-en -java/) (SECO) principio.

Sin embargo, como se mostrará en este artículo, los métodos con nombres similares o iguales a veces son útiles. Pueden mejorar la intuición de las llamadas API y, con un uso inteligente y libre, incluso pueden mejorar la legibilidad del código.

¿Qué es la sobrecarga de métodos?

Sobrecargar es el acto de definir múltiples métodos con nombres idénticos en la misma clase.

Aun así, para evitar la ambigüedad, Java exige que dichos métodos tengan firmas diferentes para poder diferenciarlos.

Es importante recordar cómo declarar un método, para tener una idea precisa de cómo ocurre la sobrecarga.

Mira, Java espera que los métodos presenta hasta seis partes:

  1. Modificadores: por ejemplo, ‘público’ y ‘privado’
  2. Tipo de devolución: por ejemplo, void, int y String
  3. Nombre/identificador de método válido
  4. Parámetros (opcional)
  5. Throwables (opcional): por ejemplo, IllegalArgumentException y IOException
  6. Cuerpo del método

Por lo tanto, un método típico puede verse así:

1
2
3
4
5
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 detalles).

Dado que Java puede diferenciar las firmas de los métodos, puede permitirse la sobrecarga de métodos.

Definamos una clase con un método sobrecargado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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 cadena detalles, mientras que otros requieren una calle, ciudad, estado, código postal, etc.

Llamar al método setDetails() con un determinado 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? {#por qué es necesario sobrecargar el método}

La sobrecarga de métodos es útil en dos escenarios principales. Cuando necesitas una clase para:

  • Crear valores predeterminados
  • Capturar tipos de argumentos alternativos

Tome la clase Dirección a continuación, por ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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

Digamos que solo conoce la “calle” y la “ciudad” de una dirección, por ejemplo. Llamarías al método setDetails() con dos parámetros String:

1
2
var address = new Address();
address.setDetails("400 Croft Road", "Sacramento");

Y a pesar de recibir algunos detalles, la clase aún generará una apariencia de dirección completa. Rellenará los detalles que faltan con los valores predeterminados.

Entonces, en efecto, los métodos sobrecargados han reducido las demandas impuestas a los clientes. Los usuarios no tienen que 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 un formato legible. Esto es especialmente conveniente cuando uno llama toString() de la clase:

1
2
400 Croft Road
Sacramento, [Unknown State], [Unknown Zip]

Como muestra el resultado anterior, una llamada toString() siempre producirá un valor que es fácil de interpretar — sin valores nulos.

Tipos de argumentos alternativos

La clase Dirección 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 configurar los detalles de la Dirección llamando a:

1
address.setDetails("400 Croft Road", "Sacramento", "95800");

o:

1
address.setDetails("400 Croft Road", "Sacramento", 95800);

Sin embargo, en ambos casos, una llamada toString en la clase generará lo siguiente:

1
2
400 Croft Road
Sacramento, [Unknown State], 95800

Método de sobrecarga frente al principio DRY

Por supuesto, la sobrecarga de métodos introduce repeticiones en una clase. Y va en contra del núcleo mismo de lo que se trata el principio DRY.

La clase Dirección, por ejemplo, tiene cinco métodos que hacen algo lo mismo. Sin embargo, en una inspección más cercana, se dará cuenta de que ese puede no ser el caso. Mira, cada uno de estos métodos maneja un escenario específico.

  1. setDetails public void (Detalles de la cadena) {}
  2. public void setDetails(String street, String city) {}
  3. public void setDetails(String street, String city, int zipCode) {}
  4. public void setDetails(String street, String city, String zip) {}
  5. public void setDetails(String street, String city, String state, String zip) {}

Mientras que 1 permite que un cliente proporcione una dirección sin limitación de 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 un cliente considere conveniente.

Entonces, a expensas de DRY-ness, 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, method-references y demás, por lo que la sobrecarga de métodos era sencilla. asunto en algunos casos.

Digamos que tenemos una clase, AddressRepository, que administra una base de datos de direcciones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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, adjuntaríamos un oyente a ObservableList, aunque en este ejemplo hemos delegado esta rutina a AddressRepository.

Como resultado, hemos eliminado el acceso directo a la ObservableList modificable. Vea, tal mitigación protege la lista de direcciones de operaciones externas no autorizadas.

No obstante, necesitamos realizar un seguimiento de la adición y eliminación de direcciones. Entonces, en una clase de cliente, podríamos agregar un oyente declarando:

1
2
3
4
var repository = new AddressRepository();
repository.addListener(listener -> {
    // Listener code omitted
});

Sin embargo, si hace esto y compila, su compilador arrojará el error:

1
2
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 estos oyentes en Java 8 y posteriores es:

1
2
3
4
5
6
7
8
9
// 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 habría sido inequívoco. Al agregar un InvalidationListener, por ejemplo, habríamos usado una clase anónima.

1
2
3
4
5
6
repository.addListener(new InvalidationListener() {
    @Override
    public void invalidated(Observable observable) {
        // Listener handling code omitted
    }
});

Mejores prácticas {#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 haya tomado malas decisiones en los tipos de parámetros durante la sobrecarga. Tal enfoque expondría a los usuarios de la API a la 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 formales, [variable de aridad](https://wiki.sei.cmu.edu/confluence/display/java/DCL58- J.+Habilitar+compilar-tiempo+tipo+comprobación+de+variable+aridad+parámetro+tipos) métodos.

Recuerde, la sobrecarga generalmente reduce la cantidad de parámetros que un cliente puede proporcionar, por lo que varargs introduce una capa adicional de complejidad. Esto se debe a que se adaptan a diferentes recuentos de parámetros; más sobre eso en un segundo.

Límite de uso de varargs en métodos sobrecargados

Hay muchas decisiones de diseño que giran en torno a la mejor manera de capturar direcciones. Los diseñadores de interfaz de usuario, por ejemplo, se enfrentan al orden y número de campos para capturar esos detalles.

Los programadores también se enfrentan a un dilema: tienen que considerar el número 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:

  1. Casa
  2. Entrada
  3. Apartamento
  4. calle
  5. Ciudad
  6. Estado
  7. Cremallera
  8. País

Sin embargo, algunos diseñadores de UI insisten en que capturar estos detalles en campos separados no es lo 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, la clase Dirección en nuestro caso contiene un setter que acepta un parámetro String - detalles. Aún así, eso en sí mismo no ayuda a la claridad del código. Es por eso que sobrecargamos ese método para cubrir varios campos de dirección.

Pero recuerde, varargs también es una excelente manera de atender a los diferentes recuentos de parámetros. Por lo tanto, podríamos simplificar el código en gran medida al incluir un método setter como:

1
2
3
4
// 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:

1
2
// 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:

1
2
3
public void setDetails(String line1, String line2, String state, String zip){
    // ...
}

O se refería a:

1
2
3
public void setDetails(String... details) {
    // ...
}

En resumen, ¿cómo debe 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. Lo que sucede, en cambio, es que el diseñador de API crea ambigüedad y esto es un error que está a punto de suceder. Como esto:

1
address.setDetails();

La llamada anterior pasa una matriz de cadenas vacía (nueva cadena[]{}). Si bien no es técnicamente erróneo, no resuelve ninguna parte del problema del dominio. Por lo tanto, a través de varargs, el código ahora se ha vuelto propenso a errores.

Sin embargo, hay un truco para contrarrestar este problema. Se trata de crear un método a partir del método con el mayor número de parámetros.

En este caso, usando el método:

1
2
3
public void setDetails(String line1, String line2, String state, String zip) {
    // ...
}

Crear:

1
2
3
public void setDetails(String line1, String line2, String state, String zip, String... other) {
    // ...
}

Aún así, el enfoque anterior es poco elegante. Aunque no tiene errores, solo aumenta la verbosidad de la API.

Tenga cuidado con el encuadre automático y la ampliación

Ahora supongamos que tenemos una clase, ‘Teléfono’, además de ‘Dirección’:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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:

1
Phone.setNumber(123);

Obtendremos la salida:

1
Set number of type int

Esto se debe a que el compilador elige primero el método sobrecargado setNumber(int).

Pero, ¿y si Phone no tuviera el método setNumber(int)? ¿Y ponemos 123 de nuevo? Obtenemos la salida:

1
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 encuadre automático para la ampliación. Recuerde, Oracle define autoboxeo 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 del tipo S al tipo T permite que una expresión de tipo S sea tratada en tiempo de compilación como si tuviera el tipo T.

A continuación, eliminemos el método setNumber(long) y establezcamos 123. Salidas Teléfono:

1
Set number of type Integer

Esto se debe a que la JVM autoboxea 123 en un Integer de int.

Con la eliminación de setNumber(Integer) la clase imprime:

1
Set number of type Object

En esencia, la JVM autoboxea y luego amplía el int 123 en un eventual Objeto.

Conclusión

La sobrecarga de métodos puede mejorar la legibilidad del código cuando se 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 las funciones 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 funciones 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: la simplicidad y la intuición.

Puede encontrar el código utilizado en este artículo en GitHub.

Licensed under CC BY-NC-SA 4.0