Semana 10 — Genéricos
Objetivos
- Escribir clases y métodos genéricos con type parameters y explicar qué errores evitan frente a usar
Object. - Restringir un type parameter con un bound (
<T extends Comparable<T>>) y usar los wildcards? extendsy? super. - Explicar el type erasure y las restricciones que impone.
- Implementar una colección genérica propia con sus pruebas.
Apuntes de la semana: notes/week-10.md · Código: code/week-10
Sesión A — Genéricos: clases y métodos
Lecturas:
- Eck, subsec. 10.1.3 Generic Programming in Java (y, como contexto, 10.1.1 y 10.1.2).
- Eck, subsec. 10.1.7 Generics and Wrapper Classes.
- Eck, sec. 10.5 Writing Generic Classes and Methods, subsecciones 10.5.1 y 10.5.2.
Lecturas complementarias:
- dev.java, Generics: Introducing Generics y Type Inference.
Temas:
- El problema: una colección de
Objectacepta cualquier cosa y obliga a hacer casting al sacar; el error aparece en ejecución (ClassCastException). Con genéricos el compilador lo detecta. - Clase genérica:
class Box<T>,record Pair<A, B>(A first, B second)(ejemplo propio). ElArrayList<Song>de la semana 4, ahora visto por dentro. - Método genérico:
static <T> T firstOrDefault(List<T> list, T fallback).Listes la interfaz que implementaArrayList; por ahora basta con eso, las interfaces de colecciones se estudian en la semana 11. Inferencia de tipos y el operador diamante<>. - Type parameters y tipos primitivos:
Box<int>no existe; boxing y unboxing, y su costo. - Convenciones de nombres:
T,E,K,V. - Error común: usar el raw type
ArrayListsin parámetro; compila con advertencias (-Xlint:unchecked) y devuelve el problema deObject.
Sesión B — Wildcards y type erasure
Lecturas:
- Eck, subsecs. 10.5.3 Wildcard Types y 10.5.4 Bounded Types.
- Eck, subsec. 10.1.3 Generic Programming in Java, relectura del párrafo sobre type erasure.
Lecturas complementarias:
- dev.java, Generics: Wildcards, Type Erasure y Restriction on Generics.
- JLS 25, §4.6 Type Erasure.
Temas:
- Invarianza:
List<Integer>no es subtipo deList<Number>aunqueIntegersea subtipo deNumber. Por qué: si lo fuera, se podría meter unDoubleen una lista de enteros. - Bounded types:
<T extends Comparable<T>> T max(List<T> list), usando elComparablede la semana 7. - Wildcards:
List<? extends Number>para leer (productor),List<? super Integer>para escribir (consumidor). Regla PECS (producer extends, consumer super) con un métodocopy(dst, src). - Type erasure: en el bytecode
Box<String>yBox<Integer>son la misma claseBox. Consecuencias: no haynew T(), ninew T[n], niinstanceof List<String>; no se puede hacer overloading de dos métodos que solo difieren en el type parameter. - Error común: llenar una API de wildcards sin necesidad. Primero tipos simples; el wildcard aparece cuando una prueba muestra que hace falta aceptar subtipos.
Laboratorio — Una colección genérica propia
Código inicial en code/week-10/: el esqueleto BoundedStack.java y los ejemplos de los apuntes (make test; make errores muestra lo que el compilador rechaza).
- Implementar una colección genérica pequeña con un arreglo interno: el stack acotado
BoundedStack<E>del esqueleto (o, como variante, un buffer circular), con su invariante y sus excepciones (IllegalStateExceptional sacar de uno vacío o al hacerpushen uno lleno). - Escribir las pruebas JUnit con dos tipos de elemento distintos y verificar que el compilador rechaza meter un tipo equivocado.
- Agregar un método genérico con bounded type (
max) y un método con wildcard (pushAll(List<? extends E>)) y probar que acepta una colección de un subtipo. - Mirar con
javap -cel bytecode de la clase y encontrar el casting que agregó el compilador.
La misma colección se vuelve iterable en la semana 11.
Entregas
- P3 en curso (Ventas). Entrega en semana 12. Donde el proyecto repita una misma estructura para distintos tipos (un repositorio de productos y otro de ventas, por ejemplo), una clase genérica evita el código duplicado.
Preguntas de repaso
- ¿Qué error detecta el compilador con
ArrayList<String>que no detecta con el raw typeArrayList? - ¿Por qué
List<Integer>no se puede pasar a un método que recibeList<Number>? ¿Qué firma lo permite? - Escriban la firma de un método que copia los elementos de una lista de
E(o de cualquier subtipo) a una lista deE(o de cualquier supertipo). - ¿Por qué no compila
new T[10]dentro de una clase genérica? ¿Cómo se resuelve en la práctica? - ¿Qué gana
<T extends Comparable<T>> T max(List<T> list)frente aComparable max(List list)?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (9)Errores (5)Pruebas (3)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
/**
* Laboratorio de la semana 10: un stack acotado genérico.
* Esqueleto: completen los métodos, su invariante y sus pruebas (BoundedStackTest.java).
*/
public class BoundedStack<E> {
/** Crea un stack vacío con capacidad para capacity elementos (capacity > 0). */
public BoundedStack(int capacity) {
// TODO: validar capacity y crear el arreglo interno (ver los apuntes: new E[n] no compila)
}
/** Pone x arriba. Lanza IllegalStateException si el stack está lleno. */
public void push(E x) {
throw new UnsupportedOperationException("por implementar");
}
/** Saca y devuelve el elemento de arriba. Lanza IllegalStateException si está vacío. */
public E pop() {
throw new UnsupportedOperationException("por implementar");
}
/** El elemento de arriba, sin sacarlo. Lanza IllegalStateException si está vacío. */
public E peek() {
throw new UnsupportedOperationException("por implementar");
}
public int size() {
throw new UnsupportedOperationException("por implementar");
}
public boolean isEmpty() {
return size() == 0;
}
public boolean isFull() {
throw new UnsupportedOperationException("por implementar");
}
}/**
* Una caja con un type parameter T: guarda a lo más un valor de tipo T.
* El compilador sabe qué tipo entra y qué tipo sale.
*/
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;
}
}import java.util.ArrayList;
import java.util.List;
/** Clases y métodos genéricos, inferencia y boxing. */
public class BoxDemo {
public static void main(String[] args) {
Box<String> city = new Box<>(); // diamante: T = String se infiere
city.put("Lima");
String s = city.get(); // sin casting
System.out.println("caja: " + s + ", vacía: " + city.isEmpty());
Pair<String, Integer> stock = new Pair<>("arroz", 40);
System.out.println(stock + " -> " + stock.swap());
List<String> empty = new ArrayList<>();
List<String> names = List.of("alice", "bob");
System.out.println(ListUtils.firstOrDefault(names, "nadie"));
System.out.println(ListUtils.firstOrDefault(empty, "nadie"));
// Box<int> no existe: se usa Integer y Java convierte solo (boxing/unboxing).
Box<Integer> count = new Box<>();
count.put(7); // boxing: 7 -> Integer.valueOf(7)
int n = count.get() + 1; // unboxing: Integer -> int
System.out.println("n = " + n);
// == entre Integer compara referencias, no valores.
Integer a = 127, b = 127, c = 1000, d = 1000;
System.out.println("127 == 127: " + (a == b) + ", 1000 == 1000: " + (c == d)
+ ", equals: " + c.equals(d));
}
}import java.util.List;
/** Type erasure y la diferencia con los arreglos. */
public class ErasureDemo {
public static void main(String[] args) {
// En ejecución, Box<String> y Box<Integer> son la misma clase.
Box<String> a = new Box<>();
Box<Integer> b = new Box<>();
System.out.println(a.getClass() + " == " + b.getClass() + ": "
+ (a.getClass() == b.getClass()));
// Los arreglos sí recuerdan su tipo y son covariantes:
// un Integer[] se puede ver como Number[], y el error aparece en ejecución.
Integer[] ints = {1, 2, 3};
Number[] nums = ints; // compila
try {
nums[0] = 2.5; // un Double en un arreglo de Integer
} catch (ArrayStoreException e) {
System.out.println("ArrayStoreException: " + e.getMessage());
}
// Con listas el compilador no deja llegar hasta ahí (ver errores/Invariance.java).
List<Integer> list = List.of(1, 2, 3);
System.out.println("list: " + list);
}
}import java.util.List;
/** Métodos genéricos sobre listas. */
public final class ListUtils {
private ListUtils() { } // solo métodos estáticos: no se crean objetos
/** 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);
}
/** 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;
}
/** 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;
}
/** 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
}
}
/** 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);
}
}
}/** Una caja que guarda cualquier cosa: así se escribían las colecciones antes de los genéricos. */
public class ObjectBox {
private Object content;
public void put(Object content) {
this.content = content;
}
public Object get() {
return content;
}
}/** Un par inmutable de dos valores, cada uno con su propio tipo. */
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);
}
}/** El problema de guardar Object: el error aparece recién al sacar, en ejecución. */
public class RawProblem {
public static void main(String[] args) {
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
try {
String other = (String) box.get(); // el error aparece aquí, en ejecución
System.out.println("otra: " + other);
} catch (ClassCastException e) {
System.out.println("ClassCastException: " + e.getMessage());
}
}
}import java.util.ArrayList;
import java.util.List;
/** Bounded types, wildcards y la regla PECS. */
public class WildcardDemo {
public static void main(String[] args) {
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));
// sum acepta List<Integer> y List<Double> gracias a ? extends Number.
List<Double> prices = List.of(2.5, 4.0);
System.out.println("sum: " + ListUtils.sum(marks) + ", " + ListUtils.sum(prices));
// addCounting acepta List<Integer>, List<Number> y List<Object>.
List<Number> numbers = new ArrayList<>();
ListUtils.addCounting(numbers, 3);
numbers.add(0.5);
System.out.println("numbers: " + numbers);
// copy: de una lista de Integer (productor) a una de Object (consumidor).
List<Object> all = new ArrayList<>(List.of("inicio"));
ListUtils.copy(all, marks);
System.out.println("all: " + all);
}
}Errores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
import java.util.List;
// Lo que el type erasure no permite.
public class ErasureLimits<T> {
T make() {
return new T();
}
T[] array(int n) {
return new T[n];
}
boolean isStrings(Object o) {
return o instanceof List<String>;
}
void print(List<String> list) { }
void print(List<Integer> list) { }
}import java.util.List;
// En una List<? extends Number> se puede leer, pero no escribir.
public class ExtendsReadOnly {
static void addOne(List<? extends Number> numbers) {
Number first = numbers.get(0);
numbers.add(1);
}
}import java.util.ArrayList;
import java.util.List;
// List<Integer> no es subtipo de List<Number>.
public class Invariance {
static double total(List<Number> numbers) {
double t = 0;
for (Number n : numbers) {
t += n.doubleValue();
}
return t;
}
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>(List.of(1, 2, 3));
List<Number> nums = ints;
System.out.println(total(ints));
}
}import java.util.ArrayList;
// Raw type: compila, pero con advertencias, y vuelve el problema de Object.
public class RawList {
public static void main(String[] args) {
ArrayList names = new ArrayList();
names.add("alice");
names.add(42);
String first = (String) names.get(1);
System.out.println(first);
}
}// Con genéricos, meter un tipo equivocado es un error de compilación.
public class WrongType {
public static void main(String[] args) {
Box<String> city = new Box<>();
city.put(42);
}
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class BoxTest {
@Test
void newBoxIsEmpty() {
Box<String> box = new Box<>();
assertTrue(box.isEmpty());
}
@Test
void returnsWhatWasPut() {
Box<String> box = new Box<>();
box.put("Lima");
assertFalse(box.isEmpty());
assertEquals("Lima", box.get());
}
@Test
void worksWithOtherTypes() {
Box<Integer> box = new Box<>();
box.put(7);
int n = box.get();
assertEquals(7, n);
}
}import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
class ListUtilsTest {
@Test
void firstOrDefaultReturnsFirst() {
assertEquals("alice", ListUtils.firstOrDefault(List.of("alice", "bob"), "nadie"));
}
@Test
void firstOrDefaultOnEmptyReturnsFallback() {
assertEquals("nadie", ListUtils.firstOrDefault(new ArrayList<String>(), "nadie"));
}
@Test
void maxWorksWithIntegersAndStrings() {
assertEquals(18, ListUtils.max(List.of(15, 18, 11)));
assertEquals("uva", ListUtils.max(List.of("pera", "uva", "mango")));
}
@Test
void maxOfEmptyListIsRejected() {
assertThrows(IllegalArgumentException.class, () -> ListUtils.max(new ArrayList<Integer>()));
}
@Test
void sumAcceptsAnyNumberList() {
assertEquals(44.0, ListUtils.sum(List.of(15, 18, 11)));
assertEquals(6.5, ListUtils.sum(List.of(2.5, 4.0)));
}
@Test
void addCountingWritesIntoNumberList() {
List<Number> out = new ArrayList<>();
ListUtils.addCounting(out, 3);
assertEquals(List.of(1, 2, 3), out);
}
@Test
void copyFromIntegersToObjects() {
List<Object> dst = new ArrayList<>(List.of("inicio"));
ListUtils.copy(dst, List.of(1, 2));
assertEquals(List.of("inicio", 1, 2), dst);
}
}import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class PairTest {
@Test
void keepsBothComponents() {
Pair<String, Integer> p = new Pair<>("arroz", 40);
assertEquals("arroz", p.first());
assertEquals(40, p.second());
}
@Test
void swapExchangesTypesAndValues() {
Pair<Integer, String> q = new Pair<>("arroz", 40).swap();
assertEquals(40, q.first());
assertEquals("arroz", q.second());
}
@Test
void pairsWithSameValuesAreEqual() {
assertEquals(new Pair<>("a", 1), new Pair<>("a", 1));
}
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 10: genéricos, wildcards y type erasure.
# make compila todo a build/
# make test corre las demostraciones y las pruebas JUnit
# make errores compila los ejemplos de errores/, que deben fallar (o advertir)
# make clean borra build/
JAVAC = javac
JFLAGS = -Xlint:all -Werror
SRC = $(wildcard *.java)
DEMOS = RawProblem BoxDemo WildcardDemo ErasureDemo
ERRORS = $(wildcard errores/*.java)
all: build/.stamp
build/.stamp: $(SRC)
mkdir -p build
$(JAVAC) $(JFLAGS) -cp $(JUNIT_JAR) -d build $(SRC)
touch $@
test: all
@for d in $(DEMOS); do echo "== $$d"; java -cp build $$d || exit 1; done
java -jar $(JUNIT_JAR) execute --class-path build --scan-class-path \
--disable-banner --disable-ansi-colors --details=tree
# Cada archivo de errores/ debe ser rechazado por javac -Xlint:all -Werror.
errores: all
@for f in $(ERRORS); do \
echo "== $$f"; \
if $(JAVAC) $(JFLAGS) -cp build -d build/errores $$f; then \
echo "se esperaba un error y compiló"; exit 1; \
fi; \
done
clean:
rm -rf build
.PHONY: all test errores clean