Hello! We continue to study the topic of generics. You already have a good amount of knowledge about them from previous lectures (about using varargs when working with generics and about type erasure ), but we haven’t covered one important topic yet: wildcards . This is a very important feature of generics. So much so that we have dedicated a separate lecture for it! However, there is nothing complicated about wildcards, you’ll see that now :) Wildcards in generics - 1Let’s look at an example:
public class Main {

   public static void main(String[] args) {

       String str = new String("Test!");
       // no problems
       Object obj = str;

       List<String> strings = new ArrayList<String>();
       // compilation error!
       List<Object> objects = strings;
   }
}
What's going on here? We see two very similar situations. In the first of these, we are trying to cast an object Stringto type Object. There are no problems with this, everything works as it should. But in the second situation, the compiler throws an error. Although it would seem that we are doing the same thing. It's just that now we're using a collection of several objects. But why does the error occur? What is the difference, essentially, whether we cast one object Stringto a type Objector 20 objects? There is an important difference between an object and a collection of objects . If a class is a successor of a class , then it is not a successor . It is for this reason that we could not bring ours to . is an heir , but is not an heir . Intuitively, this does not look very logical. Why were the creators of the language guided by this principle? Let's imagine that the compiler would not give us an error here: BАCollection<B>Collection<A>List<String>List<Object>StringObjectList<String>List<Object>
List<String> strings = new ArrayList<String>();
List<Object> objects = strings;
In this case, we could, for example, do the following:
objects.add(new Object());
String s = strings.get(0);
Since the compiler did not give us errors and allowed us to create a reference List<Object> objectto a collection of strings strings, we can add stringsnot a string, but simply any object Object! Thus, we have lost the guarantee that only the objects specified in the generic are in our collectionString . That is, we have lost the main advantage of generics - type safety. And since the compiler allowed us to do all this, it means that we will only get an error during program execution, which is always much worse than a compilation error. To prevent such situations, the compiler gives us an error:
// compilation error!
List<Object> objects = strings;
...and reminds him that List<String>he is not an heir List<Object>. This is an ironclad rule for the operation of generics, and it must be remembered when using them. Let's move on. Let's say we have a small class hierarchy:
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()");
   }
}
At the head of the hierarchy are simply Animals: Pets are inherited from them. Pets are divided into 2 types - Dogs and Cats. Now imagine that we need to create a simple method iterateAnimals(). The method should accept a collection of any animals ( Animal, Pet, Cat, Dog), iterate through all the elements, and output something to the console each time. Let's try to write this method:
public static void iterateAnimals(Collection<Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Another step in the cycle completed!");
   }
}
It would seem that the problem is solved! However, as we recently found out, List<Cat>or List<Dog>are List<Pet>not heirs List<Animal>! Therefore, when we try to call a method iterateAnimals()with a list of cats, we will receive a compiler error:
import java.util.*;

public class Main3 {


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

       for(Animal animal: animals) {

           System.out.println("Another step in the cycle completed!");
       }
   }

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

       // compiler error!
       iterateAnimals(cats);
   }
}
The situation is not looking good for us! It turns out that we will have to write separate methods for enumerating all types of animals? Actually, no, you don’t have to :) And wildcards will help us with this ! We will solve the problem within one simple method, using the following construction:
public static void iterateAnimals(Collection<? extends Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Another step in the cycle completed!");
   }
}
This is the wildcard. More precisely, this is the first of several types of wildcard - “ extends ” (another name is Upper Bounded Wildcards ). What does this design tell us? This means that the method takes as input a collection of class objects Animalor objects of any descendant class Animal (? extends Animal). AnimalIn other words, the method can accept the collection , Pet, Dogor as input Cat- it makes no difference. Let's make sure this works:
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