Genéricos en Java

Forma de abstracción de un tipo o método para permitir que sean funciones de un parámetro de tipo

Los genéricos son una función de programación genérica que se agregó al lenguaje de programación Java en 2004 dentro de la versión J2SE 5.0. Fueron diseñados para extender el sistema de tipos de Java para permitir que "un tipo o método opere en objetos de varios tipos mientras proporciona seguridad de tipos en tiempo de compilación". [1] El aspecto de seguridad de tipos en tiempo de compilación requería que las funciones polimórficas paramétricas no se implementaran en la máquina virtual Java , ya que la seguridad de tipos es imposible en este caso. [2] [3]

El marco de colecciones de Java admite genéricos para especificar el tipo de objetos almacenados en una instancia de colección.

En 1998, Gilad Bracha , Martin Odersky , David Stoutamire y Philip Wadler crearon Generic Java, una extensión del lenguaje Java para soportar tipos genéricos. [4] Generic Java se incorporó a Java con la adición de comodines .

Jerarquía y clasificación

Según la especificación del lenguaje Java : [5]

  • Una variable de tipo es un identificador no calificado. Las variables de tipo se introducen mediante declaraciones de clase genéricas, declaraciones de interfaz genéricas, declaraciones de método genéricas y declaraciones de constructor genéricas.
  • Una clase es genérica si declara una o más variables de tipo. [6] Define una o más variables de tipo que actúan como parámetros. [7] Una declaración de clase genérica define un conjunto de tipos parametrizados, uno para cada posible invocación de la sección de parámetros de tipo. Todos estos tipos parametrizados comparten la misma clase en tiempo de ejecución.
  • Una interfaz es genérica si declara una o más variables de tipo. [7] Define una o más variables de tipo que actúan como parámetros. [7] Una declaración de interfaz genérica define un conjunto de tipos, uno para cada posible invocación de la sección de parámetros de tipo. Todos los tipos parametrizados comparten la misma interfaz en tiempo de ejecución.
  • Un método es genérico si declara una o más variables de tipo. [8] Estas variables de tipo se conocen como los parámetros de tipo formales del método. La forma de la lista de parámetros de tipo formales es idéntica a la de una lista de parámetros de tipo de una clase o interfaz.
  • Un constructor puede declararse como genérico, independientemente de si la clase en la que se declara el constructor es genérica o no. Un constructor es genérico si declara una o más variables de tipo. Estas variables de tipo se conocen como los parámetros de tipo formales del constructor. La forma de la lista de parámetros de tipo formales es idéntica a la de una lista de parámetros de tipo de una clase o interfaz genérica.

Motivación

El siguiente bloque de código Java ilustra un problema que existe cuando no se utilizan genéricos. Primero, declara un ArrayListdel tipo Object. Luego, agrega un Stringal ArrayList. Finalmente, intenta recuperar el agregado Stringy lo convierte en un Integer: un error en la lógica, ya que generalmente no es posible convertir una cadena arbitraria en un entero.

final List v = new ArrayList (); v . add ( "test" ); // Una cadena que no se puede convertir a un entero final Integer i = ( Integer ) v . get ( 0 ); // Error de tiempo de ejecución            

Aunque el código se compila sin errores, arroja una excepción de tiempo de ejecución ( java.lang.ClassCastException) al ejecutar la tercera línea de código. Este tipo de error lógico se puede detectar durante el tiempo de compilación mediante el uso de genéricos [7] y es la principal motivación para usarlos. [6] Define una o más variables de tipo que actúan como parámetros.

El fragmento de código anterior se puede reescribir utilizando genéricos de la siguiente manera:

final List < String > v = new ArrayList < String > (); v . add ( "prueba" ); final Integer i = ( Integer ) v . get ( 0 ); // (error de tipo) error de tiempo de compilación           

El parámetro de tipo Stringdentro de los corchetes angulares declara que el ArrayListobjeto que se va a constituir es String(un descendiente de los constituyentes ArrayListgenéricos de Object). Con los genéricos, ya no es necesario convertir la tercera línea a ningún tipo en particular, porque el resultado de v.get(0)se define como Stringpor el código generado por el compilador.

