El propósito de este artículo es introducir genéricos de Java, para que todos puedan tener una comprensión final, clara y precisa de todos los aspectos de Java Generics, y también sentar las bases para el próximo artículo "Reinstandiar la reflexión de Java".
Introducción
Los genéricos son un punto de conocimiento muy importante en Java. Los genéricos se usan ampliamente en los marcos de colección Java. En este artículo, analizaremos el diseño de genéricos Java desde cero, lo que implicará el procesamiento de comodín y el borrado de tipo angustiante.
Conceptos básicos genéricos
Clases genéricas
Primero definamos una clase de caja simple:
Box de clase pública {objeto de cadena privada; Public void set (objeto de cadena) {this.object = object; } public String get () {Object return; }}Esta es la práctica más común. Una de las desventajas de esto es que solo se pueden cargar elementos de tipo de cadena en el cuadro. En el futuro, si necesitamos cargar otros tipos de elementos como Integer, también debemos reescribir otra caja. El código no se puede reutilizar, y el uso de genéricos puede resolver bien este problema.
Box de clase pública <T> {// t significa "tipo" privado t t; Public void set (t t) {this.t = t; } public t get () {return t; }}De esta manera, nuestra clase de caja se puede reutilizar, y podemos reemplazar T con cualquier tipo que queramos:
Box <Integer> InteGerbox = new Box <Integer> (); box <doble> doubleBox = new Box <SoTe> (); Box <String> StringBox = New Box <String> ();
Métodos genéricos
Después de leer la clase genérica, aprendamos sobre métodos genéricos. Declarar un método genérico es simple, solo agregue un formulario similar a <k, v> al tipo de retorno:
public class Util {public static <k, v> boolean compare (par <k, v> p1, par <k, v> p2) {return p1.getKey (). Equals (p2.getkey ()) && p1.getValue (). Equals (p2.getValue ()); }} par de clase pública <k, v> {private k key; valor v privado; Public Par (K Key, V Value) {this.key = key; this.Value = value; } public void setKey (k key) {this.key = key; } public void setValue (V valor) {this.value = valor; } public k getkey () {return key; } public v getValue () {Valor de retorno; }}Podemos llamar métodos genéricos como este:
Par <Integer, String> P1 = New Par <> (1, "Apple"); Par <Integer, String> P2 = New Par <> (2, "Pear"); boolean mismo = util. <Integer, String> Compare (p1, p2);
O use la inferencia de tipo en Java 1.7/1.8 para dejar que Java deduzca automáticamente los parámetros de tipo correspondientes:
Par <Integer, String> P1 = New Par <> (1, "Apple"); Par <Integer, String> P2 = New Par <> (2, "Pear"); boolean mismo = util.Compare (p1, p2);
Símbolo de límite
Ahora queremos implementar dicha función para encontrar el número de elementos en una matriz genérica que son más grandes que un elemento específico. Podemos implementarlo así:
public static <t> int countGreaterThan (t [] anArray, t elem) {int count = 0; para (t e: anArray) if (e> elem) // error del compilador ++ recuento; recuento de retorno;}Pero esto obviamente es incorrecto, porque a excepción de los tipos primitivos como corto, int, doble, largo, flotante, byte, char, etc., otras clases pueden no usar necesariamente operadores>, por lo que el compilador informa un error. ¿Cómo resolver este problema? La respuesta es usar el símbolo límite.
interfaz pública comparable <t> {public int Compareto (t o);}Haga una declaración similar a la siguiente, que es equivalente a decirle al compilador que el parámetro T de tipo t representa clases que implementan la interfaz comparable, que es equivalente a decirle al compilador que todos implementan al menos el método Compareto.
public static <t extiende <t>> int countGreaterthan (t [] anarray, t elem) {int count = 0; para (t e: anarray) if (e.compareto (elem)> 0) ++ recuento; recuento de retorno;}Comodín
Antes de comprender los comodines, primero debemos aclarar un concepto, o tomar prestada la clase de caja que definimos anteriormente, supongamos que agregamos un método como este:
public void boxtest (box <número> n) { / * ... * /}Entonces, ¿qué tipo de parámetros se permite aceptar el cuadro <número> n? ¿Podemos pasar en el cuadro <integer> o el cuadro <Wouble>? La respuesta es no. Aunque Integer y Double son subclases de número, no existe una relación entre Box <integer> o Box <Wouble> y Box <Number> en Generics. Esto es muy importante, y utilizaremos un ejemplo completo para profundizar nuestra comprensión.
Primero, definimos algunas clases simples, y las usaremos a continuación:
class Fruit {} Class Apple extiende la fruta {} Naranja de clase extiende la fruta {}En el siguiente ejemplo, creamos un lector de clase genérico, y luego en f1 (), cuando probamos la fruta f = frutiter.readexact (manzanas); El compilador informará un error porque no existe una relación entre la lista <fruit> y la lista <pple>.
public class Genericreading {Lista estática <Apple> Apple = Arrays.aslist (new Apple ()); Lista estática <Strante> fruit = arrays.aslist (nueva fruta ()); lector de clase estática <t> {t readExact (list <t> list) {return list.get (0); }} static void f1 () {Reader <Strante> frruteder = new Reader <trelt> (); // Errores: List <Strante> no se puede aplicar a la lista <pple>. // Fruit F = Fruiterer.ReadExact (manzanas); } public static void main (string [] args) {f1 (); }}Pero de acuerdo con nuestros hábitos de pensamiento habituales, debe haber una conexión entre la manzana y la fruta, pero el compilador no puede reconocerlo. Entonces, ¿cómo puedo resolver este problema en código genérico? Podemos resolver este problema usando comodines:
clase estática CovarianTreader <T> {t readcovariant (list <? extiende t> list) {return list.get (0); }} void static f2 () {covarianTreader <trante> frromeReader = new CovarianTreader <Strante> (); Fruta f = lector frutal.readCovariant (fruta); Fruit a = fruiter.readCovariant (manzanas);} public static void main (string [] args) {f2 ();}Esto es bastante similar a decirle al compilador que los parámetros aceptados por el método ReadCovariant del fruitre es tan largo como la subclase que satisface la fruta (incluida la fruta misma), de modo que la relación entre la subclase y la clase principal también está asociada.
Principios de PECS
Vimos un uso similar a <? extiende t> arriba. Usándolo, podemos obtener elementos de la lista, entonces, ¿podemos agregar elementos a la lista? Vamos a intentarlo:
Class Public Generics y Covariance {public static void main (string [] args) {// comodines permiten covarianza: list <? extiende la fruta> flist = new ArrayList <Apple> (); // Error de compilación: no se puede agregar ningún tipo de objeto: // flist.add (new Apple ()) // flist.add (neuran ()) // flist.add (new fruit ()) // flist.add (nuevo objeto ()) flist.add (null); // Legal pero poco interesante // Sabemos que devuelve al menos fruta: fruta f = flist.get (0); }}La respuesta es no, el compilador Java no nos permite hacer esto, ¿por qué? También podríamos considerar este problema desde la perspectiva del compilador. Porque la lista <? extiende la fruta> flist puede tener muchos significados:
Lista <? extiende fruta> flist = new ArrayList <Strante> (); List <? extiende fruta> flist = new ArrayList <Apple> (); List <? extiende fruta> flist = new ArrayList <arnane> ();
Por lo tanto, para las clases de recolección que implementan <? extiende t>, solo pueden considerarse como un productor que proporciona (obtener) elemento al exterior, y no se pueden usar como un consumidor para obtener (agregar) elementos al exterior.
¿Qué debemos hacer si queremos agregar el elemento? Puedes usar <? Super t>:
Public Class GenericWriting {Lista estática <Apple> Apple = New ArrayList <Apple> (); Lista estática <Strante> fruit = new ArrayList <String> (); static <t> void writeExact (list <t> list, t item) {list.add (elemento); } static void f1 () {writeExact (manzanas, new Apple ()); writeExact (fruta, nueva manzana ()); } static <t> void writeWithWildCard (list <? Super t> list, t item) {list.add (item)} static void f2 () {writeWithWildCard (manzanas, new Apple ()); writeWithWildCard (fruta, nueva manzana ()); } public static void main (string [] args) {f1 (); f2 (); }}De esta manera, podemos agregar elementos al contenedor, pero la desventaja de usar Super es que no podemos obtener elementos en el contenedor en el futuro. La razón es muy simple. Continuamos considerando este problema desde la perspectiva del compilador. Para la lista <? Lista de Super Apple>, puede tener los siguientes significados:
Lista <? Super Apple> list = new ArrayList <Apple> (); List <? Super Apple> list = new ArrayList <Strante> (); List <? Super Apple> list = new ArrayList <ject> ();
Cuando intentamos obtener una lista de manzana a través de la lista, podemos obtener una fruta, que puede ser otros tipos de frutas como el naranja.
Según el ejemplo anterior, podemos resumir una regla, "El productor extiende, el consumidor super":
Después de leer algunos código fuente de Java Collections, podemos encontrar que generalmente usamos los dos juntos, como los siguientes:
Public Class Collections {public static <t> void Copy (list <? Super t> dest, list <? Extends t> src) {for (int i = 0; i <src.size (); i ++) dest.set (i, src.get (i)); }}Tipo de borrado
Quizás lo más angustiante de Java Genorics es el tipo de borrado, especialmente para programadores con experiencia C ++. El tipo de borrado significa que Java Generics solo puede usarse para la verificación de tipo estático durante la compilación, y luego el código generado por el compilador borrará la información de tipo correspondiente. De esta manera, durante la ejecución, el JVM en realidad conoce el tipo específico representado por el genérico. El propósito de esto se debe a que Java Generics se introdujo después de 1.5. Para mantener la compatibilidad hacia abajo, solo puede hacer un borrado de tipo para ser compatible con el código no genérico anterior. Para este punto, si lee el código fuente del marco de la colección Java, puede encontrar que algunas clases en realidad no admiten genéricos.
Habiendo dicho tanto, ¿qué significa Borrure genérico? Primero veamos el siguiente ejemplo simple:
nodo de clase pública <t> {datos privados t; nodo privado <t> siguiente; nodo público (t data, nodo <t> next)} this.data = data; this.next = siguiente; } public t getData () {return data; } // ...}Después de que el compilador complete la verificación de tipo correspondiente, el código anterior realmente se convertirá en:
Nodo de clase pública {datos de objetos privados; Nodo privado Siguiente; nodo público (datos de objeto, nodo siguiente) {this.data = data; this.next = siguiente; } Public Object getData () {return data; } // ...}Esto significa que no importa si declaramos el nodo <string> o el nodo <integer>, el JVM se considera el nodo <ject> durante el tiempo de ejecución. ¿Hay alguna forma de resolver este problema? Esto requiere que restablecamos los límites nosotros mismos y modifiquemos el código anterior al siguiente:
Public Class Node <t extiende <T>> {datos de T privado; nodo privado <t> siguiente; nodo public (t data, nodo <t> next) {this.data = data; this.next = siguiente; } public t getData () {return data; } // ...}De esta manera, el compilador reemplazará el lugar donde T aparece con comparable en lugar del objeto predeterminado:
Public Class Node {datos comparables privados; Nodo privado Siguiente; nodo público (datos comparables, nodo siguiente) {this.data = data; this.next = siguiente; } public comparable GetData () {Data de retorno; } // ...}El concepto anterior puede ser más fácil de entender, pero de hecho, el borrado genérico trae muchos más problemas. A continuación, echemos un vistazo sistemático a algunos de los problemas traídos por el tipo de borrado. Es posible que algunos problemas no se encuentren en genéricos C ++, pero debe tener mucho cuidado en Java.
Pregunta 1
Las matrices genéricas no están permitidas en Java. Si el compilador hace algo como lo siguiente, informará un error:
Lista <integer> [] arrayofLists = nueva lista <integer> [2]; // Error de tiempo de compilación
¿Por qué el compilador no admite la práctica anterior? Continuar utilizando el pensamiento inversa, consideramos este problema desde la perspectiva del compilador.
Primero veamos el siguiente ejemplo:
Objeto [] strings = new String [2]; Strings [0] = "HI"; // okstrings [1] = 100; // se lanza una arraystoreException.
El código anterior es fácil de entender. Las matrices de cadenas no pueden almacenar elementos enteros, y tales errores a menudo deben descubrirse hasta que se ejecute el código, y el compilador no puede reconocerlos. A continuación, echemos un vistazo a lo que sucederá si Java admite la creación de matrices genéricas:
Objeto [] stringLists = new List <String> []; // Error del compilador, pero fingir que está permitido StringLists [0] = new ArrayList <String> (); // ok // debe lanzarse una arraystoreException, pero el tiempo de ejecución no puede detectarlo.
Supongamos que apoyamos la creación de matrices genéricas. Dado que se ha borrado la información de tipo durante el tiempo de ejecución, el JVM en realidad no sabe la diferencia entre New ArrayList <String> () y New ArrayList <Integer> () en absoluto. Si tales errores ocurren en escenarios prácticos de aplicación, serán muy difíciles de detectar.
Si todavía es escéptico de esto, puede intentar ejecutar el siguiente código:
public class EredTypeequivalence {public static void main (string [] args) {class c1 = new ArrayList <String> (). getClass (); Clase C2 = New ArrayList <Integer> (). GetClass (); System.out.println (C1 == C2); // verdadero }}Pregunta 2
Continúe reutilizando nuestra clase de nodo anterior. Para el código genérico, el compilador Java en realidad nos ayudará en secreto a implementar un método de puente.
nodo de clase pública <t> {data public t; nodo public (t data) {this.data = data; } public void setData (t data) {system.out.println ("node.setData"); this.data = data; }} La clase pública myNode extiende el nodo <integer> {public myNode (Integer data) {super (data); } public void setData (Integer Data) {System.out.println ("myNode.setData"); super.setData (datos); }}Después de leer el análisis anterior, puede pensar que después de borrar el tipo, el compilador convertirá el nodo y el mynode en lo siguiente:
Public Class Node {Public Object Data; Public nodo (datos de objeto) {this.data = data; } public void setData (datos de objeto) {system.out.println ("node.setData"); this.data = data; }} La clase pública myNode extiende el nodo {public myNode (Integer Data) {super (datos); } public void setData (Integer Data) {System.out.println ("myNode.setData"); super.setData (datos); }}En realidad, este no es el caso. Primero veamos el siguiente código. Cuando se ejecuta este código, se lanzará una ClassCastException, lo que solicita que la cadena no se puede convertir en entero:
MyNode mn = nuevo mynode (5); nodo n = mn; // Un tipo sin procesar - El compilador arroja una advertencia sin control.setData ("hola"); // hace que se arroje una ClassCastException .// Integer x = Mn.Data;Si seguimos el código que generamos anteriormente, no debemos informar un error al ejecutar a la línea 3 (tenga en cuenta que comenté la línea 4), porque el método SetData (String Data) no existe en MyNode, por lo que solo podemos llamar al método SetData (datos de objetos) del nodo de clase principal. Dado que de esta manera, el código de línea 3 anterior no debe informar un error, porque, por supuesto, la cadena se puede convertir a objeto, entonces, ¿cómo se lanza ClassCastException?
De hecho, el compilador Java maneja automáticamente el código anterior:
La clase MyNode extiende el nodo {// Método del puente generado por el compilador public void setData (objetos datos) {setData ((integer) data); } public void setData (Integer Data) {System.out.println ("myNode.setData"); super.setData (datos); } // ...}Es por eso que se informa el error anterior. Cuando setData ((Integer) datos); La cadena no se puede convertir en entero. Por lo tanto, cuando el compilador indica una advertencia sin control en la línea 2 anterior, no podemos elegir ignorarlo, de lo contrario tendremos que esperar hasta el tiempo de ejecución para encontrar la excepción. Sería genial si agregamos el nodo <integer> n = mn al principio, para que el compilador pueda ayudarnos a encontrar errores por adelantado.
Pregunta 3
Como mencionamos anteriormente, Java Generics solo puede proporcionar una verificación de tipo estático en gran medida, y luego se borrará la información de tipo, por lo que el compilador no pasará el siguiente método de uso de parámetros de tipo para crear instancias:
public static <E> void append (list <E> list) {e elem = new e (); // compilar el tiempo de error list.add (elem);}Pero, ¿qué debemos hacer si queremos crear instancias usando parámetros de tipo en ciertos escenarios? La reflexión se puede usar para resolver este problema:
public static <E> void append (List <E> List, Class <E> CLS) lanza la excepción {e elem = cls.newinstance (); // ok list.add (elem);}Podemos llamarlo así:
List <String> ls = new ArrayList <> (); append (ls, string.class);
De hecho, para el problema anterior, también puede usar patrones de diseño de fábrica y plantilla para resolverlo. Los amigos interesados pueden desear echar un vistazo a la explicación de la creación de la instancia de tipos en el Capítulo 15 en el pensamiento en Java. No entraremos en eso aquí.
Pregunta 4
No podemos usar la instancia de la palabra clave directamente para el código genérico, porque el compilador Java borrará toda la información de tipo genérico relevante al generar el código, al igual que el JVM que verificamos anteriormente no puede reconocer la diferencia entre ArrayList <Integer> y ArrayList <String> durante el tiempo de ejecución:
public static <E> void rtti (List <E> list) {if (List instanceOf ArrayList <Integer>) {// compilación de tiempo de tiempo // ...}} => {arrayList <integer>, arrayList <String>, LinkedList <caracteres>, ...}Como se indicó anteriormente, podemos usar comodines para restablecer límites para resolver este problema:
public static void rtti (list <?> list) {if (list instanceOf arrayList <?>) {// ok; instanciaf requiere un tipo reifable // ...}}Resumir
Lo anterior se trata de volver a comprender los genéricos de Java en este artículo, espero que sea útil para todos. Los amigos interesados pueden continuar referiéndose a este sitio:
Explicación detallada de los conceptos básicos de matriz de Java
Los conceptos básicos de la programación de Java: imitando el intercambio de código de inicio de sesión del usuario
Los conceptos básicos de la programación de redes de Java: comunicación unidireccional
Si hay alguna deficiencia, deje un mensaje para señalarlo. ¡Gracias amigos por su apoyo para este sitio!