JavaRush /Blog Java /Random-ES /Comodines en genéricos de Java

Comodines en genéricos de Java

Publicado en el grupo Random-ES
¡Hola! Seguimos estudiando el tema de los genéricos. Ya tienes bastante conocimiento sobre ellos gracias a conferencias anteriores (sobre el uso de varargs cuando se trabaja con genéricos y sobre el borrado de tipos ), pero aún no hemos cubierto un tema importante: los comodines . Esta es una característica muy importante de los genéricos. ¡Tanto es así que le hemos dedicado una conferencia aparte! Sin embargo, no hay nada complicado con los comodines, lo verás ahora :) Comodines en genéricos - 1Veamos un ejemplo:
public class Main {

   public static void main(String[] args) {

       String str = new String("Test!");
       // ниCómoих проблем
       Object obj = str;

       List<String> strings = new ArrayList<String>();
       // ошибка компиляции!
       List<Object> objects = strings;
   }
}
¿Que está pasando aqui? Vemos dos situaciones muy similares. En el primero de ellos, intentamos convertir un objeto Stringpara escribir Object. No hay ningún problema con esto, todo funciona como debería. Pero en la segunda situación, el compilador arroja un error. Aunque parecería que estamos haciendo lo mismo. Es solo que ahora estamos usando una colección de varios objetos. Pero ¿por qué ocurre el error? ¿Cuál es la diferencia, esencialmente, entre convertir un objeto Stringa un tipo Objecto 20 objetos? Existe una diferencia importante entre un objeto y una colección de objetos . Si una clase es sucesora de una clase , entonces no es sucesora . Es por esta razón que no pudimos traer el nuestro . es heredero , pero no es heredero . Intuitivamente, esto no parece muy lógico. ¿Por qué los creadores del lenguaje se guiaron por este principio? Imaginemos que el compilador no nos daría error aquí: BАCollection<B>Collection<A>List<String>List<Object>StringObjectList<String>List<Object>
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);
Dado que el compilador no nos dio errores y nos permitió crear una referencia List<Object> objecta una colección de cadenas strings, ¡podemos agregar stringsno una cadena, sino simplemente cualquier objeto Object! Por lo tanto, hemos perdido la garantía de que solo los objetos especificados en el genérico estén en nuestra colecciónString . Es decir, hemos perdido la principal ventaja de los genéricos: la seguridad de tipos. Y como el compilador nos permitió hacer todo esto, significa que solo obtendremos un error durante la ejecución del programa, que siempre es mucho peor que un error de compilación. Para evitar este tipo de situaciones, el compilador nos da un error:
// ошибка компиляции
List<Object> objects = strings;
...y le recuerda que List<String>no es heredero List<Object>. Ésta es una regla estricta para el funcionamiento de los genéricos y debe recordarse al utilizarlos. Vamonos. Digamos 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()");
   }
}
A la cabeza de la jerarquía están simplemente los animales: de ellos se heredan las mascotas. Las mascotas se dividen en 2 tipos: perros y gatos. Ahora imagina que necesitamos crear un método simple iterateAnimals(). El método debe aceptar una colección de cualquier animal ( Animal, Pet, Cat, Dog), iterar a través de todos los elementos y enviar algo a la consola cada vez. Intentemos escribir este método:
public static void iterateAnimals(Collection<Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
¡Parecería que el problema está solucionado! Sin embargo, como descubrimos recientemente, ¡ List<Cat>o List<Dog>no List<Pet>son herederos List<Animal>! Por lo tanto, cuando intentamos llamar a un método iterateAnimals()con una lista de gatos, recibiremos un error del compilador:
import java.util.*;

public class Main3 {


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

       for(Animal animal: animals) {

           System.out.println("Еще один шаг в цикле пройден!");
       }
   }

   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());

       //ошибка компилятора!
       iterateAnimals(cats);
   }
}
¡La situación no pinta bien para nosotros! ¿Resulta que tendremos que escribir métodos separados para enumerar todos los tipos de animales? En realidad, no, no es necesario :) ¡Y los comodines nos ayudarán con esto ! Resolveremos el problema con un método simple, utilizando la siguiente construcción:
public static void iterateAnimals(Collection<? extends Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
Este es el comodín. Más precisamente, este es el primero de varios tipos de comodines: " extiende " (otro nombre es comodines con límite superior ). ¿Qué nos dice este diseño? Esto significa que el método toma como entrada una colección de objetos de clase Animalu objetos de cualquier clase descendiente Animal (? extends Animal). AnimalEn otras palabras, el método puede aceptar la colección , Peto Dogcomo entrada Cat; no hay diferencia. Asegurémonos de que esto funcione:
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 consola:

Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Hemos creado 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 con enlace a tipos específicos en un solo método. Nos deshicimos de la necesidad de escribir un método separado para cada tipo de animal. Imagínese cuántos métodos tendríamos si nuestra aplicación se usara en un zoológico o una clínica veterinaria :) Ahora veamos una situación diferente. Nuestra jerarquía de herencia seguirá siendo la misma: la clase de nivel superior es Animal, justo debajo está la clase Pets Pety en el siguiente nivel está Caty Dog. Ahora necesitas reescribir el método iretateAnimals()para que pueda funcionar con cualquier tipo de animal excepto perros . Collection<Animal>Es decir, debería aceptar o Collection<Pet>como entrada Collection<Cat>, pero no debería funcionar con Collection<Dog>. ¿Cómo podemos lograr esto? Parece que la perspectiva de escribir un método separado para cada tipo se cierne ante nosotros nuevamente :/ ¿De qué otra manera podemos explicar nuestra lógica al compilador? ¡Y esto se puede hacer de forma muy sencilla! Aquí los comodines volverán a venir en nuestra ayuda. Pero esta vez usaremos un tipo diferente: " super " (otro nombre es Lower Bounded Wildcards ).
public static void iterateAnimals(Collection<? super Cat> animals) {

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

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
El principio es similar aquí. La construcción <? super Cat>le dice al compilador que el método iterateAnimals()puede tomar como entrada una colección de objetos de la clase Cato de cualquier otra clase antecesora Cat. CatEn nuestro caso, la clase misma , su ancestro Petsy el ancestro del ancestro encajan en esta descripción Animal. La clase Dogno encaja en esta restricción y, por lo tanto, intentar utilizar un método con una lista List<Dog>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);

   //ошибка компиляции!
   iterateAnimals(dogs);
}
Nuestro problema está resuelto y nuevamente los comodines resultaron extremadamente útiles :) Con esto concluye la conferencia. Ahora ve lo importante que es el tema de los genéricos al aprender Java: ¡le dedicamos 4 conferencias! Pero ahora dominas bien el tema y podrás demostrar tu valía en la entrevista :) ¡Y ahora es el momento de volver a las tareas! ¡Buena suerte en tus estudios! :)
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION