Cuando programamos en Java, es común encontrarnos con símbolos como los diamantes < > que encierran nombres de clases, por ejemplo List<Integer> o List<String>. Estos símbolos indican el uso de genéricos, una característica fundamental para parametrizar clases y métodos con tipos de datos variables.
Los genéricos funcionan de manera similar a los parámetros en funciones: permiten que una clase o método se adapte a diferentes tipos sin perder la seguridad en tiempo de compilación. Esto significa que, a diferencia de trabajar con Object y hacer casteos manuales, los genéricos nos ayudan a evitar errores de tipo y a que el compilador pueda verificar que estamos usando los tipos correctamente.
Si exploramos las clases estándar de Java, como las del paquete java.util, veremos que muchas están declaradas con genéricos. Por ejemplo, Map<K,V> o NavigableSet<E> usan letras mayúsculas para representar tipos genéricos. Estas letras son convenciones: T suele representar un tipo genérico cualquiera, E un elemento, K una clave, V un valor, y así sucesivamente. Aunque es posible usar nombres más largos, lo habitual es mantener estas letras para facilitar la comprensión.
Tomemos la clase Optional<T> como ejemplo. Aquí, T es un parámetro genérico que representa un tipo desconocido en el momento de definir la clase. Cuando creamos una instancia de Optional, debemos especificar qué tipo será T. Por ejemplo, si hacemos Optional<Boolean>, entonces el método get() devolverá un Boolean. Si usamos Optional<String>, get() devolverá un String. Esto es posible porque el compilador sustituye todas las apariciones de T por el tipo que hayamos indicado al instanciar.
De forma similar, la interfaz List<E> usa E para representar el tipo de los elementos que contiene la lista. Si declaramos una lista como List<Integer>, entonces el método get() devolverá un Integer y el método add() aceptará un Integer. Si cambiamos el tipo genérico a Random, entonces get() devolverá un Random y add() aceptará un Random. Esto nos permite escribir código flexible y seguro, sin necesidad de hacer casteos explícitos.
Es importante no dejar sin especificar el tipo genérico cuando usamos estas clases. Si escribimos simplemente List sin indicar el tipo, Java asumirá que es una lista de Object, lo que puede provocar que tengamos que hacer casteos y perder la semántica del tipo, además de que los IDEs nos mostrarán advertencias.
Para usar los genéricos correctamente, debemos especificar el tipo en la declaración de variables o en los parámetros de funciones. Por ejemplo, si tenemos una función que suma elementos de una lista, debemos declarar el parámetro como List<Integer> para que el compilador sepa qué tipo de elementos estamos manejando y podamos operar con ellos sin problemas.
Además, Java cuenta con un sistema de inferencia de tipos que nos permite simplificar la sintaxis al instanciar objetos genéricos. Por ejemplo, si declaramos una variable como List<String> lista, podemos instanciarla con new ArrayList<>() usando el diamante vacío. Esto indica al compilador que debe inferir el tipo genérico a partir de la declaración de la variable, evitando repetir el tipo y haciendo el código más limpio.
List<String> lista = new ArrayList<>();
En resumen, los genéricos en Java son una herramienta poderosa para crear clases y métodos que funcionan con diferentes tipos de datos de forma segura y clara. Nos permiten escribir código más robusto, evitando errores de tipo y la necesidad de casteos innecesarios. Entender cómo funcionan y cómo usarlos correctamente es clave para aprovechar al máximo el lenguaje y sus bibliotecas estándar.