JavaRush /Java Blog /Random EN /Wildcards in Java Generics

Wildcards in Java Generics

Published in the Random EN group
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!");
       // ниHowих проблем
       Object obj = str;

       List<String> strings = new ArrayList<String>();
       // ошибка компиляции!
       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:
// ошибка компиляции
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("Еще один шаг в цикле пройден!");
   }
}
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("Еще один шаг в цикле пройден!");
       }
   }

   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);
   }
}
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("Еще один шаг в цикле пройден!");
   }
}
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);
   iterateAnimals(cats);
   iterateAnimals(dogs);
}
Console output:

Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
We have created a total of 4 collections and 8 objects, and there are exactly 8 entries in the console. Everything works great! :) Wildcard allowed us to easily fit the necessary logic with binding to specific types into one method. We got rid of the need to write a separate method for each type of animal. Imagine how many methods we would have if our application was used in a zoo or a veterinary clinic :) Now let's look at a different situation. Our inheritance hierarchy will remain the same: the top-level class is Animal, just below is the class Pets Pet, and at the next level is Catand Dog. Now you need to rewrite the method iretateAnimals()so that it can work with any type of animal except dogs . Collection<Animal>That is, it should accept , Collection<Pet>or as input Collection<Cat>, but should not work with Collection<Dog>. How can we achieve this? It seems that the prospect of writing a separate method for each type is looming before us again :/ How else can we explain our logic to the compiler? And this can be done very simply! Here wildcards will come to our aid again. But this time we will use a different type - “ super ” (another name is Lower Bounded Wildcards ).
public static void iterateAnimals(Collection<? super Cat> animals) {

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

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
The principle here is similar. The construction <? super Cat>tells the compiler that the method iterateAnimals()can take as input a collection of objects of the class Cator any other ancestor class Cat. CatIn our case, the class itself , its ancestor - Pets, and the ancestor of the ancestor - fit this description Animal. The class Dogdoes not fit into this constraint, and therefore attempting to use a method with a list List<Dog>will result in a compilation error:
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);
}
Our problem is solved, and again wildcards turned out to be extremely useful :) This concludes the lecture. Now you see how important the topic of generics is when learning Java - we spent 4 lectures on it! But now you have a good grasp of the topic and will be able to prove yourself at the interview :) And now is the time to get back to the tasks! Good luck in your studies! :)
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION