Apuntes · Capítulo 10Programar con tipos
Genéricos: clases, métodos, wildcards y type erasure
Genéricos: clases, métodos, wildcards y type erasure
En la semana 4 usamos ArrayList<Song> sin preguntarnos mucho qué significa
ese <Song>. Sabíamos que la lista solo aceptaba canciones y que get
devolvía una Song sin casting. En la semana 9 vimos que un error que el
compilador detecta es mucho más barato que una excepción que aparece frente al
usuario. Este capítulo junta las dos ideas: los genéricos (generics) son
la forma en que Java deja escribir una clase o un método una sola vez para
muchos tipos, sin perder la verificación del compilador.
La primera parte cuenta el problema que resuelven, cómo se escriben clases y
métodos genéricos y qué pasa con los tipos primitivos. La segunda responde
preguntas que aparecen apenas se usan en serio: por qué una List<Integer> no
se puede pasar donde se pide una List<Number>, para qué sirven los
wildcards ? extends y ? super, y qué es el type erasure, la decisión de diseño que explica casi todas las
restricciones de los genéricos en Java.
Lectura. Este capítulo acompaña a Eck, Introduction to Programming Using Java (versión 9), subsecciones 10.1.1 a 10.1.3 (la programación genérica y cómo la hace Java), 10.1.7 (Generics and Wrapper Classes) y la sección 10.5 Writing Generic Classes and Methods (10.5.1 a 10.5.4). Como complemento, la serie Generics de dev.java. El código del capítulo está en
code/week-10y se prueba conmake testdentro del contenedor del curso.
Ruta de lectura.
- Antes de clase (obligatorio, 15–20 min): El problema: una caja de
Objecty la introducción de Clases genéricas.- En clase: en la sesión A, Varios parámetros:
Pair<A, B>,ArrayList<E>por dentro, Métodos genéricos, Genéricos y tipos primitivos y Raw types: el error que todavía compila; en la sesión B, Invarianza, Bounded types, Wildcards con La regla PECS y Type erasure. En el laboratorio, El laboratorio: una colección genérica propia.- Después de clase: Lo que el type erasure no permite, el Resumen y los Ejercicios. Para ampliar, las secciones de Eck de la nota Lectura.
El problema: una caja de Object
Supongamos que queremos una clase que guarde un valor, el que sea: un nombre,
un número, un producto. Antes de que existieran los genéricos (Java 5, 2004)
la única forma de escribirla una sola vez era usar Object, el supertipo de
todas las clases, como en
ObjectBox.java:
public class ObjectBox {
private Object content;
public void put(Object content) {
this.content = content;
}
public Object get() {
return content;
}
}
Funciona con cualquier cosa, y ese es justamente el problema.
RawProblem.java la usa:
ObjectBox box = new ObjectBox();
box.put("Lima"); // compila: un String es un Object
String city = (String) box.get(); // hay que hacer casting al sacar
System.out.println("ciudad: " + city);
box.put(42); // también compila: nadie lo impide
String other = (String) box.get(); // el error aparece aquí, en ejecución
$ java -cp build RawProblem
ciudad: Lima
ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (...)
Hay dos defectos. El primero es de comodidad: cada get necesita un casting,
porque el compilador solo sabe que sale un Object. El segundo es grave: el
compilador no puede saber qué hay dentro de la caja, así que acepta
box.put(42) aunque el programa espera textos. El error se descubre recién
al ejecutar, y no en la línea equivocada (put(42)) sino más tarde, en el
get, que puede estar en otro archivo. Es la ClassCastException de la
semana 6.
Clases genéricas
Una clase genérica declara uno o más type parameters entre < y
> después de su nombre: nombres que representan un tipo que todavía no se
conoce. Dentro de la clase, el type parameter se usa como cualquier otro tipo.
Box.java:
public class Box<T> {
private T content; // null si la caja está vacía
public void put(T content) {
this.content = content;
}
public T get() {
return content;
}
public boolean isEmpty() {
return content == null;
}
}
T no es una clase: es un nombre que se reemplaza por un tipo concreto cada
vez que se usa la clase. Box<String> es una caja cuyo put recibe un
String y cuyo get devuelve un String; Box<Integer>, una de enteros. Lo
que va entre <> al usar la clase se llama type argument.
Box<String> city = new Box<>(); // diamante: T = String se infiere
city.put("Lima");
String s = city.get(); // sin casting
Y el error de antes ahora no compila (el archivo está en
errores/WrongType.java; make errores compila cada ejemplo de esa carpeta y comprueba que falle):
$ javac -Xlint:all -cp build errores/WrongType.java
errores/WrongType.java:5: error: incompatible types: int cannot be converted to String
city.put(42);
^
1 error
Idea clave. Un type parameter le dice al compilador qué hay dentro. Con esa información puede rechazar lo que no corresponde al meterlo y evitar el casting al sacarlo. El código se escribe una sola vez y sirve para todos los tipos.
Varios parámetros: Pair<A, B>
Una clase puede tener varios type parameters. Los records también pueden
ser genéricos. Pair.java es un par inmutable
de dos valores de tipos posiblemente distintos:
public record Pair<A, B>(A first, B second) {
/** Devuelve un par nuevo con los componentes intercambiados. */
public Pair<B, A> swap() {
return new Pair<>(second, first);
}
}
Pair<String, Integer> stock = new Pair<>("arroz", 40);
System.out.println(stock + " -> " + stock.swap());
Pair[first=arroz, second=40] -> Pair[first=40, second=arroz]
Fíjense en el tipo de retorno de swap: Pair<B, A>. El compilador sabe que
stock.swap().first() es un Integer y no un String, sin que nadie lo
escriba.
ArrayList<E> por dentro
Ahora podemos leer la documentación de ArrayList de otra forma. La clase se
declara ArrayList<E> (E de element), y sus métodos usan E: add(E e),
E get(int index), E remove(int index). Cuando en la semana 4 escribimos
ArrayList<Song>, todos esos E pasaron a ser Song. No hay nada mágico en
las colecciones de la biblioteca: son clases genéricas como Box.
En Java. Los nombres de los type parameters son, por convención, una sola letra mayúscula, para distinguirlos a simple vista de las clases:
T(type) para un tipo cualquiera,E(element) para el elemento de una colección,KyV(key, value) para las claves y valores de un mapa, yA,BoS,Ucuando hay varios.
Métodos genéricos
A veces no hace falta una clase genérica entera, sino un solo método que
funcione para cualquier tipo. Un método genérico declara sus propios
type parameters antes del tipo de retorno.
ListUtils.java:
/** El primer elemento de la lista, o fallback si está vacía. */
public static <T> T firstOrDefault(List<T> list, T fallback) {
return list.isEmpty() ? fallback : list.get(0);
}
List es la interfaz que implementa ArrayList; por ahora basta con saber
que un ArrayList<String> se puede pasar donde se pide un List<String> (las
interfaces de colecciones son el tema de la semana 11). El <T> antes de T
dice “este método funciona para cualquier tipo T”, y la firma dice que lo
que devuelve es del mismo tipo que los elementos de la lista y que el valor por
defecto.
Al llamarlo, no hace falta decir cuánto vale T: el compilador lo deduce de
los argumentos. Eso se llama inferencia de tipos (type inference).
List<String> names = List.of("alice", "bob");
List<String> empty = new ArrayList<>();
System.out.println(ListUtils.firstOrDefault(names, "nadie")); // alice
System.out.println(ListUtils.firstOrDefault(empty, "nadie")); // nadie
La misma inferencia permite el operador diamante <>: en
Box<String> city = new Box<>(); el compilador completa new Box<String>()
mirando el tipo de la variable.
Error común. Poner el
<T>en el lugar equivocado. Enpublic static T firstOrDefault(...)sin<T>, el compilador busca una clase llamadaT, no la encuentra y dicecannot find symbol. Los type parameters de un método van entre los modificadores y el tipo de retorno:public static <T> T firstOrDefault(...).
Genéricos y tipos primitivos
Un type argument tiene que ser un tipo de referencia. Box<int> y
ArrayList<int> no existen. Para guardar números se usa la clase
wrapper correspondiente, que guarda el primitivo dentro de un objeto: Integer, Long, Double,
Boolean, Character… Java convierte automáticamente entre el primitivo y
su wrapper: de int a Integer (boxing) y de Integer a int
(unboxing).
Box<Integer> count = new Box<>();
count.put(7); // boxing: 7 -> Integer.valueOf(7)
int n = count.get() + 1; // unboxing: Integer -> int
Esa conversión es cómoda pero no es gratis: cada Integer es un objeto en el
heap, con su referencia y su cabecera, donde un int ocupa 4 bytes. Una
ArrayList<Integer> de un millón de números usa varias veces más memoria que
un int[] del mismo tamaño. Para colecciones grandes de números, un arreglo de
primitivos sigue siendo la mejor opción.
Error común. Comparar dos
Integercon==. Son objetos, así que==compara referencias (semana 2), no valores. Para que sea más confuso, Java guarda en caché losIntegerde −128 a 127 (es lo mínimo que garantiza; una JVM puede guardar más), así que con números pequeños parece funcionar.BoxDemo.java:Integer a = 127, b = 127, c = 1000, d = 1000; System.out.println("127 == 127: " + (a == b) + ", 1000 == 1000: " + (c == d) + ", equals: " + c.equals(d));127 == 127: true, 1000 == 1000: false, equals: trueCon wrappers, igual que con cualquier objeto, se compara con
equals.
Raw types: el error que todavía compila
Por compatibilidad con el código anterior a Java 5, todavía se puede escribir
una clase genérica sin type argument: ArrayList a secas. Eso se
llama raw type, y devuelve exactamente el problema de
ObjectBox.
errores/RawList.java:
ArrayList names = new ArrayList();
names.add("alice");
names.add(42);
String first = (String) names.get(1);
Sin opciones, javac solo deja una nota que es fácil pasar por alto:
$ javac -d build errores/RawList.java
Note: errores/RawList.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
$ java -cp build RawList
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (...)
at RawList.main(RawList.java:9)
Con -Xlint:all, la opción que usamos en todo el curso, aparecen las
advertencias que explican el problema, y con -Werror la compilación se
detiene:
$ javac -Xlint:all -Werror -d build errores/RawList.java
errores/RawList.java:6: warning: [rawtypes] found raw type: ArrayList
ArrayList names = new ArrayList();
^
missing type arguments for generic class ArrayList<E>
...
errores/RawList.java:7: warning: [unchecked] unchecked call to add(E) as a member of the raw type ArrayList
names.add("alice");
^
...
error: warnings found and -Werror specified
1 error
4 warnings
Error común. Usar raw types “porque compila”. Una advertencia
uncheckedsignifica que el compilador ya no puede garantizar que los tipos sean correctos, y que unaClassCastExceptionpuede aparecer en una línea que parece inocente. Siempre con type argument:ArrayList<String>, onew ArrayList<>()con diamante.
Invarianza: List<Integer> no es una List<Number>
Integer es subtipo de Number (también lo son Double y Long). Por eso
uno esperaría que una List<Integer> se pudiera usar donde se pide una
List<Number>. No se puede
(errores/Invariance.java):
static double total(List<Number> numbers) { ... }
List<Integer> ints = new ArrayList<>(List.of(1, 2, 3));
List<Number> nums = ints;
System.out.println(total(ints));
errores/Invariance.java:16: error: incompatible types: List<Integer> cannot be converted to List<Number>
List<Number> nums = ints;
^
errores/Invariance.java:17: error: incompatible types: List<Integer> cannot be converted to List<Number>
System.out.println(total(ints));
^
2 errors
La razón se ve imaginando que sí se pudiera. Si nums y ints fueran la
misma lista vista con dos tipos, nums.add(2.5) sería válido (un Double es
un Number), y la lista de enteros terminaría con un decimal adentro. El
próximo int x = ints.get(3) fallaría en ejecución. Para que eso no pueda
pasar, Java hace a los genéricos invariantes: List<A> y List<B> no
tienen ninguna relación de subtipo, aunque A y B sí la tengan.
En Java. Los arreglos se diseñaron al revés, y se nota.
Integer[]sí es subtipo deNumber[](los arreglos son covariantes), así que el error del párrafo anterior compila y se detecta recién al ejecutar.ErasureDemo.java:Integer[] ints = {1, 2, 3}; Number[] nums = ints; // compila nums[0] = 2.5; // un Double en un arreglo de IntegerArrayStoreException: java.lang.DoubleCada arreglo recuerda en ejecución el tipo de sus elementos y la JVM revisa cada asignación. Con las listas, el compilador no deja llegar hasta ahí.
Bounded types
Un método que busca el mayor elemento de una lista no puede funcionar para
cualquier T: necesita poder comparar los elementos. La semana 7 presentó
Comparable<T>, la interfaz con compareTo. Un bounded type exige
que T la implemente:
/** El mayor elemento según su orden natural. La lista no puede estar vacía. */
public static <T extends Comparable<T>> T max(List<T> list) {
if (list.isEmpty()) {
throw new IllegalArgumentException("lista vacía");
}
T best = list.get(0);
for (T x : list) {
if (x.compareTo(best) > 0) {
best = x;
}
}
return best;
}
<T extends Comparable<T>> se lee “para cualquier tipo T que se pueda
comparar con otro T”. Aquí extends sirve tanto para clases como para
interfaces. Dentro del método, como el compilador sabe que T tiene
compareTo, la llamada x.compareTo(best) compila. Afuera, el compilador
revisa que el argumento cumpla el bound (la restricción extends): max acepta una lista de Integer
o de String, que implementan Comparable, pero no una lista de Box.
List<Integer> marks = List.of(15, 18, 11);
List<String> words = List.of("pera", "uva", "mango");
System.out.println("max: " + ListUtils.max(marks) + ", " + ListUtils.max(words));
max: 18, uva
(Las palabras se comparan en orden alfabético, y "uva" va después de
"pera" y de "mango".) Y como el resultado es un T, ListUtils.max(marks)
devuelve un Integer, no un Comparable al que habría que hacer casting.
Wildcards
El parámetro de total(List<Number>) era demasiado estricto: solo acepta
exactamente List<Number>. Lo que queremos decir es “una lista de Number o
de algún subtipo de Number”. Eso es un wildcard:
/** Suma una lista de cualquier tipo numérico: Integer, Double, Long... */
public static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) { // leer: sale algo que es un Number
total += n.doubleValue();
}
return total;
}
List<? extends Number> acepta List<Number>, List<Integer>,
List<Double>… De una lista así se puede leer: lo que salga, sea lo que
sea, es un Number. Pero no se puede escribir en ella, porque el
compilador no sabe de qué subtipo es la lista que llegó: podría ser una
List<Double> y no aceptar un Integer
(errores/ExtendsReadOnly.java):
errores/ExtendsReadOnly.java:7: error: incompatible types: int cannot be converted to CAP#1
numbers.add(1);
^
where CAP#1 is a fresh type-variable:
CAP#1 extends Number from capture of ? extends Number
1 error
CAP#1 es el nombre que el compilador le pone a “el tipo desconocido que
llegó”. El mensaje dice: no sé qué tipo es, solo que extiende Number, así que
no puedo garantizar que un int quepa.
El caso contrario es ? super. Un método que escribe enteros en una lista
necesita una lista que acepte Integer: una List<Integer>, pero también una
List<Number> o una List<Object>.
/** Agrega 1, 2, ..., n a una lista que acepte Integer. */
public static void addCounting(List<? super Integer> out, int n) {
for (int i = 1; i <= n; i++) {
out.add(i); // escribir: entra un Integer
}
}
En una List<? super Integer> se puede escribir un Integer, pero al leer
solo se sabe que sale un Object.
La regla PECS
Las dos formas se juntan en un método que copia de una lista a otra:
/** Copia src al final de dst. src produce T; dst consume T (PECS). */
public static <T> void copy(List<? super T> dst, List<? extends T> src) {
for (T x : src) {
dst.add(x);
}
}
La regla para elegir se resume en PECS: producer extends, consumer super.
Si de un parámetro se sacan valores de tipo T (es un productor), va con
? extends T. Si se meten valores de tipo T (es un consumidor), va con
? super T. Si se hacen las dos cosas, sin wildcard: List<T>.
WildcardDemo.java copia las notas,
una List<Integer>, al final de una List<Object> que ya tenía un texto:
List<Object> all = new ArrayList<>(List.of("inicio"));
ListUtils.copy(all, marks);
System.out.println("all: " + all);
all: [inicio, 15, 18, 11]
El compilador infiere T = Integer: marks es una List<Integer>, que
cumple ? extends Integer, y all es una List<Object>, que cumple
? super Integer. Sin wildcards, con copy(List<T> dst, List<T> src), esta
llamada no compilaría: no hay un único T que sea a la vez Integer y
Object.