El fallo lógico en la tercera línea de este fragmento se detectará como un error de tiempo de compilación (con J2SE 5.0 o posterior) porque el compilador detectará que v.get(0)retorna Stringen lugar de Integer. [7] Para un ejemplo más elaborado, consulte la referencia. [9]

A continuación se muestra un pequeño extracto de la definición de las interfaces java.util.Listy java.util.Iteratordel paquete java.util:

interfaz  Lista < E > {  void add ( E x ); Iterador < E > iterador ();    }interfaz  Iterador < E > {  E siguiente (); booleano hasNext ();   }

Definiciones de clases genéricas

A continuación se muestra un ejemplo de una clase Java genérica que se puede utilizar para representar entradas individuales (asignaciones de clave a valor) en un mapa :

clase pública Entrada < KeyType , ValueType > { privada final KeyType clave ; privada final ValueType valor ;              Entrada pública ( KeyType clave , ValueType valor ) { this.key = clave ; this.value = valor ; }              public KeyType getKey () { devolver clave ; }       public ValueType getValue () { valor de retorno ; }       public String toString () { return "(" + clave + ", " + valor + ")" ; }                }

Esta clase genérica podría utilizarse de las siguientes maneras, por ejemplo:

Entrada final < String , String > calificación = nueva Entrada < String , String > ( "Mike" , "A" ); Entrada final < String , Integer > nota = nueva Entrada < String , Integer > ( "Mike" , 100 ); System . out . println ( "calificación: " + nota ); System . out . println ( "nota: " + nota );                    Entrada final < Entero , Booleano > primo = nueva Entrada < Entero , Booleano > ( 13 , verdadero ); si ( primo . obtenerValor ()) { Sistema . salir . imprimirln ( primo . obtenerClave () + " es primo." ); } de lo contrario { Sistema . salir . imprimirln ( primo . obtenerClave () + " no es primo." ); }                 

El resultado es:

Calificación: (Mike, A)marca: (Mike, 100)13 es primo.

Definiciones de métodos genéricos

A continuación se muestra un ejemplo de un método genérico que utiliza la clase genérica anterior:

public static < Tipo > Entrada < Tipo , Tipo > dos veces ( Tipo valor ) { return new Entrada < Tipo , Tipo > ( valor , valor ); }            

Nota: Si eliminamos el primero <Type>en el método anterior, obtendremos un error de compilación (no se puede encontrar el símbolo "Tipo"), ya que representa la declaración del símbolo.

En muchos casos, el usuario del método no necesita indicar los parámetros de tipo, ya que se pueden inferir:

Entrada final < String , String > par = Entrada . twice ( "Hola" );     

Los parámetros se pueden agregar explícitamente si es necesario:

Entrada final < String , String > par = Entrada . < String > dos veces ( "Hola" );     

No se permite el uso de tipos primitivos y en su lugar se deben utilizar versiones en caja :

Entrada final < int , int > par ; // La compilación falla. Utilice enteros en su lugar.    

También existe la posibilidad de crear métodos genéricos basados ​​en parámetros dados.

público < Tipo > Tipo [] toArray ( Tipo ... elementos ) { devolver elementos ; }       

En estos casos tampoco se pueden utilizar tipos primitivos, por ejemplo:

Entero [] matriz = toArray ( 1 , 2 , 3 , 4 , 5 , 6 );        

Operador de diamantes

Gracias a la inferencia de tipos , Java SE 7 y versiones posteriores permiten al programador sustituir un par de corchetes angulares vacíos ( <>, llamado operador de diamante ) por un par de corchetes angulares que contienen uno o más parámetros de tipo que implica un contexto suficientemente cercano . [10] Por lo tanto, el ejemplo de código anterior que utiliza Entryse puede reescribir como:

Entrada final < String , String > calificación = nueva Entrada <> ( "Mike" , "A" ); Entrada final < String , Integer > nota = nueva Entrada <> ( "Mike" , 100 ); System . out . println ( "calificación: " + nota ); System . out . println ( "nota: " + nota );                  Entrada final < Entero , Booleano > primo = nueva Entrada <> ( 13 , verdadero ); si ( primo . obtenerValor ()) Sistema . imprimirln ( primo . obtenerClave ( ) + " es primo." ) ; de lo contrario Sistema . imprimirln ( primo . obtenerClave ( ) + " no es primo." ) ;              

