Java Generics es uno de los cambios más significativos en la historia del lenguaje Java. Los genéricos disponibles con Java 5 han hecho que usar Java Collection Framework sea más fácil, más conveniente y más seguro. Los errores asociados con el uso incorrecto de los tipos ahora se detectan en la etapa de compilación. Sí, y el lenguaje Java en sí se ha vuelto aún más seguro. A pesar de la aparente simplicidad de los tipos genéricos, muchos desarrolladores tienen dificultades para usarlos. En esta publicación hablaré sobre las características de trabajar con Java Generics, para que tenga menos de estas dificultades. Es útil si no es un gurú genérico y ayudará a evitar muchas dificultades al sumergirse en el tema.

Trabaja con colecciones
Supongamos que un banco necesita calcular la cantidad de ahorro en las cuentas de los clientes. Antes del advenimiento de los "genéricos", el método de cálculo de la suma se veía así:
public long getSum(List accounts) { long sum = 0; for (int i = 0, n = accounts.size(); i < n; i++) { Object account = accounts.get(i); if (account instanceof Account) { sum += ((Account) account).getAmount(); } } return sum; }
Repetimos, revisamos la lista de cuentas y verificamos si el elemento de esta lista es realmente una instancia de la clase
Account
, es decir, la cuenta del usuario. El tipo de nuestro objeto de la clase
Account
y el método
getAmount
se
getAmount
, lo que devolvió el monto en esta cuenta. Luego lo resumieron todo y devolvieron la cantidad total. Se requieren dos pasos:
if (account instanceof Account) {
sum += ((Account) account).getAmount();
Si no marca (
instanceof
) para pertenecer a la clase
Account
, entonces en la segunda etapa es posible una
ClassCastException
es decir, un bloqueo del programa. Por lo tanto, tal verificación era obligatoria.
Con el advenimiento de los genéricos, la necesidad de verificación de tipo y conversión ha desaparecido:
public long getSum2(List<Account> accounts) { long sum = 0; for (Account account : accounts) { sum += account.getAmount(); } return sum; }
Ahora método
getSum2(List<Account> accounts)
acepta como argumentos solo una lista de objetos de la clase
Account
. Esta restricción se indica en el método en sí, en su firma, el programador simplemente no puede transferir ninguna otra lista, solo la lista de cuentas de clientes.
No necesitamos verificar el tipo de elementos de esta lista: está implícito en la descripción del tipo del parámetro del método
List<Account> accounts
(puede leerse como una
Account
). Y el compilador arrojará un error si algo sale mal, es decir, si alguien intenta pasar una lista de objetos distintos de la clase
Account
a este método.
En la segunda línea del cheque, la necesidad también desapareció. Si es necesario, el
casting
se realizará en la etapa de compilación.
Principio de sustitución
El principio de sustitución de Barbara Liskov es una definición específica de un subtipo en la programación orientada a objetos. La idea de Liskov de un "subtipo" define el concepto de sustitución: si
S
es un subtipo de
T
, entonces los objetos de tipo
T
en un programa pueden ser reemplazados por objetos de tipo
S
sin ningún cambio en las propiedades deseadas de este programa.
Tipo
| Subtipo
|
Numero
| Entero
|
Lista <E>
| ArrayList <E>
|
Colección <E>
| Lista <E>
|
Iterable <E>
| Colección <E>
|
Ejemplos de relación de tipo / subtipoAquí hay un ejemplo del uso del principio de sustitución en Java:
Number n = Integer.valueOf(42); List<Number> aList = new ArrayList<>(); Collection<Number> aCollection = aList; Iterable<Number> iterable = aCollection;
Integer
es un subtipo de
Number
, por lo tanto, a la variable
n
tipo
Number
se le puede asignar el valor que devuelve el método
Integer.valueOf(42)
.
Covarianza, contravarianza e invariancia
Primero, una pequeña teoría. La covarianza es la preservación de la jerarquía de herencia de los tipos de origen en tipos derivados en el mismo orden. Por ejemplo, si el
Gato es un subtipo de
Animales , entonces el
Conjunto de <Gatos> es un subtipo del
Conjunto de <Animales> . Por lo tanto, teniendo en cuenta el principio de sustitución, se puede realizar la siguiente asignación:
Many <Animals> = Many <Cats>La contravarianza es la inversión de la jerarquía de los tipos de origen en los tipos derivados. Por ejemplo, si el
Gato es un subtipo de los
, entonces el
Conjunto <Animales> es un subtipo del
Conjunto de <Gatos> . Por lo tanto, teniendo en cuenta el principio de sustitución, se puede realizar la siguiente asignación:
Many <Cats> = Many <Animals>Invarianza: falta de herencia entre los tipos derivados. Si el
Gato es un subtipo de
Animales , entonces el
Conjunto de <Gatos> no
es un subtipo del
Conjunto de <Animales> y el
Conjunto de <Animales> no
es un subtipo del
Conjunto de <Gatos> .
Las matrices en Java son covariantes . El tipo
S[]
es un subtipo de
T[]
si
S
es un subtipo de
T
Ejemplo de asignación:
String[] strings = new String[] {"a", "b", "c"}; Object[] arr = strings;
Asignamos un enlace a una matriz de cadenas a la variable
arr
, cuyo tipo es
« »
. Si las matrices no fueran covariantes, no podríamos hacer esto. Java le permite hacer esto, el programa compila y se ejecuta sin errores.
arr[0] = 42;
Pero si tratamos de cambiar el contenido de la matriz a través de la variable
arr
y escribimos el número 42 allí, obtendremos una
ArrayStoreException
en la etapa de ejecución del programa, ya que 42 no es una cadena, sino un número. Este es el inconveniente de la covarianza de las matrices Java: no podemos realizar comprobaciones en la etapa de compilación, y algo puede romperse ya en tiempo de ejecución.
Los "genéricos" son invariables. Aquí hay un ejemplo:
List<Integer> ints = Arrays.asList(1,2,3); List<Number> nums = ints;
Si toma una lista de enteros, entonces no será un subtipo de tipo
Number
, ni ningún otro subtipo. Él es solo un subtipo de sí mismo. Es decir,
List <Integer>
es una
List<Integer>
y nada más. El compilador se asegurará de que la variable
ints
declarada como una lista de objetos de la clase
Integer contenga solo objetos de la clase
Integer
y nada más. En la etapa de compilación, se realiza una verificación y nada caerá en nuestro tiempo de ejecución.
Comodines
¿Los genéricos son siempre invariantes? No Daré ejemplos:
List<Integer> ints = new ArrayList<Integer>(); List<? extends Number> nums = ints;
Esto es covarianza.
List<Integer>
- subtipo de
List<? extends Number>
List<? extends Number>
List<Number> nums = new ArrayList<Number>(); List<? super Integer> ints = nums;
Esto es contravarianza.
List<Number>
es un subtipo de
List<? super Integer>
List<? super Integer>
.
Un registro como
"? extends ..."
o
"? super ..."
se llama comodín o comodín, con un límite superior (se
extends
) o un límite inferior (
super
).
List<? extends Number>
List<? extends Number>
puede contener objetos cuya clase es
Number
o hereda de
Number
.
List<? super Number>
List<? super Number>
puede contener objetos cuya clase es
Number
o cuyo
Number
es un heredero (supertipo de
Number
).

| extiende B - comodín con límite superior super B - comodín con un límite inferior donde B - representa el borde
Un registro de la forma T 2 <= T 1 significa que el conjunto de tipos descritos por T 2 es un subconjunto del conjunto de tipos descritos por T 1
es decir Número <=? extiende objeto ? extiende Número <=? extiende objeto y ? super objeto <=? super numero
|
Más interpretación matemática del tema.Un par de tareas para probar el conocimiento:
1. ¿Por qué aparece el error en tiempo de compilación en el siguiente ejemplo? ¿Qué valor puedo agregar a la lista de
nums
?
List<Integer> ints = new ArrayList<Integer>(); ints.add(1); ints.add(2); List<? extends Number> nums = ints; nums.add(3.14);
La respuesta¿Debería declararse el contenedor con comodín ? extends
? extends
, solo puede leer los valores. No se puede agregar nada a la lista excepto null
. Para agregar un objeto a la lista necesitamos otro tipo de comodín - ? super
? super
2. ¿Por qué no puedo obtener un artículo de la lista a continuación?
public static <T> T getFirst(List<? super T> list) { return list.get(0);
La respuesta¿No puede leer un elemento de un contenedor con comodín
? super
? super
, excepto por un objeto de clase
Object
public static <T> Object getFirst(List<? super T> list) { return list.get(0); }
El Principio Get and Put o PECS (Productor Extiende Consumer Super)
La función comodín con límites superior e inferior ofrece características adicionales relacionadas con el uso seguro de los tipos. Solo puede leer de un tipo de variable, solo escribir en otro (la excepción es la capacidad de escribir
null
para
extends
y leer
Object
para
super
). Para que sea más fácil recordar cuándo usar qué comodín, existe el principio PECS: Producer Extender Consumer Super.
- Si declaramos un comodín con extensiones , entonces este es el productor . Él solo "produce", proporciona un elemento del contenedor y no acepta nada.
- Si anunciamos un comodín con super , entonces esto es consumidor . Solo acepta, pero no puede proporcionar nada.
Considere usar Wildcard y el principio PECS usando el método de copia en la clase java.util.Collections como ejemplo.
public static <T> void copy(List<? super T> dest, List<? extends T> src) { … }
El método copia elementos de la lista
src
original a la lista
dest
.
src
: ¿declarado con comodín
? extends
? extends
y es el productor, y se declara
dest
con comodín
? super
? super
y es un consumidor. Dada la covarianza y contravarianza del comodín, puede copiar elementos de la lista de
ints
a la lista de
nums
:
List<Number> nums = Arrays.<Number>asList(4.1F, 0.2F); List<Integer> ints = Arrays.asList(1,2); Collections.copy(nums, ints);
Si confundimos los parámetros del método de copia por error e intentamos copiar de la lista de
nums
a la lista de
ints
, el compilador no nos permitirá hacer esto:
Collections.copy(ints, nums);
<?> y tipos sin formato
A continuación se muestra un comodín con un comodín ilimitado. Acabamos de poner
<?>
, Sin las palabras clave
super
o
extends
:
static void printCollection(Collection<?> c) {
De hecho, dicho comodín "ilimitado" sigue siendo limitado, desde arriba.
Collection<?>
También es un comodín, como "
? extends Object
". Un registro de la forma
Collection<?>
equivalente a la
Collection<? extends Object>
Collection<? extends Object>
, lo que significa que la colección puede contener objetos de cualquier clase, ya que todas las clases en Java heredan de
Object
, por lo que la sustitución se llama ilimitada.
Si omitimos la indicación de tipo, por ejemplo, como aquí:
ArrayList arrayList = new ArrayList();
luego dicen que
ArrayList
es el tipo
Raw
del
ArrayList parametrizado
<T> . Con los tipos sin formato, volvemos a la era de los genéricos y abandonamos conscientemente todas las características inherentes a los tipos con parámetros.
Si intentamos llamar a un método parametrizado en el tipo Raw, el compilador nos dará una advertencia de "Llamada no verificada". Si intentamos asignar una referencia a un tipo Raw parametrizado a un tipo, el compilador emitirá una advertencia "Asignación no verificada". Ignorar estas advertencias, como veremos más adelante, puede provocar errores durante la ejecución de nuestra aplicación.
ArrayList<String> strings = new ArrayList<>(); ArrayList arrayList = new ArrayList(); arrayList = strings;
Captura de comodines
Ahora intentemos implementar un método que permute los elementos de una lista en el orden inverso.
public static void reverse(List<?> list);
Se produjo un error de compilación porque el método
reverse
toma una lista con un carácter comodín ilimitado
<?>
Como argumento.
<?>
significa lo mismo que
<? extends Object>
<? extends Object>
. Por lo tanto, de acuerdo con el principio PECS, la
list
es el
producer
. Y el
producer
solo produce elementos. Y en el bucle
for
llamamos al método
set()
, es decir tratando de escribir en la
list
. Y así descansamos contra la protección de Java, que no nos permite establecer algún valor por índice.
Que hacer El patrón de
Wildcard Capture
nos ayudará. Aquí creamos un método genérico de
rev
. Se declara con una variable de tipo
T
Este método acepta una lista de tipos
T
, y podemos hacer un conjunto.
public static void reverse(List<?> list) { rev(list); } private static <T> void rev(List<T> list) { List<T> tmp = new ArrayList<T>(list); for (int i = 0; i < list.size(); i++) { list.set(i, tmp.get(list.size()-i-1)); } }
Ahora todo se compilará con nosotros. La captura de comodines fue capturada aquí. Cuando se llama al método
reverse(List<?> list)
, se pasa una lista de algunos objetos (por ejemplo, cadenas o enteros) como argumento. Si podemos capturar el tipo de estos objetos y asignarlo a una variable de tipo
X
, entonces podemos concluir que
T
es
X
Puede leer más sobre
Wildcard Capture
aquí y
aquí .
Conclusión
Si necesita leer desde el contenedor, utilice un comodín con el borde superior "
? extends
". Si necesita escribir en el contenedor, utilice un comodín con un borde inferior de "
? super
". No use comodines si necesita grabar y leer.
¡No use tipos
Raw
! Si el argumento de tipo no está definido, utilice el comodín
<?>
.
Escribir variables
Cuando escribimos el identificador entre paréntesis angulares, por ejemplo,
<T>
o
<E>
al declarar una clase o método, creamos
una variable de tipo . Una variable de tipo es un identificador no calificado que se puede usar como un tipo en el cuerpo de una clase o método. Una variable de tipo se puede acotar arriba.
public static <T extends Comparable<T>> T max(Collection<T> coll) { T candidate = coll.iterator().next(); for (T elt : coll) { if (candidate.compareTo(elt) < 0) candidate = elt; } return candidate; }
En este ejemplo, la expresión
T extends Comparable<T>
define
T
(una variable de tipo) limitada anteriormente por el tipo
Comparable<T>
. A diferencia del comodín, las variables de tipo solo se pueden limitar en la parte superior (solo se
extends
). No se puede escribir
super
. Además, en este ejemplo,
T
depende de sí mismo, se llama
recursive bound
, un borde recursivo.
Aquí hay otro ejemplo de la clase Enum:
public abstract class Enum<E extends Enum<E>>implements Comparable<E>, Serializable
Aquí, la clase Enum se parametriza por tipo E, que es un subtipo de
Enum<E>
.
Límites múltiples
Multiple Bounds
: múltiples restricciones. Está escrito a través del carácter "
&
", es decir, decimos que el tipo representado por una variable de tipo
T
debe estar limitado desde arriba por la clase
Object
y la interfaz
Comparable
.
<T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
Grabar
Object & Comparable<? super T>
Object & Comparable<? super T>
forma el tipo de intersección
Multiple Bounds
. La primera limitación, en este caso,
Object
, se usa para
erasure
, el proceso de maceración de tipos. Lo realiza el compilador en la etapa de compilación.
Conclusión
Una variable de tipo solo se puede limitar sobre uno o más tipos. En el caso de restricciones múltiples, el borde izquierdo (la primera restricción) se usa en el proceso de sobrescritura (Borrado de tipo).
Tipo de borrado
Type Erasure es una asignación de tipos (posiblemente incluyendo tipos parametrizados y variables de tipo) a tipos que nunca son tipos parametrizados o tipos variables. Escribimos el tipo
T
mashing como
|T|
.
La pantalla de maceración se define de la siguiente manera:
- Mashing el tipo parametrizado G < T1 , ..., Tn > es | G |
- Machacar un tipo anidado TC es | T |. C
- Mashing el tipo de matriz T [] es | T | []
- Mezclar una variable de tipo es machacar su borde izquierdo
- Mashing cualquier otro tipo es este tipo en sí
Durante la ejecución de Type Erasure (type mashing), el compilador realiza las siguientes acciones:
- agrega fundición de tipos para proporcionar seguridad de tipo si es necesario
- genera métodos de puente para mantener el polimorfismo
T (tipo)
| | T | (Tipo de trituración)
|
List <Integer>, List <String>, List <List <String >>
| Lista
|
List <Integer> []
| Lista []
|
Lista
| Lista
|
int
| int
|
Entero
| Entero
|
<T extiende Comparable <T>>
| Comparable
|
<T extiende Objeto y Comparable <? super T >>
| Objeto
|
LinkedCollection <E> .Node
| LinkedCollection.Node
|
Esta tabla muestra en qué se convierten los diferentes tipos durante el proceso de maceración, borrado de tipos.
En la siguiente captura de pantalla hay dos ejemplos del programa:

La diferencia entre los dos es que se produce un error en tiempo de compilación a la izquierda y a la derecha todo se compila sin errores. Por qué
La respuestaEn Java, dos métodos diferentes no pueden tener la misma firma. En el proceso Type Erasure, el compilador agregará el método bridge
public int compareTo(Object o)
. Pero la clase ya contiene un método con tal firma que causará un error durante la compilación.
Compile la clase Name quitando el
compareTo(Object o)
y observe el bytecode resultante usando javap:
# javap Name.class Compiled from "Name.java" public class ru.sberbank.training.generics.Name implements java.lang.Comparable<ru.sberbank.training.generics.Name> { public ru.sberbank.training.generics.Name(java.lang.String); public java.lang.String toString(); public int compareTo(ru.sberbank.training.generics.Name); public int compareTo(java.lang.Object); }
Vemos que la clase contiene un
int compareTo(java.lang.Object)
, aunque lo eliminamos del código fuente. Este es el método puente que agregó el compilador.
Tipos reificables
En Java, decimos que un tipo es
reifiable
si su información es totalmente accesible en tiempo de ejecución. Los tipos reificables incluyen:
- Tipos primitivos ( int , long , boolean )
- Tipos no parametrizados (no genéricos) ( String , Integer )
- Tipos parametrizados cuyos parámetros se representan como comodines ilimitados (caracteres comodín ilimitados) ( Lista <?> , Colección <?> )
- Tipos sin formato (sin forma) ( List , ArrayList )
- Matrices cuyos componentes son tipos Reifiable ( int [] , Number [] , List <?> [] , List [ )
¿Por qué hay información disponible sobre algunos tipos pero no sobre otros? El hecho es que debido al proceso de sobrescritura de tipos por parte del compilador, se puede perder información sobre algunos tipos. Si se pierde, este tipo ya no será reificable. Es decir, no está disponible en tiempo de ejecución. Si está disponible, respectivamente, reificable.
La decisión de no hacer que todos los tipos genéricos estén disponibles en tiempo de ejecución es una de las decisiones de diseño más importantes y conflictivas en el sistema de tipos Java. Esto se hace, en primer lugar, por compatibilidad con el código existente. Tuve que pagar por la compatibilidad de la migración: la accesibilidad total de un sistema de tipos genéricos en tiempo de ejecución no es posible.
Qué tipos no son reificables:
- Tipo variable ( T )
- Tipo parametrizado con el tipo de parámetro especificado ( List <Number> ArrayList <String> , List <List <String>> )
- Un tipo parametrizado con el límite superior o inferior especificado ( Lista <? Amplía número>, Comparable <? Super String> ). Pero aquí hay una reserva: Lista <? extiende Object> - no reifiable, pero List <?> - reifiable
Y una tarea más. ¿Por qué en el siguiente ejemplo no se puede crear una excepción parametrizada?
class MyException<T> extends Exception { T t; }
La respuestaCada expresión catch en try-catch verifica el tipo de la excepción recibida durante la ejecución del programa (que es equivalente a la instancia de), respectivamente, el tipo debe ser Reifiable. Por lo tanto, Throwable y sus subtipos no se pueden parametrizar.
class MyException<T> extends Exception {
Advertencias no verificadas
Compilar nuestra aplicación puede producir la llamada
Unchecked Warning
, una advertencia de que el compilador no pudo determinar correctamente el nivel de seguridad de uso de nuestros tipos. Esto no es un error, sino una advertencia, por lo que puede omitirlo. Pero es aconsejable arreglarlo todo para evitar problemas en el futuro.
Contaminación del montón
Como mencionamos anteriormente, la asignación de una referencia a un tipo Raw a una variable de un tipo parametrizado lleva a la advertencia "Asignación no verificada". Si lo ignoramos, es posible una situación llamada "
Heap Pollution
" (contaminación del montón). Aquí hay un ejemplo:
static List<String> t() { List l = new ArrayList<Number>(); l.add(1); List<String> ls = l;
En la línea (1), el compilador advierte sobre "Asignación no verificada".
Necesitamos dar otro ejemplo de "contaminación de montón" - cuando usamos objetos parametrizados. El fragmento de código a continuación muestra claramente que no está permitido usar tipos parametrizados como argumentos para un método que usa
Varargs
. En este caso, el parámetro del método m es
List<String>…
, es decir de hecho, una matriz de elementos de tipo
List<String>
. Dada la regla de mostrar tipos durante el
stringLists
, el tipo
stringLists
convierte en una matriz de listas sin procesar (
List[]
), es decir la asignación se puede hacer
Object[] array = stringLists;
y luego escribe en una
array
un objeto que no sea la lista de cadenas (1), que
ClassCastException
en la cadena (2).
static void m(List<String>... stringLists) { Object[] array = stringLists; List<Integer> tmpList = Arrays.asList(42); array[0] = tmpList;
Considere otro ejemplo:
ArrayList<String> strings = new ArrayList<>(); ArrayList arrayList = new ArrayList(); arrayList = strings;
Java permite la asignación en la línea (1). Esto es necesario para la compatibilidad con versiones anteriores. Pero si intentamos ejecutar el método
add
en la línea (2), recibimos una advertencia de
Unchecked call
: el compilador nos advierte de un posible error. De hecho, estamos tratando de agregar un número entero a la lista de cadenas.
Reflexion
Aunque, durante la compilación, los tipos parametrizados se someten a un procedimiento de borrado de tipo, podemos obtener información utilizando Reflection.
- Todos los reificables están disponibles a través del mecanismo de reflexión.
- La información sobre el tipo de campos de clase, los parámetros del método y los valores devueltos por ellos está disponible a través de Reflection.
Reflection
Reifiable
, . , , , - , :
java.lang.reflect.Method.getGenericReturnType()
Generics
java.lang.Class
. :
List<Integer> ints = new ArrayList<Integer>(); Class<? extends List> k = ints.getClass(); assert k == ArrayList.class;
ints
List<Integer>
ArrayList< Integer>
.
ints.getClass()
Class<ArrayLis>
,
List<Integer>
List
.
Class<ArrayList>
k
Class<? extends List>
, ?
extends
.
ArrayList.class
Class<ArrayList>
.
Conclusión
, Reifiable. Reifiable : , , , Raw , reifiable.
Unchecked Warnings « » .
Reflection , Reifiable. Reflection , .
Type Inference
« ». () . :
List<Integer> list = new ArrayList<Integer>();
- Java 7
ArrayList
:
List<Integer> list = new ArrayList<>();
ArrayList
–
List<Integer>
.
type inference
.
Java 8 JEP 101.
Type Inference. :
- (reduction)
- (incorporation)
- (resolution)
: , , — .
, . JEP 101 .
, :
class List<E> { static <Z> List<Z> nil() { ... }; static <Z> List<Z> cons(Z head, List<Z> tail) { ... }; E head() { ... } }
List.nil()
:
List<String> ls = List.nil();
,
List.nil()
String
— JDK 7, .
, , , :
List.cons(42, List.nil());
JDK 7 compile-time error. JDK 8 . JEP-101, — . JDK 8 — :
List.cons(42, List.<Integer>nil());
JEP-101 , , :
String s = List.nil().head();
, . , JDK , :
String s = List.<String>nil().head();
JEP 101 StackOverflow . , , 7- , 8- – ? :
class Test { static void m(Object o) { System.out.println("one"); } static void m(String[] o) { System.out.println("two"); } static <T> T g() { return null; } public static void main(String[] args) { m(g()); } }
- JDK1.8:
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: invokestatic #6
0
g:()Ljava/lang/Object;
java.lang.Object
. , 3 («») ,
java.lang.String
, 6
m:([Ljava/lang/String;)
, «two».
- JDK1.7 – Java 7:
public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: invokestatic #6
,
checkcast
, Java 8,
m:(Ljava/lang/Object;)
, «one».
Checkcast
– , Java 8.
, Oracle
JDK1.7 JDK 1.8 , Java, , .
, Java 8 , Java 7, :
public static void main(String[] args) { m((Object)g()); }
Conclusión
Java Generics . , :
- Bloch, Joshua. Effective Java. Third Edition. Addison-Wesley. ISBN-13: 978-0-13-468599-1
, Java Generics.