miércoles, 8 de diciembre de 2021

Genericos en Java

En cualquier proyecto de software no trivial, los errores son simplemente una realidad. Una planificación, programación y pruebas cuidadosas pueden ayudar a reducir su omnipresencia, pero de alguna manera, en algún lugar, siempre encontrarán una manera de introducirse en su código. Esto se vuelve especialmente evidente a medida que se introducen nuevas funciones y su código base crece en tamaño y complejidad.

Afortunadamente, algunos errores son más fáciles de detectar que otros. Los errores en tiempo de compilación, por ejemplo, se pueden detectar desde el principio; puede utilizar los mensajes de error del compilador para averiguar cuál es el problema y solucionarlo, en ese mismo momento. Los errores en tiempo de ejecución, sin embargo, pueden ser mucho más problemáticos; no siempre aparecen de inmediato, y cuando lo hacen, puede ser en un punto del programa que está muy alejado de la causa real del problema.

Los genéricos (o Generics) agregan estabilidad a su código al hacer que más errores sean detectables en el momento de la compilación.

¿Por qué utilizar genéricos?

En pocas palabras, los genéricos permiten que los tipos (clases e interfaces) sean parámetros al definir clases, interfaces y métodos. Al igual que los parámetros formales más familiares utilizados en las declaraciones de métodos, los parámetros de tipo proporcionan una forma de reutilizar el mismo código con diferentes entradas. La diferencia es que las entradas a los parámetros formales son valores, mientras que las entradas a los parámetros de tipo son tipos.

El código que usa genéricos tiene muchas ventajas sobre el código no genérico:

Comprobaciones de tipo más estrictas en tiempo de compilación.

Un compilador de Java aplica una fuerte verificación de tipos al código genérico y emite errores si el código viola la seguridad de los tipos. Arreglar errores en tiempo de compilación es más fácil que arreglar errores en tiempo de ejecución, que pueden ser difíciles de encontrar.

Eliminación de conversión de tipo (Cast).

El siguiente fragmento de código sin genéricos requiere conversión:

Cuando se reescribe para usar genéricos, el código no requiere conversión:

Permitir a los programadores implementar algoritmos genéricos.

Mediante el uso de genéricos, los programadores pueden implementar algoritmos genéricos que funcionan en colecciones de diferentes tipos, se pueden personalizar y son seguros para los tipos y más fáciles de leer.

Tipos genéricos

Un tipo genérico es una clase o interfaz genérica que se parametriza sobre tipos. La siguiente clase Box se modificará para demostrar el concepto.

Box, una clase simple

Comience examinando la clase Box no genérica que opera sobre objetos de cualquier tipo. Solo necesita proporcionar dos métodos: set, que agrega un objeto al cuadro, y get, que lo recupera:


Dado que sus métodos aceptan o devuelven un objeto, puede pasar lo que quiera, siempre que no sea uno de los tipos primitivos. No hay forma de verificar, en tiempo de compilación, cómo se usa la clase. Una parte del código puede colocar un número entero en el cuadro y esperar obtener números enteros, mientras que otra parte del código puede pasar por error una cadena, lo que da como resultado un error de tiempo de ejecución.

Una versión Generic de la clase Box

Una clase genérica se define con el siguiente formato:

class name<T1, T2, ..., Tn> { /* ... */ }

La sección de parámetro de tipo, delimitada por corchetes angulares (<>), sigue al nombre de la clase. Especifica los parámetros de tipo (también llamados variables de tipo) T1, T2, ... y Tn.

Para actualizar la clase Box para usar genéricos, cree una declaración de tipo genérico cambiando el código "Box de clase pública" a "Box de clase pública <T>". Esto introduce la variable de tipo, T, que se puede usar en cualquier lugar dentro de la clase.

Con este cambio, la clase Box se convierte en:


Como puede ver, todas las apariciones de Object se reemplazan por T. Una variable de tipo puede ser cualquier tipo no primitivo que especifique: cualquier tipo de clase, cualquier tipo de interfaz, cualquier tipo de arreglo o incluso otro tipo de variable.

Esta misma técnica se puede aplicar para crear interfaces genéricas.

Convenciones de nomenclatura de parámetros de tipo

