Los genéricos son una función 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 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 seguridad del tipo en tiempo de compilación no se logró en su totalidad, ya que se demostró en 2016 que no está garantizado en todos los casos. [2]
El marco de las 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. [3] Java genérico se incorporó en Java con la adición de comodines.
Jerarquía y clasificación
Según la especificación del lenguaje Java : [4]
- 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érico y declaraciones de constructor genérico.
- Una clase es genérica si declara una o más variables de tipo. Estas variables de tipo se conocen como parámetros de tipo de la clase. Define una o más variables de tipo que actúan como parámetros. 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ámetro 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. Estas variables de tipo se conocen como parámetros de tipo de la interfaz. Define una o más variables de tipo que actúan como parámetros. Una declaración de interfaz genérica define un conjunto de tipos, uno para cada posible invocación de la sección de parámetro 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. Estas variables de tipo se conocen como parámetros de tipo formal del método. La forma de la lista de parámetros de tipo formal es idéntica a 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 en sí misma. 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 forma de la lista de parámetros de tipo formal es idéntica a 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 ArrayList
tipo de Object
. Luego, agrega un String
al ArrayList
. Finalmente, intenta recuperar el agregado String
y convertirlo en un Integer
—un error de lógica, ya que generalmente no es posible convertir una cadena arbitraria a un entero.
Lista v = new ArrayList (); v . agregar ( "prueba" ); // Una cadena que no se puede convertir en un entero Integer i = ( Integer ) v . obtener ( 0 ); // Error de tiempo de ejecución
Aunque el código se compila sin errores, lanza 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 y es la principal motivación para usarlos.
El fragmento de código anterior se puede reescribir usando genéricos de la siguiente manera:
List < String > v = new ArrayList < String > (); v . agregar ( "prueba" ); Entero i = ( Entero ) v . obtener ( 0 ); // (error de tipo) error en tiempo de compilación
El parámetro de tipo String
dentro de los corchetes angulares declara ArrayList
que está constituido por String
(un descendiente de los constituyentes ArrayList
genéricos de ' Object
). Con 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)
retorna en String
lugar de Integer
. Para obtener un ejemplo más elaborado, consulte la referencia. [5]
Aquí hay un pequeño extracto de la definición de las interfaces List
y Iterator
en el paquete java.util
:
Interfaz pública List < E > { void add ( E x ); Iterador < E > iterador (); } Iterador de interfaz pública < E > { E next (); booleano hasNext (); }
Escriba 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 para que sirvan como argumentos de tipo para tipos parametrizados. Los comodines son argumentos de tipo con el formato " >
"; opcionalmente con un superior o inferior unido . 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
se parametriza mediante un comodín:
Colección > C = new ArrayList < String > (); c . agregar ( nuevo objeto ()); // error en tiempo de compilación c . agregar ( nulo ); // permitido
Como no sabemos qué representa el tipo de elemento de c
, no podemos agregarle objetos. El add()
método toma argumentos de tipo E
, el tipo de elemento de la Collection
interfaz genérica. Cuando el argumento de tipo real es ?
, representa algún 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 nula ; que es un miembro de todo tipo. [6]
Para especificar el límite superior de un comodín de tipo, la extends
palabra clave se utiliza para indicar que el argumento de tipo es un subtipo de la clase delimitadora. Entonces List extends Number>
significa que la lista dada contiene objetos de algún tipo desconocido que extiende la Number
clase. Por ejemplo, la lista podría ser List
o List
. La lectura de un elemento de la lista devolverá un Number
. También se permite agregar elementos nulos. [7]
El uso de comodines anteriores agrega flexibilidad, ya que no existe ninguna relación de herencia entre dos tipos parametrizados con el tipo concreto como argumento de tipo. Ni List
tampoco List
es un subtipo del otro; aunque Integer
es un subtipo de Number
. Entonces, cualquier método que tome List
como parámetro no acepta un argumento de List
. Si lo hiciera, sería posible insertar un Number
que no es un Integer
en él; que viola la seguridad de tipos. Aquí hay un ejemplo que demuestra cómo se violaría la seguridad de tipos si List
fuera un subtipo de List
:
List < Integer > ints = new ArrayList < Integer > (); ints . agregar ( 2 ); Lista < Número > nums = ints ; // válido si List fuera un subtipo de List según la regla de sustitución. nums . añadir ( 3,14 ); Entero x = pulgadas . obtener ( 1 ); // ¡Ahora 3.14 está asignado a una variable Integer!
La solución con comodines funciona porque no permite operaciones que violarían la seguridad de tipos:
Lista extiende Número > nums = ints ; // OK números . añadir ( 3,14 ); // error de tiempo de compilación nums . 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, List super Number>
podría representar List
o List
. La lectura de una lista definida como List super Number>
devuelve elementos de tipo Object
. Agregar a dicha lista requiere elementos de tipo Number
, cualquier subtipo Number
o nulo (que es un miembro de cada tipo).
El mnemónico 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.
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 :
Entrada de clase pública < KeyType , ValueType > { clave KeyType final privada ; valor de ValueType final privado ; Entrada pública ( clave KeyType , valor ValueType ) { this . clave = clave ; esto . valor = valor ; } public KeyType 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 formas, por ejemplo:
Entrada < Cadena , Cadena > grado = nueva Entrada < Cadena , Cadena > ( "Mike" , "A" ); Entrada < Cadena , Entero > marca = nueva Entrada < Cadena , Entero > ( "Mike" , 100 ); Sistema . fuera . println ( "grado:" + grado ); Sistema . fuera . println ( "marca:" + marca );Entrada < Integer , Boolean > prime = new Entry < Integer , Boolean > ( 13 , verdadero ); if ( prime . getValue ()) Sistema . fuera . println ( prime . getKey () + "es prime." ); else System . fuera . println ( prime . getKey () + "no es prime." );
Produce:
grado: (Mike, A)nota: (Mike, 100)13 es primo.
Operador de diamantes
Gracias a la inferencia de tipos , Java SE 7 y superior 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 . [8] Por lo tanto, el ejemplo de código anterior Entry
se puede reescribir como:
Entrada < Cadena , Cadena > grado = nueva Entrada <> ( "Mike" , "A" ); Entrada < Cadena , Entero > marca = nueva Entrada <> ( "Mike" , 100 ); Sistema . fuera . println ( "grado:" + grado ); Sistema . fuera . println ( "marca:" + marca );Entrada < Integer , Boolean > prime = new Entry <> ( 13 , verdadero ); if ( prime . getValue ()) Sistema . fuera . println ( prime . getKey () + "es prime." ); else System . fuera . println ( prime . getKey () + "no es prime." );
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 < Type > Entry < Type , Type > dos veces ( Type value ) { return new Entry < Type , Type > ( valor , valor ); }
Nota: Si eliminamos el primero
en el método anterior, obtendremos un error de compilación (no podemos 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 < Cadena , Cadena > par = Entrada . dos veces ( "Hola" );
Los parámetros se pueden agregar explícitamente si es necesario:
Entrada < Cadena , Cadena > 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 < int , int > par ; // Falla la compilación. Utilice Integer en su lugar.
También existe la posibilidad de crear métodos genéricos basados en parámetros dados.
public < Tipo > Tipo [] toArray ( Tipo ... elementos ) { elementos de retorno ; }
En tales casos, tampoco puede usar tipos primitivos, por ejemplo:
Entero [] matriz = toArray ( 1 , 2 , 3 , 4 , 5 , 6 );
Genéricos en 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:
public < T extiende Throwable > void throwMeConditional ( condicional booleano , excepción T ) lanza T { if ( condicional ) { excepción de lanzamiento ; } }
Problemas con el borrado de tipos
Los genéricos se comprueban en tiempo de compilación para verificar su corrección de tipo. Luego, la información de tipo genérico se elimina en un proceso llamado borrado de tipo . Por ejemplo, List
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 sea de tipo correcto.
Debido al borrado de tipo, los parámetros de tipo no se pueden determinar en tiempo de ejecución. Por ejemplo, cuando ArrayList
se examina un en tiempo de ejecución, no hay una forma general de determinar si, antes del borrado de tipo, era ArrayList
un ArrayList
. Mucha gente no está satisfecha con esta restricción. [9] Hay enfoques parciales. Por ejemplo, se pueden examinar elementos individuales para determinar el tipo al que pertenecen; por ejemplo, si un ArrayList
contiene un Integer
, ese ArrayList puede haber sido parametrizado Integer
(sin embargo, puede haber sido parametrizado con cualquier padre de Integer
, como Number
o Object
).
Demostrando este punto, el siguiente código genera "Equal":
ArrayList < Integer > li = new ArrayList < Integer > (); ArrayList < Float > lf = new ArrayList < Float > (); if ( li . getClass () == lf . getClass ()) { // se evalúa como verdadero System . fuera . println ( "Igual" ); }
Otro efecto del borrado de tipo es que una clase genérica no puede extender la clase Throwable de ninguna manera, directa o indirectamente: [10]
public class GenericException < T > extiende la excepción
La razón por la que esto no es compatible se debe al borrado de tipo:
intente { lanzar una nueva GenericException < Integer > (); } catch ( GenericException < Integer > e ) { System . err . println ( "Entero" ); } catch ( GenericException < String > e ) { System . err . println ( "Cadena" ); }
Debido al borrado de tipo, el motor 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, la instanciación de una clase Java de un tipo parametrizado es imposible porque la instanciación 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 }
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 o en métodos estáticos.
Proyecto sobre genéricos
Project Valhalla es un proyecto experimental para incubar características genéricas y de lenguaje mejoradas de Java, para versiones futuras, potencialmente a partir de Java 10 en adelante. Las posibles mejoras incluyen: [11]
- especialización genérica , por ejemplo, List
- genéricos reificados ; haciendo que los tipos reales estén disponibles en tiempo de ejecución.
Ver también
Referencias
- ^ Lenguaje de programación Java
- ^ Se puede lanzar una ClassCastException incluso en ausencia de conversiones o nulos. "Los sistemas de tipos de Java y Scala no son sólidos" (PDF) .
- ^ GJ: Java genérico
- ^ Especificación del lenguaje Java, tercera edición de James Gosling, Bill Joy, Guy Steele, Gilad Bracha - Prentice Hall PTR 2005
- ^ Gilad Bracha (5 de julio de 2004). "Genéricos en el lenguaje de programación Java" (PDF) . www.oracle.com .
- ^ Gilad Bracha (5 de julio de 2004). "Genéricos en el lenguaje de programación Java" (PDF) . www.oracle.com . pag. 5.
- ^ Bracha, Gilad . "Comodines> Bonificación> Genéricos" . Los tutoriales de Java ™ . Oráculo.
... La única excepción es nula, que es un miembro de todos los tipos ...
- ^ http://docs.oracle.com/javase/7/docs/technotes/guides/language/type-inference-generic-instance-creation.html
- ^ Gafter, Neal (5 de noviembre de 2006). "Genéricos reificados para Java" . Consultado el 20 de abril de 2010 .
- ^ "Especificación del lenguaje Java, sección 8.1.2" . Oracle . Consultado el 24 de octubre de 2015 .
- ^ Goetz, Brian. "¡Bienvenido a Valhalla!" . Archivo de correo OpenJDK . OpenJDK . Consultado el 12 de agosto de 2014 .