Los genéricos son una facilidad de programación genérica que se agregaron al lenguaje de programación Java en 2004 dentro de la versión J2SE 5.0. Fueron diseñados para ampliar 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 no se logró completamente, ya que se demostró en 2016 que no está garantizado en todos los casos. [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] Java genérico se incorporó a Java con la adición de comodines .
Según la especificación del lenguaje Java : [5]
El siguiente bloque de código Java ilustra un problema que existe cuando no se utilizan genéricos. Primero, declara un ArrayList
tipo Object
. Luego, agrega un String
al ArrayList
. Finalmente, intenta recuperar el agregado String
y convertirlo en un Integer
error de lógica, ya que generalmente no es posible convertir una cadena arbitraria en un número entero.
Lista final v = nueva ArrayList (); v . agregar ( "prueba" ); // Una cadena que no se puede convertir a un entero final Integer i = ( Integer ) v . obtener ( 0 ); // Error de tiempo de ejecución
Aunque el código se compila sin errores, genera 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 motivación principal 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 usando genéricos de la siguiente manera:
Lista final <Cadena> v = nueva ArrayList <Cadena> ( ) ; v . agregar ( "prueba" ); Entero final i = ( Entero ) v . obtener ( 0 ); // (error de tipo) error en tiempo de compilación
El parámetro de tipo String
entre corchetes angulares declara que ArrayList
está constituido por String
(un descendiente de los constituyentes ArrayList
gené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)
está definido String
por el código generado por el compilador.
La falla lógica en la tercera línea de este fragmento se detectará como un error en tiempo de compilación (con J2SE 5.0 o posterior) porque el compilador detectará que v.get(0)
devuelve String
en lugar de Integer
. [7] Para un ejemplo más elaborado, ver referencia. [9]
Aquí hay un pequeño extracto de la definición de las interfaces java.util.List
y java.util.Iterator
del paquete java.util
:
Lista de interfaz <E> { anular agregar ( E x ); Iterador <E> iterador ( ) ; } iterador de interfaz <E> { mi siguiente (); booleano hasSiguiente (); }
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 :
Entrada de clase pública < Tipo de clave , Tipo de valor > { clave de tipo de clave final privada ; valor ValueType final privado ; Entrada pública ( clave KeyType , valor ValueType ) { this . clave = clave ; este . valor = valor ; } tipo de clave pública getKey () { clave de retorno ; } public ValueType getValue () { valor de retorno ; } public String toString () { return "(" + clave + ", " + valor + ")" ; } }
Esta clase genérica podría usarse de las siguientes maneras, por ejemplo:
Entrada final < Cadena , Cadena > calificación = nueva Entrada < Cadena , Cadena > ( "Mike" , "A" ); Entrada final < Cadena , Entero > marca = nueva Entrada < Cadena , Entero > ( "Mike" , 100 ); Sistema . afuera . println ( "calificación: " + calificación ); Sistema . afuera . println ( "marca: " + marca ); Entrada final < Entero , Booleano > principal = nueva Entrada < Entero , Booleano > ( 13 , verdadero ); if ( prime . getValue ()) { Sistema . afuera . println ( prime.getKey ( ) + "es primo." ) ; } más { Sistema . afuera . println ( prime.getKey ( ) + "no es primo." ) ; }
Produce:
grado: (Mike, A)marca: (Mike, 100)13 es primo.
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 nueva 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 < Cadena , Cadena > par = Entrada . dos veces ( "Hola" );
Los parámetros se pueden agregar explícitamente si es necesario:
Entrada final < Cadena , Cadena > par = Entrada . <Cadena> 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 ; // Falla la compilación. Utilice Entero 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 ) { elementos de retorno ; }
En tales casos tampoco puedes usar tipos primitivos, por ejemplo:
Entero [] matriz = toArray ( 1 , 2 , 3 , 4 , 5 , 6 );
Gracias a la inferencia de tipos , Java SE 7 y superiores 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 Entry
puede reescribirse como:
Entrada final < Cadena , Cadena > calificación = nueva Entrada <> ( "Mike" , "A" ); Entrada final < Cadena , Entero > marca = nueva Entrada <> ( "Mike" , 100 ); Sistema . afuera . println ( "calificación: " + calificación ); Sistema . afuera . println ( "marca: " + marca ); Entrada final < Entero , Booleano > principal = nueva Entrada <> ( 13 , verdadero ); if ( prime . getValue ()) Sistema . afuera . println ( prime.getKey ( ) + "es primo." ) ; más Sistema . afuera . println ( prime . getKey () + "no es primo." );
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" para que sirvan 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 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 = nueva ArrayList < String > (); C . agregar ( nuevo objeto ()); // error en tiempo de compilación c . agregar ( nulo ); // permitido
Como no sabemos qué c
significa 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 al add()
método tendría que ser un subtipo de este tipo desconocido. Como no sabemos de qué tipo es, no podemos pasar nada. La única excepción es nula ; que es miembro de cada tipo. [11]
Para especificar el límite superior de un comodín de tipo, extends
se utiliza la palabra clave para indicar que el argumento de tipo es un subtipo de la clase delimitadora. [12] Entonces 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 archivo . Nuevamente, también se permite agregar elementos nulos. [13]List<? extends Number>
Number
List<Float>
List<Number>
Number
El uso de comodines arriba 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 List<Number>
ni List<Integer>
es un subtipo del otro; aunque Integer
es un subtipo de Number
. [12] Entonces, cualquier método que tome List<Number>
como parámetro no acepta un argumento de List<Integer>
. Si así fuera, sería posible insertar un Number
que no sea un Integer
en él; lo que viola la seguridad de tipo. A continuación se muestra un ejemplo que demuestra cómo se violaría la seguridad de tipos si List<Integer>
fuera un subtipo de List<Number>
:
Lista final <Entero> ints = nueva ArrayList <> ( ) ; enteros . agregar ( 2 ); Lista final < Número > nums = ints ; // válido si List<Integer> fuera un subtipo de List<Number> según la regla de sustitución. números . añadir ( 3.14 ); Entero final x = enteros . obtener ( 1 ); // ¡ahora 3.14 está asignado a una variable entera!
La solución con comodines funciona porque no permite operaciones que violarían la seguridad de tipos:
Lista final <? extiende Número > nums = ints ; // OK números . añadir ( 3.14 ); // números de error en tiempo de compilación . agregar ( nulo ); // permitido
Para especificar la clase de límite inferior de un tipo comodín, super
se utiliza la palabra clave. Esta palabra clave indica que el argumento de tipo es un supertipo de la clase delimitadora. Entonces, podría representar o . La lectura de una lista definida como devuelve elementos de tipo . Agregar a dicha lista requiere elementos de tipo , cualquier subtipo de o nulo (que es miembro de cada tipo).List<? super Number>
List<Number>
List<Object>
List<? super Number>
Object
Number
Number
El mnemotécnico PECS (Producer Extends, Consumer Super) del libro Effective Java de Joshua Bloch ofrece una manera fácil de recordar cuándo usar comodines (correspondientes a covarianza y contravarianza ) en Java. [12]
Aunque las excepciones en sí no pueden ser genéricas, los parámetros genéricos pueden aparecer en una cláusula throws:
public < T extiende Throwable > void throwMeConditional ( condicional booleano , excepción T ) lanza T { if ( condicional ) { throw excepción ; } }
Los genéricos se verifican en tiempo de compilación para verificar la corrección de tipos. [7] La información de tipo genérico luego se elimina en un proceso llamado borrado de tipo . [6] Por ejemplo, List<Integer>
se convertirá al tipo no genérico List
, que normalmente contiene objetos arbitrarios. La verificación en tiempo de compilación garantiza que el código resultante utilice el tipo correcto. [7]
Debido al borrado de tipos, los parámetros de tipo no se pueden determinar en tiempo de ejecución. [6] Por ejemplo, cuando se examina un ArrayList
en tiempo de ejecución, no existe una forma general de determinar si, antes de borrar el tipo, era un ArrayList<Integer>
o un ArrayList<Float>
. Mucha gente no está satisfecha 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 ArrayList
contiene un Integer
, es posible que ArrayList se haya parametrizado con Integer
(sin embargo, se puede haber parametrizado con cualquier padre de Integer
, como Number
o Object
).
Para demostrar este punto, el siguiente código genera "Igual":
Lista final <Entero> li = nueva ArrayList <> ( ) ; Lista final <flotante> lf = nueva ArrayList <> ( ) ; if ( li . getClass () == lf . getClass ()) { // se evalúa como verdadero System . afuera . println ( "Igual" ); }
Otro efecto del borrado de tipo es que una clase genérica no puede extender la Throwable
clase de ninguna manera, directa o indirectamente: [15]
clase pública GenericException <T> extiende la excepción
La razón por la que esto no es compatible se debe al borrado de tipos:
intente { lanzar una nueva excepción genérica <entero> () ; } catch ( GenericException <Integer> e ) { System . errar . println ( "Entero" ); } catch ( GenericException <String> e ) { System . errar . println ( "Cadena" ); }
Debido al borrado de tipos, el tiempo de ejecución no sabrá qué bloque catch 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 del número 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 del tipo se valida en tiempo de compilación y no se incluye en el código compilado. En consecuencia, crear 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 instanciateElementType ( Lista < T > arg ) { return new T (); //provoca un error de compilación }
Debido a que 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]
Existen varias diferencias importantes entre las matrices (tanto matrices primitivas como Object
matrices) y los genéricos en Java. Dos de las principales diferencias, a saber, diferencias en términos de varianza y cosificación .
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 matrices. [6] Específicamente, los genéricos pueden ayudar a prevenir excepciones en tiempo de ejecución al generar una excepción en tiempo de compilación para obligar al desarrollador a corregir el código.
Por ejemplo, si un desarrollador declara un Object[]
objeto y crea una instancia del objeto como un Long[]
objeto nuevo, no se genera 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 String
a este Long[]
objeto, el programa generará un archivo ArrayStoreException
. [6] Esta excepción de tiempo de ejecución se puede evitar 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 generará (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>
uso ArrayList<Object>
de objeto. Para el código que usa Java SE7 o versiones posteriores, se Collection<Object>
puede crear una instancia con un ArrayList<>
objeto usando el operador de diamante.
Las matrices están cosificadas , lo que significa que un objeto de matriz impone su información de tipo en tiempo de ejecución, mientras que los genéricos en Java no están cosificados. [6]
Hablando más formalmente, los objetos con tipo genérico en Java son tipos no verificables. [6] Un tipo no verificable 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 son verificables debido al borrado de tipo. [6] Java solo aplica información de tipo en tiempo de compilación. Después de verificar la información de tipo en tiempo de compilación, la información de tipo se descarta y, en tiempo de ejecución, la información de tipo no estará disponible. [6]
Ejemplos de tipos no verificables incluyen List<T>
y List<String>
, donde T
es un parámetro formal genérico. [6]
Project Valhalla es un proyecto experimental para incubar características de lenguaje y genéricos de Java mejorados, para versiones futuras potencialmente desde Java 10 en adelante. Las posibles mejoras incluyen: [16]
...La única excepción es nula, que es miembro de cada tipo...