Red de conocimiento informático - Conocimiento informático - Encontré un problema con los genéricos mientras aprendía Java. Espero obtener una buena respuesta. ¡Muchas gracias!

Encontré un problema con los genéricos mientras aprendía Java. Espero obtener una buena respuesta. ¡Muchas gracias!

Los tipos genéricos o genéricos son extensiones del sistema de tipos del lenguaje Java que admiten la creación de clases que se pueden parametrizar por tipo. Los parámetros de tipo pueden considerarse como marcadores de posición para el tipo especificado cuando se utilizan tipos parametrizados, del mismo modo que los parámetros formales de un método son marcadores de posición para los valores pasados ​​en tiempo de ejecución.

La motivación para la generalización se puede ver en el marco de las colecciones. Por ejemplo, la clase Map te permite agregar objetos de cualquier clase a un Map, aunque el caso más común es contener objetos de un tipo específico (como String) en un Map determinado.

Dado que Map.get() está definido para devolver un Objeto, generalmente es necesario forzar el tipo de resultado de Map.get() al tipo requerido, como se muestra en el siguiente código:

Mapa m = nuevo HashMap();

m.put("clave", "blarg");

Map.put("clave", "blarg")

Cadena s = (Cadena) m.get("clave");

Cadena s = (Cadena) m.get("clave"); p >String s = (String) m.get("key");

Map.get()get("key"); , debes forzar el tipo de resultado de get() a una cadena y esperar que el resultado sea efectivamente una cadena, pero es posible que alguien haya guardado algo en ese mapa que no sea una cadena, en cuyo caso el código anterior arrojará una ClassCastException.

De hecho, podrías pensar en m como un mapa que asigna claves de cadena a valores de cadena. De esta manera, elimina las conversiones en su código y, al mismo tiempo, obtiene una capa adicional de verificación de tipos que evita que se guarden claves o valores del tipo incorrecto en la colección. Esto es lo que hacen los genéricos.

Ventajas de los genéricos

La introducción de genéricos en el lenguaje Java es una de las principales mejoras funcionales. Para admitir genéricos, no solo el lenguaje, el sistema de tipos y los compiladores sufrieron cambios importantes, sino que las bibliotecas de clases también sufrieron ajustes importantes, haciendo que muchas clases importantes (como el marco de la colección) sean genéricas. Esto trae muchos beneficios:

- Seguridad tipográfica. El objetivo principal de los genéricos es mejorar la seguridad de tipos de los programas Java. Al conocer las restricciones de tipo de las variables definidas mediante genéricos, el compilador puede verificar las suposiciones de tipo en un mayor grado. Sin los genéricos, estas suposiciones existirían sólo en la mente del programador (o, si tenía suerte, en los comentarios del código).

Una técnica popular en los programas Java es definir colecciones cuyos elementos o claves sean de tipo público, como una "lista de cadenas" o un "mapa de cadena a cadena". Los genéricos permiten al compilador imponer estas restricciones de tipo adicionales capturando esta información de tipo adicional en la declaración de variable. Los errores de tipo ahora se pueden detectar en tiempo de compilación en lugar de aparecer como ClassCastException en tiempo de ejecución. Mover la verificación de tipos del tiempo de ejecución al tiempo de compilación le ayuda a encontrar errores más fácilmente y mejora la confiabilidad de su programa.

- Elimina yesos. Otro beneficio de la generalización es la eliminación de muchas conversiones en el código fuente. Esto hará que el código sea más legible y reducirá la posibilidad de errores.

Aunque reducir las conversiones reduce la verbosidad del código que utiliza clases genéricas, declarar variables genéricas también aporta la correspondiente verbosidad. Compare los siguientes dos ejemplos de código.

Este código no utiliza genéricos:

List li = new ArrayList();

li.put(new Integer(3)); >

p>

Entero i = (Entero) li.get(0);

Este código usa genéricos:

List li = new ArrayList< Entero >();

li.put(new Integer(3));

Entero i = li.get(0); Usar una variable común una vez en un programa no reduce la verbosidad del programa. Pero para programas grandes que usan variables genéricas muchas veces, puede reducir acumulativamente la verbosidad.

- Posibles mejoras de rendimiento. Los genéricos brindan la posibilidad de una optimización a mayor escala. En los primeros días de la implementación de genéricos, el compilador insertaría conversiones en el código de bytes generado (en ausencia de genéricos, el programador especificaría conversiones). Sin embargo, dado que el compilador tiene acceso a más información de tipos, esto abre la posibilidad de optimizaciones en futuras versiones de la JVM.

