Guía de métodos de sobrecarga en Java

G

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 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 y private
  • Tipo de retorno: por ejemplo, void, inty String
  • Nombre / identificador de método válido
  • Parámetros (opcional)
  • Throwables (opcional): p. Ej., IllegalArgumentException y IOException
  • 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.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con tus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad