Saltearse al contenido

Subtipado en genéricos en Java

Vamos a analizar un caso con genéricos. ¿Es legal el siguiente fragmento de código?

1
List<String> listaString = new ArrayList<String>();
2
List<Object> listaObject = listaString;

La línea 1 ciertamente es legal. La parte más complicada de la pregunta es la línea 2. Esto se reduce a la pregunta: ¿es una List de String una List de Object? La mayoría responde instintivamente: “¡Claro!”

Bien, ahora veamos las siguientes líneas:

1
listaObject.add(new Object()); // 3
2
String s = listaString.get(0); // 4: Intenta asignar un Object a un String!

Aquí hemos aliado listaString y listaObject. Accediendo a listaString, una lista de String, a través del alias listaObject, podemos insertar objetos arbitrarios en ella. Como resultado, listaString ya no contiene solo Strings, y cuando intentamos obtener algo de ella, obtenemos algo inesperado.

Por supuesto, el compilador de Java evitará que esto suceda. La línea 2 causará un error de compilación. El error nos dice “Type mismatch: cannot convert from List<String> to List<Object>”.

En general, si Foo es un subtipo (subclase o subinterfaz) de Bar, y G es alguna declaración de tipo genérico, no es cierto que G<Foo> sea un subtipo de G<Bar>. Probablemente esta es lo más difícil que debemos aprender sobre los genéricos, porque va en contra de nuestras profundas intuiciones.

No debemos asumir que las colecciones no cambian. Nuestra intuición puede llevarnos a pensar en estas cosas como inmutables.

Por ejemplo, si el departamento de vehículos a motor brinda una lista de conductores al censo, esto parece razonable. Pensamos que una List<Conductor> es una List<Persona>, asumiendo que Conductor es un subtipo de Persona. De hecho, lo que se está pasando es una copia del registro de conductores. De lo contrario, el censo podría agregar nuevas personas que no son conductores a la lista, corrompiendo los registros.

Ejemplo

Supongamos que tenemos una jerarquía de clases para representar diferentes tipos de Empleados:

1
public class Empleado { /*...*/ }
2
3
public class Ingeniero extends Empleado { /*...*/ }
4
5
public class Gerente extends Empleado { /*...*/ }

Si intentamos hacer lo siguiente:

1
List<Empleado> empleados = new ArrayList<>();
2
empleados.add(new Ingeniero());
3
empleados.add(new Gerente());
4
5
List<Ingeniero> ingenieros = empleados; // Error de compilación

Obtendremos un error de compilación en la última línea. Esto se debe a que List<Ingeniero> no es un subtipo de List<Empleado>, a pesar de que Ingeniero es un subtipo (subclase) de Empleado. Esto podría causar que, por ejemplo como en la línea 3, se agregue un Gerente a la lista de Ingeniero, lo cual no es correcto.

Para lidiar con este tipo de situaciones, es útil considerar tipos genéricos más flexibles, utilizando comodines (wildcards).