Debido a la forma en que se implementan los genéricos, admitirlos casi no requiere cambios en la JVM o los archivos de clase. Todo el trabajo se realiza en el compilador, y el código generado por el compilador es similar al código que escribirías sin usar genéricos (y conversiones), solo que es más seguro para tipos.

Ejemplos de uso de genéricos

Muchos de los mejores ejemplos de genéricos provienen de marcos de colección, porque los genéricos le permiten especificar restricciones de tipo en los elementos almacenados en una colección. Considere el siguiente ejemplo usando la clase Map, que implica un cierto grado de optimización, es decir, el resultado devuelto por Map.get() será de hecho una cadena:

Map m = new HashMap();

Mapa m = new HashMap();

p>

m.put("key", "blarg");

Cadena s = (Cadena) m.get("key");

Si alguien coloca otro contenido que no sea una cadena en el mapa, el código anterior generará ClassCastException. Los genéricos le permiten expresar restricciones de tipo, es decir, m es un mapa que asigna claves de cadena a valores de cadena. Esto elimina la necesidad de conversiones en el código y, al mismo tiempo, obtiene una capa adicional de verificación de tipos que evita que los usuarios guarden el tipo incorrecto de clave o valor en una colección.

El siguiente ejemplo de código muestra parte de la definición de la interfaz Map en el marco de colecciones en JDK 5.0:

public interface Map {

public void put(K key, V value);

public V get(K key);

}

Tenga en cuenta los dos contenidos agregados a la interfaz:

p>

* Los parámetros de tipo K y V se especifican a nivel de clase, que representan marcadores de posición para los tipos especificados al declarar variables de tipo Map.

* K y V utilizados en las firmas de los métodos get(), put() y otros métodos.

Para obtener los beneficios del uso de genéricos, debes proporcionar valores específicos para K y V cuando defines o instancias una variable de tipo Map. Puedes hacer esto de una manera relativamente intuitiva:

Map m = new HashMap();

m.put( "key", "blarg");

String s = m.get ("key");

Cuando se utiliza la versión genérica de Map, ya no es necesario forzar el resultado de Map. get() se convierte en una cadena.

Cuando usas la versión genérica de Map, ya no necesitas convertir el resultado del Map de get() a una cadena porque el compilador sabe que get() devolverá una cadena. El uso de genéricos solo aporta seguridad de tipo adicional. Debido a que el compilador sabe más sobre los tipos de claves y valores que usted ingresa en el Mapa, la verificación de tipos se traslada del tiempo de ejecución al tiempo de compilación, lo que mejora la confiabilidad y acelera el desarrollo.

Compatibilidad con versiones anteriores

Uno de los objetivos clave al introducir genéricos en el lenguaje Java era mantener la compatibilidad con versiones anteriores. Aunque muchas clases de la biblioteca de clases estándar JDK 5.0, como Collections Framework, se han generalizado, el código existente que utiliza clases de colección como HashMap y ArrayList seguirá ejecutándose en JDK 5.0 sin modificaciones. Por supuesto, el código existente que no utiliza genéricos no obtendrá los beneficios de seguridad de tipos de los genéricos.

Parámetros de tipo

Al definir una clase genérica o declarar variables de una clase genérica, debe utilizar corchetes angulares para especificar parámetros de tipo formal. La relación entre los parámetros de tipo formal y los parámetros de tipo real es similar a la relación entre los parámetros de método formal y los parámetros de método real, excepto que los parámetros de tipo representan tipos en lugar de valores.

Los parámetros de tipo en una clase genérica se pueden usar casi en cualquier lugar donde se pueda usar un nombre de clase. Por ejemplo, lo siguiente es un extracto de la definición de la interfaz java.util.Map:

public interface Map {

public void put(K key, V value );

p>

public V get(K key);

}

La interfaz Map está parametrizada por dos tipos, a saber, el tipo de clave K y el tipo de valor. v. Los métodos que (sin genéricos) aceptan o devuelven objetos ahora usan K o V en la firma de su método, lo que indica que hay restricciones de tipo adicionales bajo la especificación del mapa.

Al declarar o crear una instancia de un objeto de tipo genérico, debe especificar el valor del parámetro de tipo:

Map map = new HashMap ( );

Tenga en cuenta que en este caso el parámetro de tipo debe especificarse dos veces. Una vez al declarar el tipo de mapa de variables y otra vez al elegir la parametrización de la clase HashMap para que se instancia una instancia del tipo correcto.