Error común. Llenar una API de wildcards “por si acaso”. Los wildcards hacen las firmas más difíciles de leer. Empiecen con tipos simples (
List<T>), y agreguen? extendso? supercuando una llamada real, o una prueba, muestre que hace falta aceptar listas de subtipos o de supertipos. Y nunca en el tipo de retorno: un método que devuelveList<? extends Number>obliga a quien lo llama a lidiar con el wildcard.
Type erasure
¿Cómo implementa la JVM los genéricos? La respuesta sorprende: casi todo el
trabajo lo hace el compilador. javac revisa todos los tipos usando los type
parameters, y después los borra: en el bytecode, Box<String> y
Box<Integer> son la misma clase Box, cuyo campo y cuyos métodos usan
Object (o el bound, si el type parameter tiene uno). Eso es el type
erasure. Un objeto Box no sabe con qué type argument se creó: en
ejecución, los type arguments no existen como tipos.
Se ve en ejecución (ErasureDemo.java):
Box<String> a = new Box<>();
Box<Integer> b = new Box<>();
System.out.println(a.getClass() + " == " + b.getClass() + ": "
+ (a.getClass() == b.getClass()));
class Box == class Box: true
Y se ve en el bytecode. Si desensamblamos Box con javap -c, get devuelve
un Object; y en BoxDemo, donde se usa city.get() como String, el
compilador agregó una instrucción checkcast: el casting que con ObjectBox
escribíamos a mano.
$ javap -c -cp build Box
...
public T get();
Code:
0: aload_0
1: getfield #7 // Field content:Ljava/lang/Object;
4: areturn
$ javap -c -cp build BoxDemo
...
14: aload_1
15: invokevirtual #16 // Method Box.get:()Ljava/lang/Object;
18: checkcast #20 // class java/lang/String
21: astore_2
...
En max, el type parameter tiene bound, y T se borra a ese bound: javap -s muestra
que su descriptor es (Ljava/util/List;)Ljava/lang/Comparable;, es decir,
recibe una List y devuelve un Comparable.


