Guía para comprender los genéricos en Java

En esta guía tutorial, profundizaremos en la comprensión de los genéricos de Java: qué son, cómo usarlos y cuándo elegir qué enfoque, con ejemplos.

Introducción

Java es un lenguaje de programación tipo seguro. La seguridad de tipos garantiza una capa de validez y robustez en un lenguaje de programación. Es una parte clave de la seguridad de Java garantizar que las operaciones realizadas en un objeto solo se realicen si el tipo del objeto lo admite.

La seguridad de tipos reduce drásticamente la cantidad de errores de programación que pueden ocurrir durante el tiempo de ejecución, lo que implica todo tipo de errores relacionados con las discrepancias de tipos. En cambio, estos tipos de errores se detectan durante el tiempo de compilación, lo que es mucho mejor que detectar errores durante el tiempo de ejecución, lo que permite a los desarrolladores tener menos viajes inesperados y no planificados al buen depurador antiguo.

La seguridad de tipo también se denomina indistintamente tipado fuerte.

Java Generics es una solución diseñada para reforzar la seguridad de tipos para la que se diseñó Java. Los genéricos permiten parametrizar tipos en métodos y clases e introducen una nueva capa de abstracción para parámetros formales. Esto se explicará en detalle más adelante.

Hay muchas ventajas de usar genéricos en Java. La implementación de genéricos en su código puede mejorar en gran medida su calidad general al evitar errores de tiempo de ejecución sin precedentes que involucran tipos de datos y encasillamiento.

Esta guía demostrará la declaración, implementación, casos de uso y beneficios de los genéricos en Java.