Tipos de comodines

Un argumento de tipo para un tipo parametrizado no está limitado a una clase o interfaz concreta. Java permite el uso de "comodines de tipo" para que sirvan como argumentos de tipo para tipos parametrizados. Los comodines son argumentos de tipo en la forma " "; opcionalmente con un límite<?> superior o inferior . Dado que se desconoce el tipo exacto representado por un comodín, se imponen restricciones sobre el tipo de métodos que se pueden llamar en un objeto que utiliza tipos parametrizados.

A continuación se muestra un ejemplo en el que el tipo de elemento de a Collection<E>está parametrizado mediante un comodín:

Colección final <?> c = new ArrayList < String > (); c . add ( new Object ()); // error de tiempo de compilación c . add ( null ); // permitido        

Como no sabemos qué tipo de elemento crepresenta, no podemos agregarle objetos. El add()método toma argumentos de tipo E, el tipo de elemento de la Collection<E>interfaz genérica. Cuando el argumento de tipo real es ?, representa un tipo desconocido. Cualquier valor de argumento de método que pasemos al add()método tendría que ser un subtipo de este tipo desconocido. Como no sabemos qué tipo es, no podemos pasar nada. La única excepción es null ; que es un miembro de cada tipo. [11]

Para especificar el límite superior de un comodín de tipo, extendsse utiliza la palabra clave para indicar que el argumento de tipo es un subtipo de la clase delimitadora. [12] Esto significa que la lista dada contiene objetos de algún tipo desconocido que extiende la clase. Por ejemplo, la lista podría ser o . Al leer un elemento de la lista se devolverá un . También se permite agregar elementos nulos. [13]List<? extends Number>NumberList<Float>List<Number>Number

El uso de comodines anterior agrega flexibilidad [12] ya que no existe ninguna relación de herencia entre dos tipos parametrizados con un tipo concreto como argumento de tipo. Ninguno de los List<Number>dos List<Integer>es un subtipo del otro; aunque Integeres un subtipo de Number. [12] Por lo tanto, cualquier método que tome List<Number>como parámetro no acepta un argumento de List<Integer>. Si lo hiciera, sería posible insertar un Numberque no es an Integeren él; lo que viola la seguridad de tipos. Aquí hay un ejemplo que demuestra cómo se violaría la seguridad de tipos si List<Integer>fuera un subtipo de List<Number>:

final List < Integer > ints = new ArrayList <> (); ints . add ( 2 ); final List < Number > nums = ints ; // válido si List<Integer> fuera un subtipo de List<Number> según la regla de sustitución. nums . add ( 3.14 ); final Integer x = ints . get ( 1 ); // ¡ahora 3.14 está asignado a una variable Integer!                

La solución con comodines funciona porque no permite operaciones que violen la seguridad de tipos:

Lista final <? extends Number > nums = ints ; // OK nums . add ( 3.14 ); // error de tiempo de compilación nums . add ( null ); // permitido         

Para especificar la clase de límite inferior de un comodín de tipo, superse utiliza la palabra clave . Esta palabra clave indica que el argumento de tipo es un supertipo de la clase de límite. Por lo tanto, podría representar o . Al leer una lista definida como se devuelven elementos del tipo . Para agregar elementos a dicha lista se requieren elementos del tipo , cualquier subtipo de o null (que es un miembro de cada tipo).List<? super Number>List<Number>List<Object>List<? super Number>ObjectNumberNumber

El mnemónico PECS (Producer Extends, Consumer Super) del libro Effective Java de Joshua Bloch ofrece una forma sencilla de recordar cuándo utilizar comodines (correspondientes a covarianza y contravarianza ) en Java. [12]

Genéricos en la cláusula throws

Aunque las excepciones en sí mismas no pueden ser genéricas, los parámetros genéricos pueden aparecer en una cláusula throws:

público < T extiende Throwable > void throwMeConditional ( booleano condicional , T excepción ) lanza T { if ( condicional ) { lanzar excepción ; } }                 

Problemas con el borrado de tipos

