Blog

Genéricos en Java: borrado de tipos, límites y comodines

Los genéricos permiten que el compilador revise lo que entra en una lista, una caja o un método, y luego borran esa información antes de que el programa se ejecute. Aprende límites, comodines, PECS y a leer una firma del JDK con programas pequeños.

Los genéricos te permiten escribir List<String> en lugar de una lista de cualquier cosa, así que el compilador revisa lo que entra y nunca haces cast de lo que sale. También son la razón de que cierto código perfectamente razonable no compile: no puedes escribir new T(), y una List<Integer> no es una List<Number>. Las dos reglas tienen sentido cuando ves qué hace el compilador con el tipo después de revisarlo.

Este post cubre el bug que resolvieron los genéricos, las clases, records y métodos genéricos, los tipos con límite, el borrado de tipos, los comodines y la regla PECS, las interfaces genéricas, los primitivos y cómo leer una firma como la de Collections.max. Cada programa de abajo se ejecutó en Java 25, y su salida está copiada de esa ejecución. Para ejecutar uno tú mismo, guárdalo como Main.java y ejecuta java Main.java.

Por qué genéricos: una lista de Object falla tarde

Antes de Java 5, una List guardaba Object, así que podías meter cualquier cosa y tenías que hacer cast de todo lo que salía. Así se ve ese estilo, que sigue siendo legal hoy:

@SuppressWarnings({"rawtypes", "unchecked"})
void main() {
    List names = new ArrayList();
    names.add("Ada");
    names.add(42);
    for (Object o : names) {
        String name = (String) o;
        IO.println(name.toUpperCase());
    }
}

Imprime y se detiene:

ADA
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')

names.add(42) es el bug, pero el programa se cae dos líneas después, en el cast. En un proyecto real, el add y el cast pueden estar en archivos distintos, escritos por personas distintas, con meses de diferencia. La excepción te dice dónde se encontró el valor equivocado, no dónde se puso.

Una List sin argumento de tipo se llama raw type (tipo sin parametrizar). Cada programa de esta serie se compila con javac -Xlint:all -Werror, y por eso está ahí la línea @SuppressWarnings. Si la quitas, la compilación se detiene:

void main() {
    List names = new ArrayList();
    names.add("Ada");
    names.add(42);
    IO.println(names.size());
}

La compilación falla con:

Main.java:2: warning: [rawtypes] found raw type: List
Main.java:2: warning: [rawtypes] found raw type: ArrayList
Main.java:3: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
Main.java:4: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
error: warnings found and -Werror specified

Sin -Werror son solo advertencias, y java Main.java ejecuta el código de todos modos. Ahora dale a la lista un argumento de tipo:

void main() {
    List<String> names = new ArrayList<>();
    names.add("Ada");
    names.add(42);
    for (String name : names) {
        IO.println(name.toUpperCase());
    }
}

La compilación falla con:

Main.java:4: error: incompatible types: int cannot be converted to String
    names.add(42);
              ^

Ahora el error señala la línea que de verdad está mal, y nada llega a ejecutarse. El bucle no necesita cast, porque el compilador ya sabe que cada elemento es un String. El <> vacío en new ArrayList<>() es el operador diamante (diamond): el compilador copia el argumento de tipo del lado izquierdo.

Clases y records genéricos

Una clase genérica declara un parámetro de tipo entre corchetes angulares, y cada uso de la clase lo completa. T es un marcador de posición para un tipo, igual que un parámetro de método es un marcador de posición para un valor:

class Box<T> {
    private T value;

    Box(T value) {
        this.value = value;
    }

    T get() {
        return value;
    }

    void set(T value) {
        this.value = value;
    }
}

record Pair<A, B>(A first, B second) {}

void main() {
    Box<String> label = new Box<>("fragile");
    label.set("this side up");
    IO.println(label.get().length());

    var entry = new Pair<>("Ada", 36);
    String name = entry.first();
    int age = entry.second();
    IO.println(name + " is " + age);
    IO.println(entry);
}

Imprime:

12
Ada is 36
Pair[first=Ada, second=36]

label.get() devuelve un String, así que .length() funciona sin cast. Pair<A, B> tiene dos parámetros, y el record completa los dos a partir de los argumentos del constructor: Pair<String, Integer>. Por convención, los parámetros de tipo son una sola letra mayúscula: T de tipo, E de elemento, K y V de clave y valor.

