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 ? extends y ? 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:

Lecturas complementarias:

Temas:

  • El problema: una colección de Object acepta 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). El ArrayList<Song> de la semana 4, ahora visto por dentro.
  • Método genérico: static <T> T firstOrDefault(List<T> list, T fallback). List es la interfaz que implementa ArrayList; 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 ArrayList sin parámetro; compila con advertencias (-Xlint:unchecked) y devuelve el problema de Object.

Sesión B — Wildcards y type erasure

Lecturas:

Lecturas complementarias:

Temas:

  • Invarianza: List<Integer> no es subtipo de List<Number> aunque Integer sea subtipo de Number. Por qué: si lo fuera, se podría meter un Double en una lista de enteros.
  • Bounded types: <T extends Comparable<T>> T max(List<T> list), usando el Comparable de 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étodo copy(dst, src).
  • Type erasure: en el bytecode Box<String> y Box<Integer> son la misma clase Box. Consecuencias: no hay new T(), ni new T[n], ni instanceof 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).

  1. 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 (IllegalStateException al sacar de uno vacío o al hacer push en uno lleno).
  2. Escribir las pruebas JUnit con dos tipos de elemento distintos y verificar que el compilador rechaza meter un tipo equivocado.
  3. 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.
  4. Mirar con javap -c el 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

  1. ¿Qué error detecta el compilador con ArrayList<String> que no detecta con el raw type ArrayList?
  2. ¿Por qué List<Integer> no se puede pasar a un método que recibe List<Number>? ¿Qué firma lo permite?
  3. Escriban la firma de un método que copia los elementos de una lista de E (o de cualquier subtipo) a una lista de E (o de cualquier supertipo).
  4. ¿Por qué no compila new T[10] dentro de una clase genérica? ¿Cómo se resuelve en la práctica?
  5. ¿Qué gana <T extends Comparable<T>> T max(List<T> list) frente a Comparable 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.

BoundedStack.javaDescargar BoundedStack.java
/**
 * 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");
    }
}
Box.javaDescargar Box.java
/**
 * 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;
    }
}
BoxDemo.javaDescargar BoxDemo.java
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));
    }
}
ErasureDemo.javaDescargar ErasureDemo.java
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);
    }
}
ListUtils.javaDescargar ListUtils.java
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);
        }
    }
}
ObjectBox.javaDescargar ObjectBox.java
/** 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;
    }
}
Pair.javaDescargar Pair.java
/** 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);
    }
}
RawProblem.javaDescargar RawProblem.java
/** 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());
        }
    }
}
WildcardDemo.javaDescargar WildcardDemo.java
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.

errores/ErasureLimits.javaDescargar errores/ErasureLimits.java
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) { }
}
errores/ExtendsReadOnly.javaDescargar errores/ExtendsReadOnly.java
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);
    }
}
errores/Invariance.javaDescargar errores/Invariance.java
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));
    }
}
errores/RawList.javaDescargar errores/RawList.java
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);
    }
}
errores/WrongType.javaDescargar errores/WrongType.java
// 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.

BoxTest.javaDescargar BoxTest.java
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);
    }
}
ListUtilsTest.javaDescargar ListUtilsTest.java
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);
    }
}
PairTest.javaDescargar PairTest.java
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.

MakefileDescargar Makefile
# 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