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 :)
Let’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 String
to 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 String
to a
type Object
or 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>
String
Object
List<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> object
to a collection of strings strings
, we can add strings
not 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 Animal
or objects of any descendant
class Animal (? extends Animal)
. Animal
In other
words, the method can accept the collection , Pet
, Dog
or 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
GO TO FULL VERSION