Saltearse al contenido

Comodines en genéricos en Java

Supongamos que tenemos una clase genérica Caja que puede contener cualquier tipo de objeto:

1
public class Caja<T> {
2
private T objeto;
3
4
public void set(T obj) {
5
objeto = obj;
6
}
7
8
public T get() {
9
return objeto;
10
}
11
}

Ahora, queremos escribir un método que pueda inspeccionar el contenido de cualquier Caja:

1
public static void inspeccionarCaja(Caja<Object> caja) {
2
Object objeto = caja.get();
3
// Inspeccionar objeto con algún código
4
}

Sin embargo, este enfoque es demasiado restrictivo. El método inspeccionarCaja solo puede aceptar Caja<Object>, pero no cajas con tipos más específicos, como Caja<String> o Caja<Integer>.

Para solucionar esto, podemos usar un comodín:

1
public static void inspeccionarCaja(Caja<?> caja) {
2
Object objeto = caja.get();
3
// Inspeccionar objeto
4
}

El ? es un comodín que representa un tipo desconocido. Caja<?> significa “una Caja de algún tipo”. Ahora, inspeccionarCaja puede aceptar cualquier tipo de Caja.

Pero, hay una restricción: no podemos insertar objetos en una Caja<?> porque no conocemos su tipo exacto:

1
Caja<?> caja = new Caja<>();
2
caja.set(new Object()); // Error de compilación

Si intentamos caja.set(new Object()), obtendremos un error de compilación porque Object podría no ser compatible con el tipo desconocido ?.

Sin embargo, sí podemos leer de una Caja<?> y obtener un Object:

1
Object objeto = caja.get(); // Correcto

Esto es seguro porque sabemos que caja contiene algún tipo de objeto.

Claro, aquí está la última sección con subsecciones para límites superiores e inferiores, y ejemplos más consistentes:

Comodines delimitados

En ocasiones, tenemos más información sobre el tipo desconocido representado por el comodín. Podemos aplicar límites o restricciones a ese tipo desconocido utilizando las palabras clave extends o super.

Límite superior (Upper Bound)

Podemos restringir el comodín a ser un subtipo de un tipo específico usando extends. Esto se conoce como un límite superior.

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

1
class Vehiculo {}
2
class Automovil extends Vehiculo {}
3
class Motocicleta extends Vehiculo {}

Podemos escribir un método que opere en cualquier Caja que contenga un Vehiculo o un subtipo de Vehiculo:

1
public static void inspeccionarCajaVehiculo(Caja<? extends Vehiculo> caja) {
2
Vehiculo vehiculo = caja.get();
3
// Trabajar con vehiculo
4
}

Aquí, ? extends Vehiculo significa “algún tipo que es Vehiculo o uno de sus subtipos”. Esto permite que inspeccionarCajaVehiculo acepte Caja<Vehiculo>, Caja<Automovil>, Caja<Motocicleta>, etc.

Con este límite superior, sabemos que el contenido de la caja será compatible con Vehiculo, por lo que podemos asignarlo a una variable de tipo Vehiculo sin problemas.

Límite inferior (Lower Bound)

De manera similar, podemos restringir el comodín a ser un súper tipo de un tipo específico usando super. Esto se conoce como un límite inferior.

Retomando el ejemplo anterior, podemos escribir un método que acepte cajas cuyos contenidos sean Motocicleta o un súper tipo de Motocicleta (como Vehiculo u Object):

1
public static void inspeccionarCajaObjeto(Caja<? super Motocicleta> caja) {
2
Object objeto = caja.get();
3
// Trabajar con objeto
4
}

? super Motocicleta significa “algún tipo que es Motocicleta o un súper tipo de Motocicleta (como Vehiculo u Object)“. Entonces, inspeccionarCajaObjeto puede aceptar Caja<Motocicleta>, Caja<Vehiculo> o Caja<Object>.

Con este límite inferior, sabemos que el contenido de la caja será un súper tipo de Motocicleta, por lo que es seguro asignarlo a una variable de tipo Object.

Pautas para el uso de comodines

Uno de los aspectos más confusos al aprender a programar con genéricos es determinar cuándo usar un comodín delimitado superiormente (upper bounded) y cuándo usar un comodín delimitado inferiormente (lower bounded). Acá podemos analizar algunas pautas a seguir al diseñar nuestro código.

Para esta discusión, es útil pensar en las variables como proveedoras de una de dos funciones:

  • Una variable “In” (entrada). Una variable “in” proporciona datos al código. Imaginemos un método copiar con dos argumentos: copiar(src, dest). El argumento src proporciona los datos a copiar, por lo que es el parámetro “in”.

  • Una variable “Out” (salida). Una variable “out” mantiene datos para ser utilizados en otro lugar. En el ejemplo copiar(src, dest), el argumento dest acepta datos, por lo que es el parámetro “out”.

Por supuesto, algunas variables se utilizan tanto para propósitos “in” como “out”. Este escenario también se aborda en las pautas.

Podemos usar el principio “in” y “out” al decidir si usar un comodín y qué tipo de comodín es apropiado. La siguiente lista proporciona las pautas a seguir:

  • Una variable “in” se define con un comodín delimitado superiormente, utilizando la palabra clave extends.
  • Una variable “out” se define con un comodín delimitado inferiormente, utilizando la palabra clave super.
  • En el caso de que la variable “in” pueda ser accedida usando métodos definidos en la clase Object, usamos un comodín no delimitado.
  • En el caso de que el código necesite acceder a la variable como “in” y “out”, no usamos un comodín.

Estas pautas no se aplican al tipo de retorno de un método. Se debe evitar usar un comodín como tipo de retorno porque obliga a los programadores que usan el código a lidiar con comodines.

Ejemplos

Consideremos la clase Caja que vimos anteriormente:

1
public class Caja<T> {
2
private T objeto;
3
4
public void set(T obj) {
5
objeto = obj;
6
}
7
8
public T get() {
9
return objeto;
10
}
11
}

Siguiendo las pautas, si tenemos un método que llena una caja, la caja sería una variable “out”, por lo que deberíamos usar un comodín delimitado inferiormente:

1
public static <T> void llenarCaja(Caja<? super T> caja, T objeto) {
2
caja.set(objeto);
3
}

Aquí, ? super T significa “algún tipo que sea un súper tipo de T”. Este método puede aceptar cualquier Caja cuyo tipo de contenido sea un súper tipo del tipo de objeto.

Por otro lado, si tenemos un método que inspecciona el contenido de una caja, la caja sería una variable “in”, por lo que deberíamos usar un comodín delimitado superiormente:

1
public static void inspeccionarCaja(Caja<? extends Object> caja) {
2
Object obj = caja.get();
3
// Inspeccionar obj
4
}

Aquí, ? extends Object significa “algún tipo que sea Object o uno de sus subtipos”. Este método puede aceptar cualquier Caja cuyo tipo de contenido sea un subtipo de Object (es decir, cualquier tipo).

Si la variable se usa tanto para “in” como para “out”, no deberíamos usar un comodín:

1
public static <T> void procesamientoCaja(Caja<T> caja) {
2
T obj = caja.get(); // "in"
3
caja.set(obj); // "out"
4
// Procesar obj
5
}

En este caso, T es un parámetro de tipo formal, lo que nos permite acceder y modificar el contenido de la caja de manera segura.