Articulo de referencia

Genéricos en Java

Los genéricos son una funcionalidad de la programación genérica que se incorporó al lenguaje de programación Java en 2004 con la versión J2SE 5.0. Se diseñaron para extender el ...

Los genéricos son una funcionalidad de la programación genérica que se incorporó al lenguaje de programación Java en 2004 con la versión J2SE 5.0. Se diseñaron para extender el sistema de tipos de Java y permitir que "un tipo o método opere sobre objetos de diversos tipos, a la vez que se garantiza la seguridad de tipos en tiempo de compilación". [ 1 ] El aspecto de la seguridad de tipos en tiempo de compilación requería que las funciones paramétricamente polimórficas no se implementaran en la máquina virtual de 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 admitir 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 clases genéricas, declaraciones de interfaces genéricas, declaraciones de métodos genéricos y declaraciones de constructores genéricos. Estas suelen ser una sola letra mayúscula, pero las que tienen más de una letra se escriben normalmente en MAYÚSCULAS (a diferencia de otros lenguajes como C++ , C# y Rust , donde se usa PascalCase ), por ejemplo . [ 6 ]java.util.PrimitiveIterator<T, T_CONS>
  • Una clase 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. [ 8 ] 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. [ 8 ] Define una o más variables de tipo que actúan como parámetros. [ 8 ] 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. [ 9 ] Estas variables de tipo se conocen como 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 es genérica. Un constructor es genérico si declara una o más variables de tipo. Estas variables de tipo se conocen como parámetros de tipo formales del constructor. La lista de parámetros de tipo formales tiene la misma estructura que la 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 ArrayListde tipo Object. Luego, agrega un java.lang.Stringal java.util.ArrayList. Finalmente, intenta recuperar el agregado java.lang.Stringy convertirlo a un java.lang.Integer—un error de lógica, ya que es imposible convertir cualquier instancia de cadena a un entero .

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

Esto es similar a C , que carece de genéricos, y sus estructuras de datos tipo colección normalmente almacenan datos como void*(un puntero void ), desde el cual los objetos deben convertirse explícitamente.

Aunque el código se compila sin errores, lanza una excepción en tiempo de ejecución ( java.lang.ClassCastException) al ejecutar la tercera línea de código. Este tipo de error lógico puede detectarse durante el tiempo de compilación mediante el uso de genéricos [ 8 ] y es la principal motivación para usarlos. [ 7 ] 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:

import java.util.ArrayList ; import java.util.List ;List < String > v = new ArrayList < String > (); v . add ( "test" ); Integer i = ( Integer ) v . get ( 0 ); // (error de tipo) error de compilación

El parámetro de tipo dentro de los corchetesjava.lang.String angulares declara que está constituido por (un descendiente de los constituyentes genéricos de ). Con los genéricos, ya no es necesario convertir la tercera línea a ningún tipo en particular, porque el resultado de se define como por el código generado por el compilador .java.util.ArrayListjava.lang.Stringjava.util.ArrayListjava.lang.Objectv.get(0)java.lang.String

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

Aquí hay un pequeño extracto de la definición de las interfaces java.util.Listy java.util.Iteratoren el paquete java.util:

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

Definiciones de clases genéricas

Aquí hay 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 < K , V > { clave privada K ; valor privado V ;public Entry ( K clave , V valor ) { this . clave = clave ; this . valor = valor ; }public K getKey () { return key ; }public V getValue () { return valor ; }public String toString () { return String . format ( "(%s, %s)" , key , value ); } }

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

Entrada < Cadena , Cadena > calificación = nueva Entrada < Cadena , Cadena > ( "Mike" , "A" ); Entrada < Cadena , Entero > calificación = nueva Entrada < Cadena , Entero > ( "Mike" , 100 ); Sistema . salida . printf ( "calificación: %s%n" , calificación ); Sistema . salida . printf ( "calificación: %s%n" , calificación );Entry < Integer , Boolean > prime = new Entry <> ( 13 , true ); if ( prime . getValue ()) { System . out . println ( "%s es primo.%n" , prime . getKey ()); } else { System . out . println ( "%s no es primo.%n" , prime . getKey ()); }

El resultado es:

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

Definiciones de métodos genéricos

Aquí hay un ejemplo de un método genérico que utiliza la clase genérica anterior, para un tipo genérico T:

public static < T > Entry < T , T > twice ( T value ) { return new Entry < T , T > ( value , value ); }

Nota: Si eliminamos el primero <T>en el método anterior, obtendremos un error de compilación (no se puede encontrar el símbolo " T"), 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 estos se pueden inferir:

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

Los parámetros pueden añadirse explícitamente si es necesario:

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

No se permite el uso de tipos primitivos , y en su lugar se deben usar versiones encapsuladas ; solo se pueden almacenar tipos de referencia en un parámetro de tipo genérico. Por ejemplo, falla la compilación y en su lugar se deben usar las clases envoltorio para primitivos (como .Entry<int,int>pair;Entry<Integer,Integer>pair;

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

public < T > T [] toArray ( T ... elements ) { return elements ; }

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

Integer [] array = toArray ( 1 , 2 , 3 , 4 , 5 , 6 );

Si bien los tipos primitivos no se pueden almacenar, los arreglos de tipos primitivos (ya que son objetos, no tipos primitivos en sí mismos) sí se pueden:

import java.util.ArrayList ; import java.util.List ;// List<int[]> y List<Integer[]> son igualmente aceptables List < int []> listOfArrays = new ArrayList <> (); List < Integer []> myOtherList = listOfArrays ;

operador de diamantes

Utilizando la inferencia de tipos , Java SE 7 y versiones posteriores permiten al programador sustituir un par de corchetes angulares vacíos ( <>, llamado el " operador diamante ") por un par de corchetes angulares que contienen uno o más parámetros de tipo que implica un contexto suficientemente cercano . [ 11 ] Si bien <>a menudo se le llama el "operador diamante", no es un operador, sino simplemente una lista vacía de parámetros de tipo.

Por lo tanto, el ejemplo de código anterior Entryse puede reescribir como:

Entrada < Cadena , Cadena > calificación = nueva Entrada <> ( "Mike" , "A" ); Entrada < Cadena , Entero > calificación = nueva Entrada <> ( "Mike" , 100 );System.out.printf ( " calificación : % s%n" , calificación); System.out.printf ( " nota : % s % n " , nota ) ;Entry < Integer , Boolean > prime = new Entry <> ( 13 , true ); if ( prime . getValue ()) { System . out . println ( "%s es primo.%n" , prime . getKey ()); } else { System . out . println ( "%s no es primo.%n" , prime . getKey ()); }

El denominado operador diamante no existe en otros lenguajes derivados de Java, como C# o Kotlin , que tienen sus propios métodos para evitar información de tipo redundante. Si bien existe algo visualmente similar en C++ , no indica la deducción de parámetros de tipo, sino que simplemente se refiere al parámetro de tipo predeterminado.

plantilla < typename T = int > clase Box { // ... };Box <> v ; // se refiere a Box<int>

Tipo comodines

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

Aquí hay un ejemplo donde el tipo de elemento de a java.util.Collection<E>se parametriza mediante un comodín:

import java.util.ArrayList ; import java.util.Collection ;Colección <?> c = new ArrayList < String > (); c . add ( new Object ()); // error de compilación c . add ( null ); // permitido

Como no sabemos qué crepresenta el tipo de elemento, 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 algún tipo desconocido. Cualquier valor de argumento de método que le pasemos add()tendría que ser un subtipo de este tipo desconocido. Como no sabemos qué tipo es, no podemos pasarle nada. La única excepción es null ; que es un miembro de todos los tipos. [ 12 ]

Para especificar el límite superior de un comodín de tipo, se utiliza la palabra clave para indicar que el argumento de tipo es un subtipo de la clase delimitadora. [ 13 ] Por lo tanto , significa que la lista dada contiene objetos de algún tipo desconocido que extiende la clase. Por ejemplo, la lista podría ser o . Leer un elemento de la lista devolverá un . Agregar elementos nulos también está permitido. [ 14 ]extendsList<?extendsNumber>NumberList<Float>List<Number>Number

El uso de comodines arriba agrega flexibilidad [ 13 ] ya que no existe ninguna relación de herencia entre dos tipos parametrizados cualesquiera con un tipo concreto como argumento de tipo. Ni List<Number>ni List<Integer>es un subtipo del otro; aunque java.lang.Integeres un subtipo de java.lang.Number. [ 13 ] 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 java.lang.Numberque no sea un java.lang.Integeren él; lo cual 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>:

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

La solución con comodines funciona porque impide operaciones que violarían la seguridad de tipos:

import java.util.List ;List <? extends Number > nums = ints ; // OK nums.add ( 3.14 ) ; // error 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 . Leer de una lista definida como devuelve elementos de tipo . Agregar a dicha lista requiere elementos de tipo , cualquier subtipo de o null (que es un miembro de cada tipo).List<?superNumber>List<Number>List<Object>List<?superNumber>ObjectNumberNumber

La regla mnemotécnica PECS (Producer Extends, Consumer Super) del libro Effective Java de Joshua Bloch ofrece una manera sencilla de recordar cuándo usar comodines (correspondientes a covarianza y contravarianza ) en Java. [ 13 ]

Los genéricos en la cláusula throws

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

public < T extends Throwable > void throwMeConditional ( boolean conditional , T exception ) throws T { if ( conditional ) { throw exception ; } }

En este caso, es posible ya que el parámetro de tipo está limitado inferiormente por java.lang.Throwable, lo que garantiza que Tpuede aparecer en la throwscláusula.

Problemas con el borrado de tipos

Los tipos genéricos se comprueban en tiempo de compilación para verificar su corrección de tipo. [ 8 ] La información del tipo genérico se elimina mediante un proceso llamado borrado de tipo . [ 7 ] Por ejemplo, se convertirá al tipo no genérico , que normalmente contiene objetos arbitrarios. La comprobación en tiempo de compilación garantiza que el código resultante utilice el tipo correcto. [ 8 ]List<Integer>List

Debido al borrado de tipos, los parámetros de tipo no se pueden determinar en tiempo de ejecución. [ 7 ] Por ejemplo, cuando ArrayListse examina un en tiempo de ejecución, no hay una forma general de determinar si, antes del borrado de tipos, era un ArrayList<Integer>o un ArrayList<Float>. Muchas personas están insatisfechas con esta restricción. [ 15 ] Hay enfoques parciales. Por ejemplo, se pueden examinar elementos individuales para determinar el tipo al que pertenecen; por ejemplo, si un java.util.ArrayList<E>contiene un java.lang.Integer, que java.util.ArrayList<E>puede haber sido parametrizado con Integer(sin embargo, puede haber sido parametrizado con cualquier padre de java.lang.Integer, como java.lang.Numbero java.lang.Object).

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

import java.util.ArrayList ; import java.util.List ;List <Integer> li = new ArrayList <> ( ) ; List <Float> lf = new ArrayList <> ( ); if ( 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 java.lang.Throwableclase de ninguna manera, directa o indirectamente: [ 16 ]

// provoca un error de compilación public class GenericException < T > extends Exception { // ... }

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

try { throw new GenericException < Integer > (); } catch ( GenericException < Integer > e ) { System . err . println ( "Integer" ); } catch ( GenericException < String > e ) { System . err . println ( "String" ); }

Debido al borrado de tipos, el entorno de ejecución no sabrá qué bloque catch ejecutar, por lo que esto está prohibido por el compilador.

Los genéricos de Java difieren de las plantillas de C++ . Los genéricos de Java generan una única versión compilada de una clase o función genérica, independientemente del número de tipos de parámetros utilizados. Además, el entorno de ejecución de Java no necesita saber qué tipo de parámetro se utiliza, ya que la información de tipo se valida en tiempo de compilación y no se incluye en el código compilado. Por consiguiente, instanciar una clase Java de un tipo de parámetro es imposible, puesto que la instanciación requiere una llamada a un constructor, que no está disponible si se desconoce el tipo.

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

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

Dado que solo existe 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. Por consiguiente, el parámetro de tipo no puede utilizarse 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 versiones anteriores de programas escritos antes de Java SE5. [ 8 ]

Diferencias con los arreglos

Existen varias diferencias importantes entre los arrays (tanto arrays primitivos como java.lang.Objectarrays) y los genéricos en Java. Dos de las principales diferencias son, a saber, las diferencias en términos de varianza y reificación .

Covarianza, contravarianza e invarianza

Los genéricos son invariantes, mientras que los arreglos son covariantes . [ 7 ] Esta es una ventaja de usar genéricos en comparación con objetos no genéricos como los arreglos. [ 7 ] 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 obligar al desarrollador a corregir el código.

Por ejemplo, si un desarrollador declara un java.lang.Object[]objeto y lo instancia como un nuevo java.lang.Long[]objeto, no se genera ninguna excepción en tiempo de compilación (ya que los arreglos son covariantes). [ 7 ] Esto puede dar la falsa impresión de que el código está escrito correctamente. Sin embargo, si el desarrollador intenta agregar un java.lang.Stringa este java.lang.Long[]objeto, el programa generará una excepción java.lang.ArrayStoreException. [ 7 ] Esta excepción en tiempo de ejecución puede evitarse por completo si el desarrollador utiliza genéricos.

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

Cosificación

Los arrays se reifican , lo que significa que un objeto array impone su información de tipo en tiempo de ejecución, mientras que los genéricos en Java no se reifican. [ 7 ] Esto se debe a que los arrays son clases reales generadas por el compilador en la JVM; para cada tipo T, existe un tipo T[].

En términos más formales, los objetos con tipo genérico en Java son tipos no reificables. [ 7 ] Un tipo no reificable es un tipo cuya representación en tiempo de ejecución tiene menos información que su representación en tiempo de compilación. [ 7 ]

Los objetos con tipo genérico en Java no son reificables debido al borrado de tipos. [ 7 ] Java solo aplica la información de tipo en tiempo de compilación. Después de que la información de tipo se verifica en tiempo de compilación, se descarta y, en tiempo de ejecución, no estará disponible. [ 7 ]

Ejemplos de tipos no reificables incluyen List<T>y List<String>, donde Tes un parámetro formal genérico. [ 7 ]

Proyecto sobre medicamentos genéricos

El proyecto Valhalla es un proyecto experimental para incubar mejoras en los genéricos y características del lenguaje Java, para futuras versiones potencialmente a partir de Java 10. Las posibles mejoras incluyen: [ 17 ]

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 son inconsistentes" (PDF) .
  3. Bloch 2018 , págs. 123–125, Capítulo §5 Punto 27: Eliminar las advertencias no verificadas.
  4. GJ: Java genérico
  5. Especificación del lenguaje Java, tercera edición, por James Gosling, Bill Joy, Guy Steele y Gilad Bracha – Prentice Hall PTR 2005
  6. Oracle Corporation (6 de junio de 2026). "Interface PrimitiveIterator<T,T_CONS>" . docs.oracle.com . Oracle Corporation.
  7. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Bloch 2018 , págs. 126–129, Capítulo §5 Elemento 28: Preferir listas a matrices.
  8. 1 2 3 4 5 6 7 8 Bloch 2018 , págs. 117–122, Capítulo §5 Punto 26: No utilice tipos sin procesar.
  9. Bloch 2018 , págs. 135–138, Capítulo §5 Punto 30: Favorecer los métodos genéricos.
  10. Gilad Bracha (5 de julio de 2004). "Genéricos en el lenguaje de programación Java" (PDF) . www.oracle.com .
  11. "Inferencia de tipos para la creación de instancias genéricas" .
  12. Gilad Bracha (5 de julio de 2004). "Genéricos en el lenguaje de programación Java" (PDF) . www.oracle.com . pág. 5. 
  13. 1 2 3 4 Bloch 2018 , págs. 139–145, Capítulo §5 Elemento 31: Utilice comodines limitados para aumentar la flexibilidad de la API.
  14. Bracha, Gilad . "Wildcards > Bonus > Generics" . The Java™ Tutorials . Oracle. ...La única excepción es null, que es un miembro de todos los tipos...
  15. Gafter, Neal (2006-11-05). "Genéricos reificados para Java" . Recuperado el 2010-04-20 .
  16. "Especificación del lenguaje Java, Sección 8.1.2" . Oracle . Consultado el 24 de octubre de 2015 .
  17. Goetz, Brian. "¡Bienvenido a Valhalla!" . Archivo de correo de OpenJDK . OpenJDK . Consultado el 12 de agosto de 2014 .

Referencias

  • Bloch, Joshua (2018). «Java eficaz: Guía del lenguaje de programación» (tercera  ed.). Addison-Wesley. ISBN 978-0134685991.