Por convención, los nombres de los parámetros de tipo son letras mayúsculas simples. Esto contrasta fuertemente con las convenciones de nomenclatura de variables que ya conoce, y con una buena razón: sin esta convención, sería difícil distinguir entre una variable de tipo y un nombre de clase o interfaz ordinario.

Los nombres de parámetros de tipo más utilizados son:
  • E - Elemento (utilizado ampliamente por Java Collections Framework)
  • K - Clave
  • N - Número
  • T - Tipo
  • V - Valor
  • S, U, V, etc. - 2. °, 3. °, 4. ° tipos
Verá estos nombres utilizados en la API de Java SE y en el resto de esta lección.

Invocación y creación de instancias de un tipo genérico

Para hacer referencia a la clase Box genérica desde su código, debe realizar una invocación de tipo genérico, que reemplaza T con algún valor concreto, como Integer:

Box <Integer> integerBox;
Puede pensar que una invocación de tipo genérico es similar a una invocación de método ordinario, pero en lugar de pasar un argumento a un método, está pasando un argumento de tipo (Integer en este caso) a la clase Box en sí.

Terminología de parámetro de tipo y argumento de tipo: muchos desarrolladores usan los términos "parámetro de tipo" y "argumento de tipo" indistintamente, pero estos términos no son lo mismo. Al codificar, se proporcionan argumentos de tipo para crear un tipo parametrizado. Por lo tanto, la T en Foo <T> es un parámetro de tipo y la Cadena en Foo <String> f es un argumento de tipo. Esta lección observa esta definición al usar estos términos.
Como cualquier otra declaración de variable, este código en realidad no crea un nuevo objeto Box. Simplemente declara que integerBox contendrá una referencia a una "Box de Integer", que es como se lee Box <Integer>.

Una invocación de un tipo genérico se conoce generalmente como tipo parametrizado.

Para crear una instancia de esta clase, use la nueva palabra clave, como de costumbre, pero coloque <Integer> entre el nombre de la clase y el paréntesis:

Box <Integer> integerBox = new Box <Integer> ();

El diamante

En Java SE 7 y versiones posteriores, puede reemplazar los argumentos de tipo necesarios para invocar el constructor de una clase genérica con un conjunto vacío de argumentos de tipo (<>) siempre que el compilador pueda determinar, o inferir, los argumentos de tipo del contexto . Este par de paréntesis angulares, <>, se llama informalmente diamante. Por ejemplo, puede crear una instancia de Box <Integer> con la siguiente declaración:

Box <Integer> integerBox = new Box <> ();
Para obtener más información sobre la notación de diamantes y la inferencia de tipos, consulte Inferencia de tipos.

Parámetros de tipo múltiple

Como se mencionó anteriormente, una clase genérica puede tener múltiples parámetros de tipo. Por ejemplo, la clase genérica OrderedPair, que implementa la interfaz de par genérica:


Las siguientes declaraciones crean dos instancias de la clase OrderedPair:

Pair <String, Integer> p1 = new OrderedPair <String, Integer> ("Par", 8);
Pair <String, String> p2 = new OrderedPair <String, String> ("hola", "mundo");
El código, new OrderedPair <String, Integer>, instancia K como String y V como Integer. Por lo tanto, los tipos de parámetros del constructor de OrderedPair son String e Integer, respectivamente. Debido al autoboxing, es válido pasar un String y un int a la clase.

Como se menciona en The Diamond, debido a que un compilador de Java puede inferir los tipos K y V de la declaración OrderedPair <String, Integer>, estas declaraciones se pueden abreviar usando la notación de diamante:

OrderedPair <String, Integer> p1 = new OrderedPair <> ("Par", 8);
OrderedPair <String, String> p2 = new OrderedPair <> ("hola", "mundo");
Para crear una interfaz genérica, siga las mismas convenciones que para crear una clase genérica.

Tipos parametrizados
También puede sustituir un parámetro de tipo (es decir, K o V) con un tipo parametrizado (es decir, List <String>). Por ejemplo, usando el ejemplo OrderedPair <K, V>:

OrderedPair <String, Box <Integer>> p = new OrderedPair <> ("primes", new Box <Integer> (...));

No hay comentarios:

Publicar un comentario