Cuando el compilador encuentra una variable de tipo Map, sabe que K y V ahora están vinculados a String, por lo que sabe que llamar a Map.get() en dicha variable producirá Tipo de cadena.

Cualquier clase puede tener un parámetro de tipo, excepto los tipos de excepción, enumeraciones o clases internas anónimas.

Parámetros de tipo con nombre

La convención de nomenclatura recomendada es utilizar nombres de una sola letra en mayúsculas para los parámetros de tipo. Esto difiere de la convención de C++ (ver Apéndice A: Comparación con plantillas de C++) y refleja la suposición de que la mayoría de las clases genéricas tendrán una pequeña cantidad de parámetros de tipo. Para patrones genéricos comunes, se recomiendan los siguientes nombres:

* K - clave, como la clave de un mapa.

* V -- Valores, como el contenido de listas y colecciones, o valores en mapas.

* E -- Clase de excepción.

*T--Genérico.

Los genéricos no son covariantes

Una fuente común de confusión acerca de los genéricos es la suposición de que son matrices similares a las covariantes. De hecho, no covarían. List no es un supertipo de List.

Si A extiende a B, entonces la matriz de A también es la matriz de B. Está completamente bien usar A[] donde se requiere B[]:

Entero[] intArray = new Integer[10];

Number[] numberArray = intArray;

El código anterior es válido porque Integer es Number, por lo que la matriz de Integer es la matriz de Numbers. Pero este no es el caso de los genéricos. El siguiente código no es válido:

List intList = new ArrayList();

List numberList = intList // no válido

Inicialmente, la mayoría de los programadores de Java encuentran esta falta de covarianza molesta o incluso "mala", pero tiene sentido.

List numberList = intList; // invalid

numberList.add(new Float(3.1415));

Porque tanto intList como numberList tienen alias. , el código anterior le permitirá colocar contenidos que no sean enteros en una intList si está permitido. Sin embargo, como verá en la siguiente pantalla, puede definir genéricos de una manera más flexible.

paquete com.ibm.course.generics;