Los genéricos se comprueban en tiempo de compilación para comprobar su tipo correcto. [7] A continuación, la información de tipo genérico se elimina en un proceso denominado borrado de tipo . [6] Por ejemplo, List<Integer>se convertirán al tipo no genérico List, que normalmente contiene objetos arbitrarios. La comprobación en tiempo de compilación garantiza que el código resultante utilice el tipo correcto. [7]

Debido al borrado de tipo, los parámetros de tipo no se pueden determinar en tiempo de ejecución. [6] Por ejemplo, cuando ArrayListse examina an en tiempo de ejecución, no hay una forma general de determinar si, antes del borrado de tipo, era an ArrayList<Integer>o an ArrayList<Float>. Muchas personas no están satisfechas con esta restricción. [14] Hay enfoques parciales. Por ejemplo, se pueden examinar elementos individuales para determinar el tipo al que pertenecen; por ejemplo, si an ArrayListcontiene an Integer, esa ArrayList puede haber sido parametrizada con Integer(sin embargo, puede haber sido parametrizada con cualquier padre de Integer, como Numbero Object).

Para demostrar este punto, el siguiente código genera "Igual":

Lista final < Entero > li = new ArrayList <> (); Lista final < Flotante > lf = new ArrayList <> (); si ( li . getClass () == lf . getClass ()) { // se evalúa como verdadero System . out . println ( "Igual" ); }                

Otro efecto del borrado de tipos es que una clase genérica no puede extender la Throwableclase de ninguna manera, directa o indirectamente: [15]

clase pública GenericException < T > extiende Exception    

La razón por la que esto no se admite se debe al borrado de tipos:

intentar { lanzar nueva GenericException < Integer > (); } atrapar ( GenericException < Integer > e ) { System . err . println ( "Integer" ); } atrapar ( GenericException < String > e ) { System . err . println ( "String" ); }            

Debido al borrado de tipos, el entorno de ejecución no sabrá qué bloque de captura ejecutar, por lo que el compilador lo prohíbe.

Los genéricos de Java difieren de las plantillas de C++ . Los genéricos de Java generan solo una versión compilada de una clase o función genérica, independientemente de la cantidad de tipos de parametrización utilizados. Además, el entorno de ejecución de Java no necesita saber qué tipo parametrizado se utiliza porque la información de tipo se valida en tiempo de compilación y no se incluye en el código compilado. En consecuencia, la creación de una instancia de una clase Java de un tipo parametrizado es imposible porque la creación de instancias requiere una llamada a un constructor, que no está disponible si el tipo es desconocido.

Por ejemplo, el siguiente código no se puede compilar:

< T > T instantiateElementType ( List < T > arg ) { return new T (); //provoca un error de compilación }        

Como solo hay una copia por clase genérica en tiempo de ejecución, las variables estáticas se comparten entre todas las instancias de la clase, independientemente de su parámetro de tipo. En consecuencia, el parámetro de tipo no se puede utilizar en la declaración de variables estáticas ni en métodos estáticos.

El borrado de tipos se implementó en Java para mantener la compatibilidad con programas escritos antes de Java SE5. [7]

Diferencias con las matrices

Existen varias diferencias importantes entre las matrices (tanto matrices primitivas como Objectmatrices) y los genéricos en Java. Dos de las principales diferencias son las diferencias en términos de varianza y cosificación .

Covarianza, contravarianza e invarianza

Los genéricos son invariantes, mientras que las matrices son covariantes . [6] Este es un beneficio de usar genéricos en comparación con objetos no genéricos como las matrices. [6] Específicamente, los genéricos pueden ayudar a prevenir excepciones en tiempo de ejecución al lanzar una excepción en tiempo de compilación para forzar al desarrollador a corregir el código.

Por ejemplo, si un desarrollador declara un Object[]objeto y crea una instancia del objeto como un nuevo Long[]objeto, no se lanza ninguna excepción en tiempo de compilación (ya que las matrices son covariantes). [6] Esto puede dar la falsa impresión de que el código está escrito correctamente. Sin embargo, si el desarrollador intenta agregar un Stringa este Long[]objeto, el programa lanzará un ArrayStoreException. [6] Esta excepción en tiempo de ejecución se puede evitar por completo si el desarrollador usa genéricos.

Si el desarrollador declara un Collection<Object>objeto y crea una nueva instancia de este objeto con el tipo de retorno ArrayList<Long>, el compilador de Java arrojará (correctamente) una excepción en tiempo de compilación para indicar la presencia de tipos incompatibles (ya que los genéricos son invariantes). [6] Por lo tanto, esto evita posibles excepciones en tiempo de ejecución. Este problema se puede solucionar creando una instancia de Collection<Object>using ArrayList<Object>object en su lugar. Para el código que utiliza Java SE7 o versiones posteriores, se Collection<Object>puede crear una instancia de con un ArrayList<>objeto utilizando el operador de diamante

Cosificación

Las matrices se reifican , lo que significa que un objeto de matriz aplica su información de tipo en tiempo de ejecución, mientras que los genéricos en Java no se reifican. [6]

Hablando de manera más formal, los objetos con tipo genérico en Java son tipos no confiables. [6] Un tipo no confiable es un tipo cuya representación en tiempo de ejecución tiene menos información que su representación en tiempo de compilación. [6]

Los objetos con tipo genérico en Java no se pueden verificar debido al borrado de tipo. [6] Java solo aplica la información de tipo en tiempo de compilación. Una vez que se verifica la información de tipo en tiempo de compilación, se descarta y, en tiempo de ejecución, la información de tipo no estará disponible. [6]

Los ejemplos de tipos no verificables incluyen List<T>y List<String>, donde Tes un parámetro formal genérico. [6]

Proyecto sobre genéricos

El proyecto Valhalla es un proyecto experimental para incubar funciones genéricas y del lenguaje Java mejoradas, para versiones futuras posiblemente a partir de Java 10 en adelante. Las posibles mejoras incluyen: [16]

Véase también

Citas

  1. ^ Lenguaje de programación Java
  2. ^ Se puede lanzar una ClassCastException incluso en ausencia de conversiones o valores nulos. "Los sistemas de tipos de Java y Scala no son sólidos" (PDF) .
  3. ^ Bloch 2018, págs. 123–125, Capítulo §5, Punto 27: Eliminar las advertencias no marcadas.
  4. ^ GJ: Java genérico
  5. ^ Especificación del lenguaje Java, tercera edición, de James Gosling, Bill Joy, Guy Steele y Gilad Bracha – Prentice Hall PTR 2005
  6. ^ abcdefghijklmno Bloch 2018, págs. 126–129, Capítulo §5, ítem 28: Preferir listas a matrices.
  7. ^ abcdefgh Bloch 2018, pp. 117–122, Capítulo §5, Elemento 26: No utilice tipos sin procesar.
  8. ^ Bloch 2018, pp. 135–138, Capítulo §5, ítem 30: Favorecer métodos genéricos.
  9. ^ Gilad Bracha (5 de julio de 2004). "Elementos genéricos en el lenguaje de programación Java" (PDF) . www.oracle.com .
  10. ^ "Inferencia de tipos para la creación de instancias genéricas".
  11. ^ Gilad Bracha (5 de julio de 2004). "Generics in the Java Programming Language" (PDF) . www.oracle.com . p. 5.
  12. ^ abcd Bloch 2018, págs. 139–145, Capítulo §5, Elemento 31: Utilice comodines delimitados para aumentar la flexibilidad de la API.
  13. ^ Bracha, Gilad . "Comodines > Bonificación > Genéricos". Tutoriales de Java™ . Oracle. ...La única excepción es null, que es miembro de cada tipo...
  14. ^ Gafter, Neal (5 de noviembre de 2006). "Genéricos reificados para Java" . Consultado el 20 de abril de 2010 .
  15. ^ "Especificación del lenguaje Java, sección 8.1.2". Oracle . Consultado el 24 de octubre de 2015 .
  16. ^ Goetz, Brian. "¡Bienvenidos al Valhalla!". Archivo de correo OpenJDK . OpenJDK . Consultado el 12 de agosto de 2014 .

Referencias

  • Bloch, Joshua (2018). "Effective Java: Programming Language Guide" (tercera edición). Addison-Wesley. ISBN 978-0134685991.
Obtenido de "https://es.wikipedia.org/w/index.php?title=Fármacos_genéricos_en_Java&oldid=1231696469"