CodeGym/Blog Java/Random-ES/Comodines en genéricos
Autor
Artem Divertitto
Senior Android Developer at United Tech

Comodines en genéricos

Publicado en el grupo Random-ES
¡Hola! Continuemos nuestro estudio de los genéricos. Ya ha adquirido una gran cantidad de conocimientos sobre ellos en lecciones anteriores (sobre el uso de varargs cuando se trabaja con genéricos y sobre el borrado de tipos ), pero hay un tema importante que aún no hemos considerado: los comodines . Esta es una característica muy importante de los genéricos. ¡Tanto es así que le hemos dedicado una lección aparte! Dicho esto, no hay nada particularmente complicado en los comodines. Lo verás enseguida :) Comodines en genéricos - 1Veamos un ejemplo:
public class Main {

   public static void main(String[] args) {

       String str = new String("Test!");
       // No problem
       Object obj = str;

       List<String> strings = new ArrayList<String>();
       // Compilation error!
       List<Object> objects = strings;
   }
}
¿Que está pasando aqui? Vemos dos situaciones muy similares. En el caso, lanzamos un Stringobjeto a un Objectobjeto. No hay problemas aquí, todo funciona como se esperaba. Pero en la segunda situación, el compilador genera un error. Pero estamos haciendo lo mismo, ¿no? Esta vez simplemente estamos usando una colección de varios objetos. Pero, ¿por qué se produce el error? ¿Cual es la diferencia? ¿ Estamos lanzando un Stringobjeto a uno Objecto 20 objetos? Hay una distinción importante entre un objeto y una colección de objetos . Si la Bclase es un elemento secundario de la Aclase, entonces Collection<B>no es un elemento secundario de Collection<A>. Es por eso que no pudimos lanzar nuestro List<String>a unList<Object>. Stringes hijo de Object, pero List<String>no es hijo de List<Object>. Esto puede no parecer muy intuitivo. ¿Por qué los creadores del lenguaje lo hicieron de esta manera? Imaginemos que el compilador no nos da error:
List<String> strings = new ArrayList<String>();
List<Object> objects = strings;
En este caso, podríamos, por ejemplo, hacer lo siguiente:
objects.add(new Object());
String s = strings.get(0);
Debido a que el compilador no nos dio ningún error y nos permitió crear una List<Object>referencia que apunta a strings, ¡podemos agregar cualquier Objectobjeto antiguo a la stringscolección! Por lo tanto, hemos perdido la garantía de que nuestra colección contiene solo los Stringobjetos especificados por el argumento de tipo en la invocación de tipo genérico . En otras palabras, hemos perdido la principal ventaja de los genéricos: la seguridad de tipos. Y debido a que el compilador no nos impidió hacer esto, obtendremos un error solo en tiempo de ejecución, que siempre es mucho peor que un error de compilación. Para evitar situaciones como esta, el compilador nos da un error:
// Compilation error
List<Object> objects = strings;
...y nos recuerda que List<String>no es descendiente de List<Object>. Esta es una regla infalible para los genéricos, y debe recordarse cuando se trabaja con ellos. Vamonos. Supongamos que tenemos una pequeña jerarquía de clases:
public class Animal {

   public void feed() {

       System.out.println("Animal.feed()");
   }
}

public class Pet extends Animal {

   public void call() {

       System.out.println("Pet.call()");
   }
}

public class Cat extends Pet {

   public void meow() {

       System.out.println("Cat.meow()");
   }
}
La jerarquía está encabezada por una clase Animal simple, que es heredada por Pet. Pet tiene 2 subclases: Perro y Gato. Ahora supongamos que necesitamos crear un iterateAnimals()método simple. El método debe tomar una colección de cualquier animal ( Animal, Pet, Cat, Dog), iterar sobre todos los elementos y mostrar un mensaje en la consola durante cada iteración. Intentemos escribir tal método:
public static void iterateAnimals(Collection<Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Another iteration in the loop!");
   }
}
¡Parece que el problema está resuelto! Sin embargo, como aprendimos recientemente, List<Cat>y List<Dog>no List<Pet>son descendientes de List<Animal>! Esto significa que cuando intentamos llamar al iterateAnimals()método con una lista de gatos, obtenemos un error de compilación:
import java.util.*;

public class Main3 {


   public static void iterateAnimals(Collection<Animal> animals) {

       for(Animal animal: animals) {

           System.out.println("Another iteration in the loop!");
       }
   }