¿Por qué usar genéricos? {#por qué usar genéricos}

Para proporcionar contexto sobre cómo los genéricos refuerzan la tipificación fuerte y evitan errores de tiempo de ejecución relacionados con el encasillamiento, echemos un vistazo a un fragmento de código.

Digamos que desea almacenar un montón de variables String en una lista. Codificar esto sin usar genéricos se vería así:

1
2
List stringList = new ArrayList();
stringList.add("Apple");

Este código no activará ningún error en tiempo de compilación, pero la mayoría de los IDE le advertirán que la Lista que ha inicializado es de un tipo sin procesar y debe parametrizarse con un genérico.

Los IDE le advierten de los problemas que pueden ocurrir si no parametriza una lista con un tipo. Uno es poder agregar elementos de cualquier tipo de datos a la lista. Las listas, por defecto, aceptarán cualquier tipo de Objeto, que incluye cada uno de sus subtipos:

1
2
3
List stringList = new ArrayList();
stringList.add("Apple");
stringList.add(1);

Agregar dos o más tipos diferentes dentro de la misma colección viola las reglas de seguridad de tipos. Este código se compilará con éxito, pero esto definitivamente causará una multitud de problemas.

Por ejemplo, ¿qué sucede si tratamos de recorrer la lista? Usemos un bucle for mejorado:

1
2
3
for (String string : stringList) {
    System.out.println(string);
}

Seremos recibidos con un:

1
2
Main.java:9: error: incompatible types: Object cannot be converted to String
        for (String string : stringList) {

De hecho, esto no se debe a que juntamos un String y un Integer. Si cambiamos el ejemplo y agregamos dos Strings:

1
2
3
4
5
6
7
List stringList = new ArrayList();
stringList.add("Apple");
stringList.add("Orange");
        
for (String string : stringList) {
    System.out.println(string);
}

Todavía seríamos recibidos con:

1
2
Main.java:9: error: incompatible types: Object cannot be converted to String
        for (String string : stringList) {

Esto se debe a que sin ninguna parametrización, la ‘Lista’ solo trata con ‘Objetos’. Puedes técnicamente eludir esto usando un ‘Objeto’ en el bucle for mejorado:

1
2
3
4
5
6
7
List stringList = new ArrayList();
stringList.add("Apple");
stringList.add(1);
        
for (Object object : stringList) {
    System.out.println(object);
}

Que imprimiría:

1
2
Apple
1

Sin embargo, esto va en contra de la intuición y no es una solución real. Esto es solo evitar el problema de diseño subyacente de una manera insostenible.

Otro problema es la necesidad de encasillar cada vez que accede y asigna elementos dentro de una lista sin genéricos. Para asignar nuevas variables de referencia a los elementos de la lista, debemos encasillarlos, ya que el método get() devuelve Objects:

1
2
String str = (String) stringList.get(0);
Integer num = (Integer) stringList.get(1);

En este caso, ¿cómo podrá determinar el tipo de cada elemento durante el tiempo de ejecución, para saber a qué tipo convertirlo? No hay muchas opciones y las que tienes a tu disposición complican las cosas de manera desproporcionada, como usar bloques try/catch para probar y convertir elementos en algunos tipos predefinidos.

Además, si no puede convertir el elemento de la lista durante la asignación, se mostrará un error como este:

1
Type mismatch: cannot convert from Object to Integer

En OOP, la conversión explícita debe evitarse tanto como sea posible porque no es una solución confiable para problemas relacionados con OOP.

Por último, debido a que la clase List es un subtipo de Collection, debería tener acceso a iteradores utilizando el objeto Iterator, el método iterator() y bucles for-each. Si una colección se declara sin genéricos, definitivamente no podrá usar ninguno de estos iteradores de manera razonable.

Esta es la razón por la que se crearon los genéricos de Java y por qué son una parte integral del ecosistema de Java. Echemos un vistazo a cómo declarar clases genéricas y reescribamos este ejemplo para utilizar genéricos y evitar los problemas que acabamos de ver.

Clases y objetos genéricos

Declaremos una clase con un tipo genérico. Para especificar un tipo de parámetro en una clase o un objeto, usamos los símbolos de corchetes angulares <> junto a su nombre y le asignamos un tipo dentro de los corchetes. La sintaxis para declarar una clase genérica se ve así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Thing<T> { 
    private T val;
    
    public Thing(T val) { this.val = val;}
    public T getVal() { return this.val; }
  
    public <T> void printVal(T val) {
      System.out.println("Generic Type" + val.getClass().getName());
    }
}

Nota: A los tipos genéricos NO se les pueden asignar tipos de datos primitivos como int, char, long, double o float. Si desea asignar estos tipos de datos, utilice sus clases contenedoras en su lugar.

La letra T dentro de los corchetes angulares se denomina parámetro de tipo. Por convención, los parámetros de tipo son de una sola letra (A-Z) y mayúsculas. Algunos otros nombres de parámetros de tipo comunes utilizados son K (Clave), V (Valor), E (Elemento) y N (Número).

Aunque, en teoría, puede asignar cualquier nombre de variable a un parámetro de tipo que siga las convenciones de variables de Java, hay una buena razón para seguir la convención típica de parámetros de tipo para diferenciar una variable normal de un parámetro de tipo.

El val es de un tipo genérico. Puede ser una ‘Cadena’, un ‘Entero’ u otro objeto. Dada la clase genérica ‘Cosa’ declarada anteriormente, instanciamos la clase como algunos objetos diferentes, de diferentes tipos:

1
2
3
4
5
6
7
8
public void callThing() {
    // Three implementations of the generic class Thing with 3 different data types
    Thing<Integer> thing1 = new Thing<>(1); 
    Thing<String> thing2 = new Thing<>("String thing"); 
    Thing<Double> thing3 = new Thing<>(3.5);
  
    System.out.println(thing1.getVal() + " " + thing2.getVal() + " " + thing3.getVal());
}

Observe cómo no estamos especificando el tipo de parámetro antes de que llame el constructor. Java infiere el tipo del objeto durante la inicialización, por lo que no necesitará volver a escribirlo durante la inicialización. En este caso, el tipo ya se deduce de la declaración de la variable. Este comportamiento se denomina inferencia de tipos. Si heredamos esta clase, en una clase como SubThing, tampoco necesitaríamos establecer explícitamente el tipo al instanciarlo como Thing, ya que inferiría el tipo de su clase principal.

Puedes especificarlo en ambos lugares, pero es simplemente redundante:

1
2
3
Thing<Integer> thing1 = new Thing<Integer>(1); 
Thing<String> thing2 = new Thing<String>("String thing"); 
Thing<Double> thing3 = new Thing<Double>(3.5);

Si ejecutamos el código, dará como resultado:

1
1 String thing 3.5

El uso de genéricos permite la abstracción segura de tipos sin tener que usar el encasillamiento, que es mucho más riesgoso a largo plazo.

De manera similar, el constructor List acepta un tipo genérico:

1
2
3
public interface List<E> extends Collection<E> {
// ...
}

En nuestros ejemplos anteriores, no hemos especificado un tipo, lo que da como resultado que la ‘Lista’ sea una ‘Lista’ de ‘Objetos’. Ahora, reescribamos el ejemplo anterior:

1
2
3
4
5
6
7
List<String> stringList = new ArrayList<>();
stringList.add("Apple");
stringList.add("Orange");
        
for (String string : stringList) {
    System.out.println(string);
}

Esto resulta en:

1
2
Apple
Orange

¡Funciona de maravilla! Nuevamente, no necesitamos especificar el tipo en la llamada ArrayList(), ya que infiere el tipo de la definición List<String>. El único caso en el que tendrá que especificar el tipo después de la llamada al constructor es si está aprovechando la función inferencia de tipo de variable local de Java 10+:

1
2
3
var stringList = new ArrayList<String>();
stringList.add("Apple");
stringList.add("Orange");

Esta vez, dado que estamos usando la palabra clave var, que no es de tipo seguro en sí misma, la llamada ArrayList<>() no puede inferir el tipo, y simplemente se establecerá por defecto a un tipo Objeto si no lo especificamos nosotros mismos.

Métodos genéricos

Java admite declaraciones de métodos con parámetros genéricos y tipos de devolución. Los métodos genéricos se declaran exactamente como los métodos normales, pero tienen la notación de corchetes angulares antes del tipo de retorno.

Declaremos un método genérico simple que acepte 3 parámetros, los agregue en una lista y lo devuelva:

1
2
3
4
5
public static <E> List<E> zipTogether(E element1, E element2, E element3) {
    List<E> list = new ArrayList<>();
    list.addAll(Arrays.asList(element1, element2, element3));
    return list;
}

Ahora, podemos ejecutar esto como:

1
System.out.println(zipTogether(1, 2, 3));

Lo que resulta en:

1
[1, 2, 3]

Pero también, podemos añadir otros tipos:

1
System.out.println(zipTogether("Zeus", "Athens", "Hades"));

Lo que resulta en:

1
[Zeus, Athens, Hades]

También se admiten varios tipos de parámetros para objetos y métodos. Si un método usa más de un parámetro de tipo, puede proporcionar una lista de todos ellos dentro del operador de diamante y separar cada parámetro con comas:

1
2
3
4
// Methods with void return types are also compatible with generic methods
public static <T, K, V> void printValues(T val1, K val2, V val3) {
    System.out.println(val1 + " " + val2 + " " + val3);
}

Aquí, puede ser creativo con lo que pasa. Siguiendo las convenciones, pasaremos un tipo, clave y valor:

1
printValues(new Thing("Employee"), 125, "David");

Lo que resulta en:

1
Thing{val=Employee} 125 David

Sin embargo, tenga en cuenta que los parámetros de tipo genérico, que se pueden inferir, no necesitan declararse en la declaración genérica antes del tipo de retorno. Para demostrarlo, vamos a crear otro método que acepte 2 variables: un ‘Mapa’ genérico y una ‘Lista’ que puede contener exclusivamente valores de ‘Cadena’:

1
2
3
public <K, V> void sampleMethod(Map<K, V> map, List<String> lst) {
    // ...
}

Aquí, los tipos genéricos K y V se asignan a Map<K, V> ya que son tipos inferidos. Por otro lado, dado que List<String> solo puede aceptar cadenas, no hay necesidad de agregar el tipo genérico a la lista <K, V>.

Ahora hemos cubierto clases genéricas, objetos y métodos con uno o más parámetros de tipo. ¿Qué pasa si queremos limitar el grado de abstracción que tiene un parámetro de tipo? Esta limitación se puede implementar mediante el enlace de parámetros.

Parámetros de tipo acotado

Enlace de parámetros permite que el parámetro de tipo se limite a un objeto y sus subclases. Esto le permite aplicar ciertas clases y sus subtipos, sin dejar de tener la flexibilidad y la abstracción de usar parámetros de tipo genérico.

Para especificar que un parámetro de tipo está acotado, simplemente usamos la palabra clave extends en el parámetro de tipo - <N número de extensiones>. Esto asegura que el parámetro de tipo N que proporcionamos a una clase o método sea de tipo Número.

Declaremos una clase, llamada InvoiceDetail, que acepta un parámetro de tipo, y asegurémonos de que ese parámetro de tipo sea del tipo Number. De esta forma, los tipos genéricos que podemos usar al instanciar la clase se limitan a números y decimales de punto flotante, ya que Number es la superclase de todas las clases que involucran números enteros, incluidas las clases contenedoras y los tipos de datos primitivos:

1
2
3
4
5
6
7
class InvoiceDetail<N extends Number> {
    private String invoiceName;
    private N amount;
    private N discount;
  
    // Getters, setters, constructors...
}

Aquí, ’extiende’ puede significar dos cosas: ’extiende’, en el caso de las clases, e ‘implementa’ en el caso de las interfaces. Dado que Number es una clase abstracta, se usa en el contexto de extender esa clase.

Al extender el parámetro de tipo N como una subclase Número, la creación de instancias de cantidad y descuento ahora se limitan a Número y sus subtipos. Intentar configurarlos en cualquier otro tipo desencadenará un error de tiempo de compilación.

Intentemos asignar erróneamente valores String, en lugar de un tipo Number:

1
InvoiceDetail<String> invoice = new InvoiceDetail<>("Invoice Name", "50.99", ".10");

Dado que String no es un subtipo de Number, el compilador detecta eso y genera un error:

1
Bound mismatch: The type String is not a valid substitute for the bounded parameter <N extends Number> of the type InvoiceDetail<N>

Este es un gran ejemplo de cómo el uso de genéricos refuerza la seguridad de tipos.

Además, un solo parámetro de tipo puede extender múltiples clases e interfaces usando el operador & para las clases extendidas subsecuentemente:

1
2
3
public class SampleClass<E extends T1 & T2 & T3> {
    // ...
}

También vale la pena señalar que otro gran uso de parámetros de tipo acotado es en las declaraciones de métodos. Por ejemplo, si desea imponer que los tipos pasados ​​a un método se ajusten a algunas interfaces, puede asegurarse de que los parámetros de tipo amplíen una determinada interfaz.

Un ejemplo clásico de esto es hacer cumplir que dos tipos son ‘Comparables’, si los está comparando en un método como:

1
2
3
public static <T extends Comparable<T>> int compare(T t1, T t2) {
    return t1.compareTo(t2);
}

Aquí, usando genéricos, hacemos cumplir que t1 y t2 son ambos Comparables, y que pueden compararse genuinamente con el método compareTo(). Sabiendo que los Strings son comparables y anulan el método compareTo(), podemos usarlos cómodamente aquí:

1
System.out.println(compare("John", "Doe"));

El código da como resultado:

1
6

Sin embargo, si intentamos usar un tipo no Comparable, como Thing, que no implementa la interfaz Comparable:

1
System.out.println(compare(new Thing<String>("John"), new Thing<String>("Doe")));

Aparte de que el IDE marque esta línea como errónea, si intentamos ejecutar este código, dará como resultado:

1
2
3
4
5
6
java: method compare in class Main cannot be applied to given types;
  required: T,T
  found:    Thing<java.lang.String>,Thing<java.lang.String>
  reason: inference variable T has incompatible bounds
    lower bounds: java.lang.Comparable<T>
    lower bounds: Thing<java.lang.String>

En este caso, dado que ‘Comparable’ es una interfaz, la palabra clave ’extiende’ en realidad impone que la interfaz sea implementada por ‘T’, no extendida.

Comodines en genéricos

Los comodines se utilizan para simbolizar cualquier tipo de clase y se indican con ?. En general, querrá usar comodines cuando tenga posibles incompatibilidades entre diferentes instancias de un tipo genérico. Hay tres tipos de comodines: límite superior, límite inferior e ilimitado.

La elección del enfoque que usará suele estar determinada por el principio EN FUERA. El principio IN-OUT define In-variables y Out-variables, que, en términos más simples, representan si una variable se usa para proporcionar datos o para servir en su salida.

Por ejemplo, un método sendEmail(String cuerpo, String destinatario) tiene un cuerpo In-variable y un destinatario Out-variable. La variable body proporciona datos sobre el cuerpo del correo electrónico que desea enviar, mientras que la variable recipient proporciona la dirección de correo electrónico a la que desea enviarlo.

También hay variables mixtas, que se utilizan para proporcionar datos y luego hacer referencia al resultado en sí, en cuyo caso, querrá evitar el uso de comodines.

En términos generales, querrá definir In-variables con comodines de límite superior, usando la palabra clave extends y Out-variables con comodines de límite inferior, usando la palabra clave super.

Para In-variables a las que se puede acceder a través del método de un objeto, debe preferir comodines ilimitados.

Comodines con límite superior

Los comodines límite superior se utilizan para proporcionar un tipo genérico que limita una variable a una clase o una interfaz y todos sus subtipos. El nombre, superior-limitado se refiere al hecho de que vinculó la variable a un tipo superior - y todos sus subtipos.

En cierto sentido, las variables con límite superior son más relajadas que las variables con límite inferior, ya que permiten más tipos. Se declaran utilizando el operador comodín ? seguido de la palabra clave extends y la clase o interfaz de supertipo (el límite superior de su tipo):

1
<? extends SomeObject>

Aquí, ’extiende’, de nuevo, significa ’extiende’ clases e ‘implementa’ interfaces.

Para recapitular, los comodines con límite superior se usan normalmente para objetos que proporcionan entrada para ser consumidas en variables.

Nota: ¿Hay una clara diferencia entre Class<Generic> y Class<? extiende Genérico>. El primero permite sólo utilizar el tipo Genérico. En este último, todos los subtipos de Genérico también son válidos.

Hagamos un tipo superior (Empleado) y su subclase (Desarrollador):

1
2
3
4
5
public abstract class Employee {
    private int id;
    private String name;
    // Constructor, getters, setters
}

Y:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Developer extends Employee {
    private List<String> skillStack;

    // Constructor, getters and setters

    @Override
    public String toString() {
        return "Developer {" +
                "\nskillStack=" + skillStack +
                "\nname=" + super.getName() +
                "\nid=" + super.getId() +
                "\n}";
    }
}

Ahora, hagamos un método printInfo() simple, que acepte una lista con límite superior de objetos Empleado:

1
2
3
4
5
public static void printInfo(List<? extends Employee> employeeList) {
    for (Employee e : employeeList) {
        System.out.println(e.toString());
    }
}

La ‘Lista’ de empleados que proporcionamos tiene un límite superior a ‘Empleado’, lo que significa que podemos arrojar cualquier instancia de ‘Empleado’, así como sus subclases, como ‘Desarrollador’:

1
2
3
4
5
6
List<Developer> devList = new ArrayList<>();

devList.add(new Developer(15, "David", new ArrayList<String>(List.of("Java", "Spring"))));
devList.add(new Developer(25, "Rayven", new ArrayList<String>(List.of("Java", "Spring"))));

printInfo(devList);

Esto resulta en:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Developer{
skillStack=[Java, Spring]
name=David
id=15
}
Developer{
skillStack=[Java, Spring]
name=Rayven
id=25
}

Comodines de límite inferior

Los comodines de límite inferior son lo opuesto a los de límite superior. Esto permite restringir un tipo genérico a una clase o interfaz y todos sus supertipos. Aquí, la clase o interfaz es el límite inferior:

Subtypes and Supertypes

La declaración de comodines de límite inferior sigue el mismo patrón que los comodines de límite superior: un comodín (?) seguido de super y el supertipo:

1
<? super SomeObject>

Basado en el principio IN-OUT, los comodines de límite inferior se utilizan para los objetos que están involucrados en la salida de datos. Estos objetos se denominan variables out.

Revisemos la funcionalidad de correo electrónico de antes y hagamos una jerarquía de clases:

1
2
3
4
public class Email {
    private String email;
    // Constructor, getters, setters, toString()
}

Ahora, hagamos una subclase para Email:

1
2
3
public class ValidEmail extends Email {
    // Constructor, getters, setters
}

También querremos tener alguna clase de utilidad, como MailSender para "enviar" correos electrónicos y notificarnos los resultados:

1
2
3
4
5
public class MailSender {
    public String sendMail(String body, Object recipient) {
        return "Email sent to: " + recipient.toString();
    }
}

Finalmente, escribamos un método que acepte una lista de cuerpo y destinatarios y les envíe el cuerpo, notificándonos el resultado:

1
2
3
4
5
6
7
8
9
public static String sendMail(String body, List<? super ValidEmail> recipients) {
    MailSender mailSender = new MailSender();
    StringBuilder sb = new StringBuilder();
    for (Object o : recipients) {
        String result = mailSender.sendMail(body, o);
        sb.append(result+"\n");
    }
    return sb.toString();
}

Aquí, hemos utilizado un tipo genérico de límite inferior de ValidEmail, que extiende Email. Por lo tanto, somos libres de crear instancias de Correo electrónico y colocarlas en este método:

1
2
3
4
5
6
List<Email> recipients = new ArrayList<>(List.of(
        new Email("[correo electrónico protegido]"), 
        new Email("[correo electrónico protegido]")));
        
String result = sendMail("Hello World!", recipients);
System.out.println(result);

Esto resulta en:

1
2
Email sent to: Email{email='[correo electrónico protegido]'}
Email sent to: Email{email='[correo electrónico protegido]'}

Comodines ilimitados

Los comodines ilimitados son comodines sin ningún tipo de vinculación. En pocas palabras, son comodines que amplían cada clase individual a partir de la clase base “Objeto”.

Los comodines ilimitados se utilizan cuando la clase Objeto es a la que se accede o se manipula, o si el método en el que se utiliza no accede ni se manipula mediante un parámetro de tipo. De lo contrario, el uso de comodines ilimitados comprometerá la seguridad de tipos del método.

Para declarar un comodín ilimitado, simplemente use el operador de signo de interrogación encapsulado entre corchetes angulares <?>.

Por ejemplo, podemos tener una Lista de cualquier elemento:

1
2
3
4
5
public void print(List<?> elements) {
    for(Object element : elements) {
        System.out.println(element);
    }
}

System.out.println() acepta cualquier objeto, así que estamos listos para ir aquí. Si el método fuera copiar una lista existente en una lista nueva, entonces los comodines con límite superior son más favorables.

¿Diferencia entre comodines delimitados y parámetros de tipo delimitados?

Es posible que haya notado que las secciones para comodines acotados y parámetros de tipo acotado están separadas pero más o menos tienen la misma definición, y en el nivel superficial parecen intercambiables:

1
2
<E extends Number>
<? extends Number>

Entonces, ¿cuál es la diferencia entre estos dos enfoques? Hay varias diferencias, de hecho:

  • Los parámetros de tipo acotado aceptan múltiples extensiones usando la palabra clave &, mientras que los comodines acotados solo aceptan un solo tipo para extender.
  • Los parámetros de tipo acotado solo se limitan a los límites superiores. Esto significa que no puede usar la palabra clave super en parámetros de tipo acotado.
  • Los comodines limitados solo se pueden usar durante la creación de instancias. No se pueden usar para declaraciones (por ejemplo, declaraciones de clases y llamadas de constructores). Algunos ejemplos de uso no válido de comodines son:
    • class Example<? extends Object> {...}
    • GenericObj<?> = new GenericObj<?>()
    • GenericObj<? extends Object> = new GenericObj<? extends Object>()
  • Los comodines limitados no deben usarse como tipos de devolución. Esto no activará ningún error o excepción, pero obliga a un manejo y encasillamiento innecesarios, lo que va completamente en contra de la seguridad de tipo que logran los genéricos.
  • El operador ? no se puede usar como un parámetro real y solo se puede usar como un parámetro genérico. Por ejemplo:
    • public <?> void printDisplay(? var) {} will fail during compilation, while
    • public <E> void printDisplay(E var) compiles and runs successfully.

Beneficios del uso de genéricos

A lo largo de la guía, hemos cubierto el principal beneficio de los genéricos: proporcionar una capa adicional de seguridad tipográfica para su programa. Aparte de eso, los genéricos ofrecen muchos otros beneficios sobre el código que no los usa.

  1. Los errores de tiempo de ejecución relacionados con tipos y conversión se detectan durante el tiempo de compilación. La razón por la que se debe evitar la conversión de tipos es que el compilador no reconoce las excepciones de conversión durante el tiempo de compilación. Cuando se usa correctamente, los genéricos evitan por completo el uso de encasillamiento y, posteriormente, evita todas las excepciones de tiempo de ejecución que podría desencadenar.
  2. Las clases y los métodos son más reutilizables. Con los genéricos, las clases y los métodos pueden ser reutilizados por diferentes tipos sin tener que anular los métodos o crear una clase separada.

Conclusión

La aplicación de genéricos a su código mejorará significativamente la reutilización del código, la legibilidad y, lo que es más importante, la seguridad de los tipos. En esta guía, hemos analizado qué son los genéricos, cómo puede aplicarlos, las diferencias entre los enfoques y cuándo elegir cuál. r cuál.

Licensed under CC BY-NC-SA 4.0