importar java.util.GenericsExample {

public static void main(String[] args) {

p>

Entero[] entero = nuevo Entero[5];

Número[] número = entero

System.out.println(número[ 0]); // nulo

número[0] = nuevo Float(7.65

System.out.println(número[0]); >System.out. println(integer[0]);

List list = new ArrayList()

// No coincide el tipo: no se puede convertir desde List<. .Integer> Lista<.Integer> .

Entero> convertido a Lista

// Lista listObj = lista

}

}

Lista<; Número> listObj = list; genera un error de compilación:

Y System.out.println(number[0]); y System.out.println( integer[0]);

Excepción en el hilo "principal" java.lang.ArrayStoreException: java.lang.Float

en com.ibm.course.generics.GenericsExample.main(GenericsExample.java:15)

Escriba comodines

Supongamos que tiene un método como este:

void printList(List l) {

for (Object o : l)

System.out.println(o);

}

El código anterior se compila bien en JDK 5.0, pero si intenta utilizar List< Entero> llámalo, recibirás una advertencia. La advertencia se produce porque está pasando un tipo genérico (List) a un método que solo promete tratarlo como una Lista (el llamado tipo primitivo), lo que rompe la seguridad de tipos al usar genéricos.

¿Qué pasará si intentas escribir el siguiente método?

void printList(List< Objeto> l) {

for (Objeto o : l)

System.out.println(o); >

}

Aún no se compilará porque List<.Integer> no es List (como aprendimos en la pantalla anterior, las generalizaciones no son covariantes). Esta es la parte realmente molesta: ¡ahora su versión genérica no es tan útil como la versión normal no genérica!

La solución es utilizar comodines de tipo:

void printList(List l) {

for (Object o : l)

System.out.println(o);

}

El signo de interrogación en el código anterior es un comodín de tipo. list es el supertipo de cualquier Lista genérica, por lo que puede pasar List, List o List>> a printList().

paquete com.ibm.course.generics

importar java.util.ArrayList

importar java.util.List

public class GenericExample {

public static void main(String[] args) {

List entero = new ArrayList(); > entero.add(nuevo Integer(0));

integer.add(nuevo Integer(1));

List str = new ArrayList();

str.add(new String("Mundo"));

Lista li=integer

li=str; >

printList(entero);

printList(str);

}

Lista

impresión pública estática vacía (Lista l) {

for (Objeto o : l) {

System.out.println(o); >

}

}

}

El programa de ejemplo anterior no tiene advertencias ni errores de compilación.

¿Qué tipo de comodines

Los comodines de tipo, introducidos en la pantalla anterior, le permiten declarar variables de tipo Lista

List li = new ArrayList()

li.add(new Integer(42)); p>List lu = li;

System.out.println(lu.get(0));

¿Por qué funciona el código? Para lu, el compilador no tiene idea del valor del parámetro de tipo Lista. Pero el compilador es lo suficientemente inteligente como para hacer alguna inferencia de tipos. En este caso, se infiere que el parámetro de tipo desconocido debe extender el Objeto. (Esta inferencia no es un gran salto, pero el compilador puede hacer una inferencia de tipos bastante admirable, como verá más adelante (en la sección "Detalles básicos")). Por lo tanto, el compilador le permite llamar a List.get() e infiere que el tipo de retorno es un objeto.

Por otro lado, el siguiente código no funciona:

List li = new ArrayList()

li.add; (nuevo entero(42));

Lista lu = li

lu.add(nuevo entero(43)); ?add (new Integer(43)); // Error

En este ejemplo, para lu, el compilador no puede razonar cuidadosamente sobre los parámetros de tipo de List para determinar que pasar un número entero a List.add() es tipo. -seguro de. Entonces el compilador no te permitirá hacer esto.

Si todavía cree que el compilador sabe qué métodos cambiarán el contenido de la lista y cuáles no, tenga en cuenta que el siguiente código funcionará bien porque no depende de que el compilador tenga ningún conocimiento. del parámetro de tipo lu:

List li = new ArrayList<.

List< Integer> (

li.add(new Integer(); 42));

Lista lu = li

lu.clear();

Métodos genéricos

(en En la sección "Parámetros de tipo", ha visto que una clase se puede generalizar agregando una lista de parámetros de tipo formal a la definición de clase, ya sea que la clase en la que se define el método esté generalizada o no.

Clases. aplique restricciones de tipo en firmas de múltiples métodos. En List, el parámetro de tipo V aparece en las firmas de get(), add(), contains() y otros métodos al crear el tipo Map. necesita establecer restricciones de tipo entre métodos. El valor que pase a add() será del mismo tipo que el valor devuelto por get()

La misma razón para declarar un método genérico. declarar restricciones de tipo entre múltiples parámetros del método. Por ejemplo, el método ifThenElse() en el siguiente código devuelve el segundo o tercer parámetro según el valor booleano de su primer parámetro:

public T. ifThenElse(boolean b, T primero, T segundo) {

return b ? primero : segundo

}

Tenga en cuenta que puede llamar a ifThenElse() sin; decirle explícitamente al compilador cuál desea que sea el valor de T. El compilador no necesita indicar explícitamente el valor de T, solo sabe que todos los valores deben ser iguales porque el compilador le permite llamar al siguiente código. El compilador puede utilizar la inferencia de tipos para deducir que la cadena que reemplaza a T satisface todas las restricciones de tipo:

String s = ifThenElse(b, "a", "b");

Del mismo modo, También puedes llamar:

Integer i = ifThenElse(b, new Integer(1), new Integer(2)

Sin embargo, el compilador no permite el siguiente código. porque Ningún tipo satisface las restricciones de tipo requeridas:

String s = ifThenElse(b, "pi", new Float(3.14));

¿Por qué eligió utilizar un método genérico? , en lugar de agregar el tipo T en la definición de clase? (Hay al menos dos casos en los que debes hacer esto:

* Los métodos genéricos son estáticos, en cuyo caso no puedes usar parámetros de tipo de clase.

* Cuando una restricción de tipo en T es verdaderamente local para el método, esto significa que el mismo tipo de restricción T no se puede usar en otra firma de método de la misma clase. Las firmas de tipo adjuntas se pueden simplificar haciendo que los parámetros de tipo de un método genérico sean locales.

Tipos restringidos

En el ejemplo anterior de un método genérico, el parámetro de tipo V es un tipo sin restricciones o sin restricciones. A veces es necesario especificar restricciones adicionales en los parámetros de tipo cuando no están completamente especificados.

Considere el ejemplo de la clase Matrix, que utiliza un parámetro de tipo V vinculado por la clase Number:

clase pública Matrix { ...}

El compilador le permite crear variables de tipo Matrix o Matrix, pero si intenta definir una variable de tipo Matrix, se produce un error. Se determina que el parámetro de tipo V está limitado por Número. Si no hay restricción de tipo, se supone que los parámetros de tipo están restringidos a Objeto. Es por eso que el ejemplo del método genérico en la pantalla anterior permite llamar a List.get() en una Lista para devolver un Objeto incluso si el compilador no conoce el tipo del parámetro de tipo V.