Comodines en genéricos en Java
Supongamos que tenemos una clase genérica Caja
que puede contener cualquier tipo de objeto:
1public 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
:
1public static void inspeccionarCaja(Caja<Object> caja) {2 Object objeto = caja.get();3 // Inspeccionar objeto con algún código4}
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:
1public static void inspeccionarCaja(Caja<?> caja) {2 Object objeto = caja.get();3 // Inspeccionar objeto4}
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:
1Caja<?> caja = new Caja<>();2caja.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
:
1Object 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:
1class Vehiculo {}2class Automovil extends Vehiculo {}3class Motocicleta extends Vehiculo {}
Podemos escribir un método que opere en cualquier Caja
que contenga un Vehiculo
o un subtipo de Vehiculo
:
1public static void inspeccionarCajaVehiculo(Caja<? extends Vehiculo> caja) {2 Vehiculo vehiculo = caja.get();3 // Trabajar con vehiculo4}
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
):
1public static void inspeccionarCajaObjeto(Caja<? super Motocicleta> caja) {2 Object objeto = caja.get();3 // Trabajar con objeto4}
? 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 argumentosrc
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 argumentodest
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:
1public 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:
1public 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:
1public static void inspeccionarCaja(Caja<? extends Object> caja) {2 Object obj = caja.get();3 // Inspeccionar obj4}
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:
1public static <T> void procesamientoCaja(Caja<T> caja) {2 T obj = caja.get(); // "in"3 caja.set(obj); // "out"4 // Procesar obj5}
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.