Métodos genéricos e inferencia de tipos

Un método puede tener sus propios parámetros de tipo, declarados justo antes del tipo de retorno. El compilador deduce cuáles son a partir de los argumentos de cada llamada. Esto se llama inferencia de tipos:

static <T> T firstOrDefault(List<T> items, T fallback) {
    return items.isEmpty() ? fallback : items.getFirst();
}

void main() {
    String city = firstOrDefault(List.of("Lisbon", "Pune"), "nowhere");
    Integer score = firstOrDefault(List.of(), 0);
    IO.println(city);
    IO.println(score);

    var mixed = List.<Number>of(1, 2.5);
    mixed.forEach(n -> IO.println(n.doubleValue()));
}

Imprime:

Lisbon
0
1.0
2.5

En la primera llamada, T es String. En la segunda, List.of() está vacía, así que el compilador toma T del valor por defecto 0 y lo hace Integer.

List.<Number>of(1, 2.5) es un testigo de tipo explícito (explicit type witness): tú nombras T en lugar de dejar que el compilador lo infiera. Casi nunca lo necesitas. Probamos lo mismo con nuestro propio método, Main.<String>firstOrDefault(...), y javac respondió cannot find symbol para Main. La clase de un archivo fuente compacto no tiene un nombre que puedas escribir en el código, así que no puedes poner un testigo en sus métodos estáticos.

Tipos con límite: pedirle más a T

Una T simple podría ser cualquier tipo, así que los únicos métodos que puedes llamar sobre ella son los de Object. Para llamar a compareTo, tienes que decirle al compilador que T es algo comparable. Esto pasa si no lo haces:

static <T> T max(List<T> items) {
    T best = items.getFirst();
    for (T item : items) {
        if (item.compareTo(best) > 0) {
            best = item;
        }
    }
    return best;
}

void main() {
    IO.println(max(List.of(3, 9, 4)));
}

La compilación falla con:

Main.java:4: error: cannot find symbol
        if (item.compareTo(best) > 0) {
                ^

Un límite (bound) lo arregla. <T extends Comparable<T>> significa “cualquier T, siempre que una T se pueda comparar con otra T“. Aquí extends también se usa para interfaces:

static <T extends Comparable<T>> T max(List<T> items) {
    T best = items.getFirst();
    for (T item : items) {
        if (item.compareTo(best) > 0) {
            best = item;
        }
    }
    return best;
}

void main() {
    IO.println(max(List.of(3, 9, 4)));
    IO.println(max(List.of("pear", "apple", "plum")));
}

Imprime:

9
plum

Un parámetro de tipo puede tener varios límites, unidos con &. La clase, si hay una, va primero. <T extends Number & Comparable<T>> permite que un mismo método use doubleValue() de Number y compareTo de Comparable:

static <T extends Number & Comparable<T>> String summary(List<T> items) {
    T best = items.getFirst();
    double total = 0;
    for (T item : items) {
        total += item.doubleValue();
        if (item.compareTo(best) > 0) {
            best = item;
        }
    }
    return "max " + best + ", total " + total;
}

void main() {
    IO.println(summary(List.of(3, 9, 4)));
    IO.println(summary(List.of(1.5, 0.25)));
}

Imprime:

max 9, total 16.0
max 1.5, total 1.75

Ten este en mente. Nuestro max tiene un hueco con el que vamos a chocar al final del post, y Collections.max tiene una firma más rara justamente porque no tiene ese hueco.

Borrado de tipos: el tipo se revisa y luego se quita

Java revisa los tipos genéricos al compilar y luego los quita. En tiempo de ejecución, una List<String> y una List<Integer> son las dos simplemente ArrayList:

record Box<T>(T value) {}

record Ranked<T extends Comparable<T>>(T value) {}

void main() {
    List<String> words = new ArrayList<>();
    List<Integer> numbers = new ArrayList<>();
    IO.println(words.getClass() == numbers.getClass());
    IO.println(words.getClass().getName());

    IO.println(Box.class.getRecordComponents()[0].getType());
    IO.println(Ranked.class.getRecordComponents()[0].getType());
}

Imprime:

true
java.util.ArrayList
class java.lang.Object
interface java.lang.Comparable

Hay un solo objeto de clase para todas las listas. Nada en tiempo de ejecución registra si una lista era para strings o para números. Las dos últimas líneas muestran qué queda de T en el record compilado: una T simple se vuelve Object, y una T con límite se vuelve su primer límite, Comparable. Ese reemplazo se llama borrado de tipos (type erasure).

Explicado como si tuvieras diez años

Una caja genérica es una lonchera con una etiqueta en la tapa: “solo sándwiches”. Cuando la llenas, un adulto revisa la etiqueta. Si intentas meter un calcetín, te detiene ahí mismo.

Una vez llena la lonchera, arrancan la etiqueta antes de meterla en tu mochila. A la hora del almuerzo nadie revisa la etiqueta, porque ya no hay ninguna. Tampoco hace falta. La revisión ya se hizo, así que lo que hay dentro son sándwiches.

Por eso todas las loncheras de la mochila se ven iguales. No le puedes preguntar a una lonchera de la mochila “¿eras una caja de sándwiches?”, porque la respuesta se fue a la basura con la etiqueta.

La versión precisa

javac revisa cada uso de un tipo genérico contra sus argumentos de tipo. Después compila el código con cada parámetro de tipo reemplazado por su borrado, que es su primer límite, u Object si no tiene ninguno. Donde tu código lee una T y la usa como String, javac inserta un cast oculto. El bytecode de ArrayList es una sola clase, compartida por cada ArrayList<Whatever>.

Java eligió el borrado en Java 5 para que el código genérico y el código viejo no genérico pudieran ejecutarse juntos, usando los mismos archivos de clase. El costo es que el argumento de tipo no está ahí en tiempo de ejecución, así que ninguna operación que lo necesite en tiempo de ejecución se puede escribir.

Dónde falla la analogía: la etiqueta no siempre desaparece. Los tipos declarados de los campos, los parámetros de métodos y las superclases conservan sus firmas genéricas en el archivo de clase, y la reflexión puede leerlas. Lo que se pierde es el argumento de tipo de cada objeto: un ArrayList concreto no sabe que se creó como ArrayList<String>. Y al adulto se le puede engañar: un raw type o un cast no comprobado se salta la revisión, y entonces un calcetín sí puede terminar en la lonchera.

Lo que el borrado no permite

No puedes crear una T, porque en tiempo de ejecución no hay ninguna T a la que llamarle un constructor:

class Factory<T> {
    T make() {
        return new T();
    }
}

void main() {
    IO.println(new Factory<String>());
}

La compilación falla con:

Main.java:3: error: unexpected type
        return new T();
                   ^

Tampoco puedes crear un T[]. Un array guarda su tipo de elemento en tiempo de ejecución, y el borrado significa que no hay ninguno que guardar:

<T> T[] makeArray(int size) {
    return new T[size];
}

void main() {
    IO.println(makeArray(3).length);
}

La compilación falla con:

Main.java:2: error: generic array creation
    return new T[size];
           ^

La forma habitual de esquivar las dos cosas es pasar algo que sepa hacer la creación, como un Class<T> o un Supplier<T>.

Tampoco puedes preguntarle a un objeto si es una List<String>, porque el objeto no lo sabe:

void main() {
    Object thing = new ArrayList<String>();
    IO.println(thing instanceof List<String>);
}

La compilación falla con:

Main.java:3: error: Object cannot be safely cast to List<String>
    IO.println(thing instanceof List<String>);
               ^

Este nos sorprendió. El error no dice que los genéricos estén prohibidos en instanceof. Habla de seguridad. Si la variable es una Collection<String>, entonces thing instanceof List<String> compila y se ejecuta, porque cualquier cosa que pase la comprobación de verdad es una lista de strings. Eso llegó en Java 16: javac --release 15 lo rechaza. Desde Object, pregunta instanceof List<?> en su lugar.

Por último, dos sobrecargas que solo difieren en sus argumentos de tipo no pueden vivir en la misma clase:

void print(List<String> items) {
    IO.println("strings");
}

void print(List<Integer> items) {
    IO.println("integers");
}

void main() {
    print(List.of("a"));
}

La compilación falla con:

Main.java:5: error: name clash: print(List<Integer>) and print(List<String>) have the same erasure
void print(List<Integer> items) {
     ^

Después del borrado, los dos métodos son print(List), y un archivo de clase no puede tener dos métodos con el mismo nombre y los mismos parámetros. Dales nombres distintos.

Cuando un raw type o un cast no comprobado deja que una variable List<String> apunte a una lista que tiene un Integer, eso se llama contaminación del heap (heap pollution), y la ClassCastException aparece más tarde, lejos de la línea que la causó, como en el primer programa.

Comodines: List<Integer> no es una List<Number>

Un Integer es un Number, así que parece que una List<Integer> debería ser una List<Number>. El compilador dice que no:

void main() {
    List<Integer> ints = new ArrayList<>(List.of(1, 2));
    List<Number> nums = ints;
    nums.add(2.5);
    IO.println(ints);
}

La compilación falla con:

Main.java:3: error: incompatible types: List<Integer> cannot be converted to List<Number>
    List<Number> nums = ints;
                        ^

La línea 4 es la razón. Si la línea 3 estuviera permitida, nums.add(2.5) metería un Double en una lista que ints todavía promete que solo tiene enteros. Los tipos genéricos son invariantes: List<A> y List<B> no tienen relación, a menos que A y B sean el mismo tipo.

Los arrays tomaron la otra decisión, y lo pagan en tiempo de ejecución:

void main() {
    Integer[] ints = {1, 2};
    Number[] nums = ints;
    IO.println(nums[0]);
    nums[1] = 2.5;
}

Imprime y se detiene:

1
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Double

Los arrays son covariantes: un Integer[] es un Number[], así que la asignación compila. La misma escritura incorrecta falla después, al ejecutarse. Eso solo funciona porque un array recuerda su tipo de elemento, que es justo lo que los genéricos borrados no pueden hacer. Por eso los genéricos atrapan el error en tiempo de compilación.

? extends T: una lista de la que lees

La invarianza haría inútil un método sencillo. Un sum(List<Number>) no podría recibir una List<Integer>. Un comodín (wildcard) lo resuelve. List<? extends Number> significa “una lista de algún tipo que es Number o un subtipo”:

double sum(List<? extends Number> items) {
    double total = 0;
    for (Number n : items) {
        total += n.doubleValue();
    }
    return total;
}

void main() {
    IO.println(sum(List.of(1, 2, 3)));
    IO.println(sum(List.of(1.5, 2.5)));
}

Imprime:

6.0
4.0

Leer es seguro. Tenga lo que tenga la lista en realidad, cada elemento es algún tipo de Number. Escribir no lo es:

void main() {
    List<Integer> ints = new ArrayList<>();
    List<? extends Number> nums = ints;
    nums.add(2.5);
}

La compilación falla con:

Main.java:4: error: incompatible types: double cannot be converted to CAP#1
    nums.add(2.5);
             ^

CAP#1 es el nombre que javac le da a “el tipo desconocido detrás de este ?“. Podría ser Integer, podría ser Double, y el compilador no puede demostrar que 2.5 cabe, así que se niega. No puedes agregar nada salvo null a una List<? extends Number>.

? super T: una lista en la que escribes

List<? super Integer> significa “una lista de Integer o de uno de sus supertipos”. Podría ser una List<Integer>, una List<Number> o una List<Object>. Cualquiera de ellas puede guardar un Integer, así que agregar uno es seguro. Al leer solo obtienes Object, porque no sabes cuál de esas listas tienes.

Los dos comodines se encuentran en un método de copia, que lee de una lista y escribe en otra:

static <T> void copy(List<? super T> dst, List<? extends T> src) {
    for (T item : src) {
        dst.add(item);
    }
}

void main() {
    List<Integer> ints = List.of(1, 2);
    List<Double> doubles = List.of(2.5);
    List<Number> numbers = new ArrayList<>();
    List<Object> anything = new ArrayList<>(List.of("start"));

    copy(numbers, ints);
    copy(numbers, doubles);
    copy(anything, ints);
    IO.println(numbers);
    IO.println(anything);
}

Imprime:

[1, 2, 2.5]
[start, 1, 2]

Un solo método copia enteros en una List<Number> y en una List<Object>, y doubles en la misma List<Number>. Con copy(List<T> dst, List<T> src), solo compilaría la primera llamada, y solo si T fuera Integer para las dos listas, cosa que numbers no es. El propio Collections.copy del JDK tiene esta firma.

La regla práctica es PECS: producer extends, consumer super (el que produce usa extends, el que consume usa super). Si un parámetro produce valores que tu método lee, usa extends. Si consume valores que tu método escribe, usa super. Si hace las dos cosas, usa una T simple.

producer extends: salen datos, lees src: List<? extends T> for (T item : src) copy(dst, src) consumer super: entran datos, escribes copy(dst, src) dst.add(item) dst: List<? super T> agregar a src: no compila. leer de dst: solo da Object.

La lista de origen produce valores T, así que su parámetro usa extends y el método solo lee de ella. La lista de destino consume valores T, así que su parámetro usa super y el método solo escribe en ella.

? solo: cualquier lista

Un comodín sin límite, List<?>, significa “una lista de algún tipo que no me importa”. Úsalo cuando el método solo necesita cosas que funcionan con cualquier lista, como size() o imprimir:

String describe(List<?> items) {
    return items.size() + " items, first is " + items.getFirst();
}

void main() {
    IO.println(describe(List.of("a", "b")));
    IO.println(describe(List.of(3.5, 7.0, 1.0)));
}

Imprime:

2 items, first is a
3 items, first is 3.5

List<?> no es lo mismo que la List raw. El raw type apaga la revisión, así que puedes agregar cualquier cosa. List<?> mantiene la revisión, así que no puedes agregar nada salvo null, y javac no da ninguna advertencia.

Interfaces genéricas: Comparable<T> y un repositorio

Las interfaces también reciben parámetros de tipo, y la más usada del JDK es Comparable<T>. Implementar Comparable<Version> significa que un Version se puede comparar con otro Version, y entonces nuestro max con límite lo acepta:

record Version(int major, int minor) implements Comparable<Version> {
    @Override
    public int compareTo(Version other) {
        if (major != other.major) {
            return Integer.compare(major, other.major);
        }
        return Integer.compare(minor, other.minor);
    }
}

static <T extends Comparable<T>> T max(List<T> items) {
    T best = items.getFirst();
    for (T item : items) {
        if (item.compareTo(best) > 0) {
            best = item;
        }
    }
    return best;
}

void main() {
    var versions = List.of(new Version(21, 0), new Version(25, 1), new Version(25, 0));
    IO.println(max(versions));
}

Imprime:

Version[major=25, minor=1]

compareTo recibe un Version, no un Object, así que no hay cast ni instanceof dentro.

Tus propias interfaces pueden ser genéricas de la misma forma. Una muy común en código de negocio es el repositorio, que guarda y encuentra entidades por ID. Repository<T, ID> funciona con cualquier tipo de entidad y cualquier tipo de ID:

interface Repository<T, ID> {
    void save(ID id, T item);

    Optional<T> findById(ID id);

    List<T> findAll();
}

record User(String name) {}

class InMemoryRepository<T, ID> implements Repository<T, ID> {
    private final Map<ID, T> items = new LinkedHashMap<>();

    @Override
    public void save(ID id, T item) {
        items.put(id, item);
    }

    @Override
    public Optional<T> findById(ID id) {
        return Optional.ofNullable(items.get(id));
    }

    @Override
    public List<T> findAll() {
        return List.copyOf(items.values());
    }
}

void main() {
    Repository<User, Integer> users = new InMemoryRepository<>();
    users.save(1, new User("Ada"));
    users.save(2, new User("Linus"));
    IO.println(users.findById(2));
    IO.println(users.findById(7));
    IO.println(users.findAll());
}

Imprime:

Optional[User[name=Linus]]
Optional.empty
[User[name=Ada], User[name=Linus]]

users.save("one", ...) no compilaría, porque ID es Integer para este repositorio. Frameworks como Spring Data construyen sus repositorios sobre la misma idea. La parte sobre Optional cubre lo que devuelve findById.

Primitivos y genéricos: no existe List<int>

Un argumento de tipo tiene que ser un tipo de referencia, así que los primitivos como int no están permitidos:

void main() {
    List<int> scores = new ArrayList<>();
    IO.println(scores);
}

La compilación falla con:

Main.java:2: error: unexpected type
    List<int> scores = new ArrayList<>();
         ^

El borrado es la razón. Un parámetro de tipo se vuelve Object o un límite, y un int no es un objeto. Escribes List<Integer>, y Java hace boxing de cada int a un Integer al entrar y unboxing al salir. Es cómodo, y tiene una trampa en la que todos caen alguna vez:

void main() {
    List<Integer> scores = new ArrayList<>(List.of(1, 2, 3));
    scores.remove(1);
    IO.println(scores);
    scores.remove(Integer.valueOf(1));
    IO.println(scores);
}

Imprime:

[1, 3]
[3]

List tiene dos métodos remove, remove(int index) y remove(Object o). scores.remove(1) elige el de int, porque no necesita boxing, así que quita el elemento en el índice 1, que es 2. Si querías decir el valor 1, pasa un Integer, como hace la segunda llamada.

El boxing también cuesta memoria: una List<Integer> guarda una referencia a un objeto separado por cada número. El proyecto Valhalla trabaja en clases de valor (value classes) que reducirían esa diferencia, pero no forman parte de Java 25.

Leer una firma genérica del JDK

Las firmas genéricas del JDK se ven densas, pero cada pieza responde una pregunta, y ya viste todas las piezas. Esta es la declaración de Collections.max:

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)

Léela pieza por pieza:

  1. <T ...>: max es un método genérico con un parámetro de tipo, T.
  2. T extends ... Comparable<? super T>: una T tiene que poder compararse con T o con algún supertipo de T. Esto es PECS otra vez. La comparación consume una T, así que es super.
  3. Collection<? extends T> coll: el argumento produce valores T, así que es extends. Sirve cualquier colección cuyos elementos sean T o un subtipo, no solo una List.
  4. T antes de max: el método devuelve una T.
  5. Object &: el primer límite decide el borrado. Con él, el método borrado devuelve Object, exactamente como hacía max antes de Java 5, así que el código viejo ya compilado que lo llamaba sigue enlazando.

El paso 2 es el hueco de nuestro propio max. LocalDate no implementa Comparable<LocalDate>. Implementa Comparable<ChronoLocalDate>, una interfaz que está por encima. Nuestro <T extends Comparable<T>> no puede aceptar eso:

static <T extends Comparable<T>> T max(List<T> items) {
    T best = items.getFirst();
    for (T item : items) {
        if (item.compareTo(best) > 0) {
            best = item;
        }
    }
    return best;
}

void main() {
    var dates = List.of(LocalDate.of(2026, 3, 1), LocalDate.of(2026, 9, 14));
    IO.println(max(dates));
}

La compilación falla con:

Main.java:13: error: method max in class Main cannot be applied to given types;
  reason: inference variable T has incompatible equality constraints ChronoLocalDate,LocalDate

Collections.max acepta la misma lista, y la reflexión confirma el paso 5:

void main() throws NoSuchMethodException {
    var dates = List.of(LocalDate.of(2026, 3, 1), LocalDate.of(2026, 9, 14));
    IO.println(Collections.max(dates));

    var erased = Collections.class.getMethod("max", Collection.class);
    IO.println(erased.getReturnType());
}

Imprime:

2026-09-14
class java.lang.Object

Comparable<? super T> deja que T sea LocalDate mientras la comparación está definida en ChronoLocalDate. Si escribes un método de biblioteca que compara cosas, copia esa forma. getMethod("max", Collection.class) encuentra el método por su tipo de parámetro borrado, porque es el único tipo que queda en tiempo de ejecución.

Qué recordar

  • Los genéricos mueven los errores de tipo del tiempo de ejecución al tiempo de compilación. Una List raw trae de vuelta la ClassCastException tardía, y -Xlint:all avisa de ella.
  • Los parámetros de tipo van después del nombre de una clase (Box<T>) o antes del tipo de retorno de un método (<T> T first(...)). Normalmente el compilador los infiere.
  • Un límite, <T extends Comparable<T>>, te deja llamar los métodos del límite sobre una T. Varios límites se unen con &.
  • Los argumentos de tipo se borran después de revisarlos. Por eso no compilan new T(), new T[], instanceof List<String> desde Object ni las sobrecargas que solo difieren en los argumentos de tipo.
  • List<Integer> no es una List<Number>. Los arrays son covariantes y, en cambio, fallan en tiempo de ejecución con ArrayStoreException.
  • PECS: usa ? extends T para un parámetro del que lees, ? super T para uno en el que escribes, y ? cuando el tipo no importa.
  • Los argumentos de tipo tienen que ser tipos de referencia, así que int se convierte en Integer con boxing. Cuidado con remove(int) frente a remove(Object).

El compilador revisa el argumento de tipo y luego lo tira, así que cada regla de los genéricos trata de lo que puede demostrar antes de que eso pase.

¿Qué tan útil te resultó este post?

¡Haz clic en un corazón para calificar!

Calificación promedio 0 / 5. Total de votos: 0

Todavía no hay votos. Sé el primero en calificar este post.