   public static void main(String[] args) {


       List<Cat> cats = new ArrayList<>();
       cats.add(new Cat());
       cats.add(new Cat());
       cats.add(new Cat());
       cats.add(new Cat());

       // Compilation error!
       iterateAnimals(cats);
   }
}
¡La situación no pinta muy bien para nosotros! ¿Tenemos que escribir métodos separados para enumerar cada tipo de animal? En realidad, no, no lo hacemos :) ¡Y da la casualidad de que los comodines nos ayudan con esto! Podemos resolver el problema con un método simple usando la siguiente construcción:
public static void iterateAnimals(Collection<? extends Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Another iteration in the loop!");
   }
}
Este es un comodín. Más precisamente, este es el primero de varios tipos de comodines. Se conoce como comodines de límite superior y se expresa mediante ? se extiende ¿Qué nos dice este constructo? Esto significa que el método acepta una colección de Animalobjetos o una colección de objetos de cualquier clase que descienda de Animal(? extiende Animal). En otras palabras, el método puede aceptar una colección de objetos Animal, Pet, Dogu Catobjetos; no hay diferencia. Vamos a convencernos de que funciona:
public static void main(String[] args) {

   List<Animal> animals = new ArrayList<>();
   animals.add(new Animal());
   animals.add(new Animal());

   List<Pet> pets = new ArrayList<>();
   pets.add(new Pet());
   pets.add(new Pet());

   List<Cat> cats = new ArrayList<>();
   cats.add(new Cat());
   cats.add(new Cat());

   List<Dog> dogs = new ArrayList<>();
   dogs.add(new Dog());
   dogs.add(new Dog());

   iterateAnimals(animals);
   iterateAnimals(pets);
   iterateAnimals(cats);
   iterateAnimals(dogs);
}
Salida de la consola:
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Creamos un total de 4 colecciones y 8 objetos, y hay exactamente 8 entradas en la consola. ¡Todo funciona muy bien! :) El comodín nos permitió ajustar fácilmente la lógica necesaria vinculada a tipos específicos en un solo método. Eliminamos la necesidad de escribir un método separado para cada tipo de animal. Imagina cuántos métodos hubiéramos necesitado si nuestra aplicación fuera utilizada por un zoológico o una oficina veterinaria :) Pero ahora veamos una situación diferente. Nuestra jerarquía de herencia permanece sin cambios: la clase de nivel superior es Animal, con la Petclase justo debajo, y las clases Caty Dogen el siguiente nivel. Ahora necesitas reescribir el iterateAnimals()método para que funcione con cualquier tipo de animal, excepto perros . Es decir, debe aceptar Collection<Animal>,Collection<Pet>o Collection<Car>, pero no debería funcionar con Collection<Dog>. ¿Cómo podemos lograr esto? Parece que nuevamente nos enfrentamos a la perspectiva de escribir un método separado para cada tipo:/ ¿De qué otra manera le explicamos al compilador lo que queremos que suceda? ¡En realidad es bastante simple! Una vez más, los comodines vienen en nuestra ayuda aquí. Pero esta vez usaremos otro tipo de comodín: un comodín de límite inferior , que se expresa mediante super .
public static void iterateAnimals(Collection<? super Cat> animals) {

   for(int i = 0; i < animals.size(); i++) {

       System.out.println("Another iteration in the loop!");
   }
}
Aquí el principio es similar. La <? super Cat>construcción le dice al compilador que el iterateAnimals()método puede aceptar como entrada una colección de Catobjetos o cualquier ancestro de la Catclase como entrada. En este caso, la Catclase, su padre Pety el padre de su padre Animalcoinciden con esta descripción. La Dogclase no coincide con nuestra restricción, por lo que intentar usar el método con un List<Dog>argumento generará un error de compilación:
public static void main(String[] args) {

   List<Animal> animals = new ArrayList<>();
   animals.add(new Animal());
   animals.add(new Animal());

   List<Pet> pets = new ArrayList<>();
   pets.add(new Pet());
   pets.add(new Pet());

   List<Cat> cats = new ArrayList<>();
   cats.add(new Cat());
   cats.add(new Cat());

   List<Dog> dogs = new ArrayList<>();
   dogs.add(new Dog());
   dogs.add(new Dog());

   iterateAnimals(animals);
   iterateAnimals(pets);
   iterateAnimals(cats);

   // Compilation error!
   iterateAnimals(dogs);
}
Hemos resuelto nuestro problema y, una vez más, los comodines resultaron ser extremadamente útiles :) Con esto, la lección ha llegado a su fin. Ahora ve lo importantes que son los genéricos en su estudio de Java: ¡hemos tenido 4 lecciones completas sobre ellos! Pero ahora conoces bien el tema y puedes demostrar tus habilidades en entrevistas de trabajo :) ¡Y ahora es hora de volver a las tareas! ¡El mejor de los éxitos en tus estudios! :)
Comentarios
  • Populares
  • Nuevas
  • Antiguas
Debes iniciar sesión para dejar un comentario
Esta página aún no tiene comentarios