Cómo funcionan los wildcards de un genérico

Cuando trabajamos con genéricos en Java, es común encontrarnos con un símbolo curioso dentro de los diamantes: el signo de interrogación. Este símbolo, conocido como wildcard, representa un tipo genérico desconocido y nos permite manejar colecciones de manera flexible sin romper el sistema de tipos del lenguaje.

Aunque a primera vista pueda parecer extraño usar un signo de interrogación como tipo, su comportamiento es similar al de Object. Por ejemplo, si tenemos una lista con wildcard, podemos recorrer sus elementos como si fueran objetos, accediendo a métodos comunes como toString(), hashCode() o equals(). Sin embargo, no podemos tratarlos directamente como un tipo específico, como Integer, porque el compilador no permite hacer ese casteo.

Una pregunta natural es por qué no usar simplemente Object en lugar de un wildcard. La respuesta está en cómo Java maneja los genéricos y su sistema de tipos. Si definimos un método que recibe una lista de Object, no podremos pasarle una lista de Empleado, aunque Empleado sea una subclase de Object. Esto se debe a que List<Empleado> no es un subtipo de List<Object>. Por lo tanto, usar Object limita la flexibilidad y puede hacer que nuestro código no compile.

Veamos un ejemplo para ilustrar esto:

class Empleado {
    private int id;
    private double sueldo;

    public Empleado(int id, double sueldo) {
        this.id = id;
        this.sueldo = sueldo;
    }

    public int getID() {
        return id;
    }

    public double getSueldo() {
        return sueldo;
    }
}

public class Contabilidad {
    public static void imprimir(List<?> lista) {
        for (Object obj : lista) {
            System.out.println(obj.toString());
        }
    }

    public static void main(String[] args) {
        List<Empleado> empleados = new ArrayList<>();
        empleados.add(new Empleado(1, 1000));
        empleados.add(new Empleado(2, 1500));

        imprimir(empleados); // Funciona porque usamos wildcard
    }
}

En este código, el método imprimir acepta una lista con wildcard, lo que significa que puede recibir una lista de cualquier tipo. Si intentáramos cambiar el parámetro a List<Object>, el compilador nos impediría pasar una lista de Empleado.

Sin embargo, usar un wildcard sin restricciones también tiene sus inconvenientes. Por ejemplo, podríamos pasar una lista de Integer a un método que espera una lista de wildcard, aunque no tenga sentido en nuestro dominio de problema. Para evitar esto, Java nos permite acotar el wildcard usando la palabra clave extends, limitando el tipo a una jerarquía específica.

Así, si queremos que nuestro método acepte listas de Empleado o de cualquier subclase de Empleado, podemos escribir:

public static void imprimir(List<? extends Empleado> lista) {
    for (Empleado e : lista) {
        System.out.println("ID: " + e.getID() + ", Sueldo: " + e.getSueldo());
    }
}

Con esta restricción, el wildcard solo acepta tipos que extienden de Empleado, lo que nos permite acceder a los métodos específicos de esa clase sin problemas. Además, ahora podemos pasar listas de Empleado, Programador o Manager (siempre que estas clases extiendan de Empleado) sin que el compilador se queje.

Es importante destacar que, aunque Programador y Manager sean subclases de Empleado, no podemos pasar directamente una lista de Programador a un método que espera una lista de Empleado. Esto se debe a que List<Programador> no es un subtipo de List<Empleado>. Pero si usamos wildcard con extends, esta restricción desaparece y el código compila correctamente.

Por otro lado, Java también ofrece la palabra clave super para acotar wildcards por abajo, es decir, para aceptar tipos que sean superclases de un determinado tipo. Aunque este concepto es un poco más avanzado y no entraremos en detalle aquí, es útil para casos donde queremos aceptar tipos más generales en lugar de más específicos.

En resumen, el wildcard en genéricos nos permite escribir código más flexible y seguro, respetando las reglas del sistema de tipos de Java. Usar ? sin restricciones es como decir acepto cualquier tipo, mientras que usar ? extends Clase nos permite limitar ese cualquier tipo a una jerarquía concreta, facilitando el acceso a métodos específicos y evitando errores de compilación. Este conocimiento es fundamental para trabajar con colecciones genéricas y aprovechar al máximo las capacidades del lenguaje.

Lista de reproducción
  1. 1
    ¿Qué es un genérico?
    8 minutos
  2. 2
    Genéricos en Java: cómo crear tu propia clase
    7 minutos
  3. 3
    Cómo funcionan los wildcards de un genérico
    9 minutos