¿Por qué Java lo hizo así? Por compatibilidad: cuando llegaron los genéricos,
en 2004, había millones de líneas de código y bibliotecas compiladas que usaban
ArrayList sin parámetros. Con el type erasure, el bytecode de una
ArrayList<String> es el mismo de siempre, y el código viejo y el nuevo
pueden mezclarse. (Otros lenguajes, como C#, eligieron lo contrario y
guardan el type argument en ejecución.)
Idea clave. Los genéricos de Java son una verificación en compilación. En ejecución hay una sola clase, con
Object, y castings que el compilador agregó. En código sin warnings deunchecked, esos castings no fallan.
En Java. Dos precisiones. Primero,
Tno desaparece del todo: el.classguarda la declaración genérica como metadato (por esojavapmuestrapublic T get()), y la reflexión la puede leer. Lo que no existe es el type argument de cada objeto. Segundo, la garantía de los castings vale si todo el código respeta los tipos. Un raw type, un castuncheckedo una biblioteca que los use pueden meter unIntegeren unaList<String>(eso se llama heap pollution), y entonces elcheckcastsí lanzaClassCastException, lejos de donde estuvo el error.
Lo que el type erasure no permite
Como en ejecución T no existe, todo lo que necesitaría saber cuánto vale T
mientras el programa corre está prohibido. Los cuatro casos típicos están en
errores/ErasureLimits.java:
public class ErasureLimits<T> {
T make() {
return new T(); // ¿qué constructor llamar?
}
T[] array(int n) {
return new T[n]; // ¿de qué tipo es el arreglo?
}
boolean isStrings(Object o) {
return o instanceof List<String>; // en ejecución solo hay List
}
void print(List<String> list) { }
void print(List<Integer> list) { } // los dos se borran a print(List)
}
errores/ErasureLimits.java:19: error: name clash: print(List<Integer>) and print(List<String>) have the same erasure
void print(List<Integer> list) { }
^
errores/ErasureLimits.java:6: error: unexpected type
return new T();
^
required: class
found: type parameter T
...
errores/ErasureLimits.java:10: error: generic array creation
return new T[n];
^
errores/ErasureLimits.java:14: error: Object cannot be safely cast to List<String>
return o instanceof List<String>;
^
4 errors
new T(): la JVM tendría que saber qué clase construir, y en ejecuciónTesObject.new T[n]: un arreglo recuerda el tipo de sus elementos (por eso existeArrayStoreException), yTno se conoce. Es la restricción que más se nota al escribir una colección propia: el arreglo interno se declara comoObject[]y se hace casting aTal sacar, o se crea unObject[]y se convierte aT[]una sola vez, en el constructor. En ambos casos el compilador da una advertenciaunchecked, que se silencia con@SuppressWarnings("unchecked")solo en ese método, y solo después de convencerse de que únicamente entran objetos de tipoT. Es lo que harán en el laboratorio.instanceof List<String>: en ejecución hay unaList, sin saber de qué. Sí se puede preguntaro instanceof List<?>.- Overloading que solo cambia el type argument:
print(List<String>)yprint(List<Integer>)quedarían las dos comoprint(List)en el bytecode. Hay que darles nombres distintos.
El laboratorio: una colección genérica propia
En el laboratorio escriben BoundedStack<E>, un stack con capacidad máxima,
guardado en un arreglo. En code/week-10
está el esqueleto, con los métodos y su contrato. Al escribirlo aparecen
todos los temas del capítulo:
- el type parameter
Een la clase y en sus métodos; - el arreglo interno y el
new E[n]que no compila; - un invariante (
0 <= size <= capacity) y las excepciones de la semana 9 (IllegalStateExceptional sacar de un stack vacío o al hacerpushen uno lleno); - pruebas JUnit con dos tipos de elemento distintos;
- un método con wildcard,
pushAll(List<? extends E> items), que tiene que aceptar una lista de un subtipo deE; - y, con
javap -c, elcheckcastque agregó el compilador donde se usa lo que devuelvepop.
El código de la semana y make test
$ cd code/week-10
$ make test
...
== RawProblem
ciudad: Lima
ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (...)
== BoxDemo
caja: Lima, vacía: false
Pair[first=arroz, second=40] -> Pair[first=40, second=arroz]
alice
nadie
n = 8
127 == 127: true, 1000 == 1000: false, equals: true
== WildcardDemo
max: 18, uva
sum: 44.0, 6.5
numbers: [1, 2, 3, 0.5]
all: [inicio, 15, 18, 11]
== ErasureDemo
class Box == class Box: true
ArrayStoreException: java.lang.Double
list: [1, 2, 3]
...
[ 13 tests successful ]
[ 0 tests failed ]
Las pruebas están en BoxTest.java,
PairTest.java y
ListUtilsTest.java. make errores
compila uno por uno los archivos de errores/ y verifica que ninguno
compile con -Xlint:all -Werror: es la forma de dejar escritos ejemplos de lo
que el compilador rechaza.
Resumen
- Una colección de
Objectacepta cualquier cosa y obliga a hacer casting al sacar; el error de tipo aparece en ejecución (ClassCastException). - Una clase genérica (
Box<T>,Pair<A, B>) declara type parameters; al usarla, cada uso fija los type arguments, y el compilador rechaza lo que no corresponde. - Un método genérico declara sus parámetros antes del tipo de retorno
(
static <T> T firstOrDefault(...)). El compilador infiereT; el diamante<>usa la misma inferencia. - No hay genéricos de primitivos: se usan los wrappers, con boxing y
unboxing automáticos, que cuestan memoria. Los
Integerse comparan conequals. - Los raw types desactivan la verificación;
-Xlint:alllos delata. - Los genéricos son invariantes:
List<Integer>no esList<Number>. Los arreglos son covariantes y lo pagan conArrayStoreException. - Un bounded type (
<T extends Comparable<T>>) permite usar los métodos del bound dentro del método genérico. - Wildcards:
? extends Tpara leer (productor),? super Tpara escribir (consumidor): PECS. - Type erasure: en el bytecode
Tse reemplaza porObject(o por su bound) y el compilador insertacheckcast. Por eso no haynew T(), ninew T[n], niinstanceof List<String>, ni overloading que solo cambie el type argument.
Ejercicios
-
Escriban una clase
ObjectStackque guardeObjecten unArrayListy un programa que meta unStringy luego unInteger, y saque el primero comoString. Luego cámbienla aStack<T>. ¿En qué línea aparece el error en cada versión, y en qué momento (al compilar o al ejecutar)? -
Agreguen a
Pair<A, B>un métodowithFirstque reciba un nuevo primer componente, posiblemente de otro tipo, y devuelva un par nuevo. Escriban su firma completa antes de implementarlo. ¿Por qué el método necesita su propio type parameter? -
En
jshell, evalúenInteger x = 100, y = 100;y luegox == y. Repitan con 200. Después evalúenLong.valueOf(5).equals(5). Expliquen cada resultado. -
¿Cuáles de estas líneas compilan? Para las que no, digan qué error da el compilador y por qué. Verifíquenlo después.
List<Number> a = new ArrayList<Integer>(); List<? extends Number> b = new ArrayList<Integer>(); b.add(3); Number n = b.get(0); List<? super Integer> c = new ArrayList<Number>(); c.add(3); Integer i = c.get(0); -
Escriban un método genérico
static <T> void swap(T[] a, int i, int j)que intercambie dos elementos de un arreglo, y sus pruebas con unString[]y unInteger[]. ¿Se puede llamar con unint[]? ¿Por qué? -
Escriban
static <T extends Comparable<T>> T min(List<T> list)y sus pruebas, incluida la de una lista vacía. Luego intenten llamarlo con unaList<Box<String>>. Copien el mensaje del compilador y expliquen qué dice. -
Escriban un método
countGreater(List<? extends Number> list, double limit)que cuente cuántos elementos son mayores quelimit. Pruébenlo con unaList<Integer>y unaList<Double>. ¿Qué pasa si quitan el wildcard? -
Para cada uno de estos métodos, digan si el parámetro es productor, consumidor o ambos, y escriban su firma con los wildcards adecuados: (a) imprimir todos los elementos de una lista; (b) agregar a una lista los números pares hasta
n; (c) ordenar una lista en su lugar; (d) pasar todos los elementos de un stack a una lista. -
Compilen
RawProblem.javayBoxDemo.javay desensamblen ambos conjavap -c. Comparen las instrucciones que siguen a cadaget. ¿Qué diferencia hay entre el casting de uno y el del otro, en el bytecode y en quién garantiza que no falle? -
¿Por qué Java no permite
new T[n]pero sí permitenew ArrayList<T>()? Piensen en qué tiene que saber la JVM en ejecución en cada caso. -
Un compañero escribe
void add(Box<String> b)yvoid add(Box<Integer> b)en la misma clase, para tratar cada caja de forma distinta. Expliquen por qué no compila y propongan dos formas de resolverlo. -
Lean la subsección 10.5.3 de Eck sobre wildcards. Escriban con sus palabras la diferencia entre
List<?>,List<Object>y el raw typeList. ¿Qué se puede meter en cada una y qué sale al leer?