Hi! Let's continue our study of generics.
You've already gained a substantial body of knowledge about them from previous lessons (about using varargs when working with generics and about type erasure), but there is an important topic that we have not yet considered — wildcards.
This is very important feature of generics. So much so that we have dedicated a separate lesson to it! That said, there's nothing particularly complicated about wildcards. You'll see that right away :)Let's look at an example:
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;
}
}
What's going on here?
We see two very similar situations. In the case, we cast a String
object to an Object
object. There are no problems here — everything works as expected.
But in the second situation, the compiler generates an error. But we're doing the same thing, aren't we? This time we're simply using a collection of several objects.
But why does the error occur? What's the difference? Are we casting one String
object to an Object
or 20 objects?
There is an important distinction between an object and a collection of objects.
If the B
class is a child of the A
class, then Collection<B>
is not a child of Collection<A>
.
This is why we weren't able to cast our List<String>
to a List<Object>
. String
is a child of Object
, but List<String>
is not a child of List<Object>
.
This may not seem super intuitive. Why did the language's creators make it this way?
Let's imagine that the compiler doesn't give us an error:
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);
Because the compiler didn't give us any error and allowed us to create a List<Object>
reference that points to strings
, we can add any old Object
object to the strings
collection!
Thus, we've lost the guarantee that our collection contains only the String
objects specified by the type argument in the generic type invocation. In other words, we have lost the main advantage of generics — type safety.
And because the compiler didn't stop us from doing this, we will get an error only at run time, which is always much worse than a compilation error.
To prevent situations like this, the compiler gives us an error:
// Compilation error
List<Object> objects = strings;
...and reminds us that List<String>
is not a descendant of List<Object>
.
This is an ironclad rule for generics, and it must be remembered when working with them.
Let's move on.
Suppose 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()");
}
}
The hierarchy is topped by a simple Animal class, which is inherited by Pet. Pet has 2 subclasses: Dog and Cat.
Now suppose that we need to create a simple iterateAnimals()
method. The method should take a collection of any animals (Animal
, Pet
, Cat
, Dog
), iterate over all the elements, and display a message on the console during each iteration.
Let's try to write such a method:
public static void iterateAnimals(Collection<Animal> animals) {
for(Animal animal: animals) {
System.out.println("Another iteration in the loop!");
}
}
It seems that the problem is solved!
However, as we recently learned, List<Cat>
, List<Dog>
and List<Pet>
are not descendants of List<Animal>
!
This means that when we try to call the iterateAnimals()
method with a list of cats, we get a compilation error:
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);
}
}
The situation doesn't look very good for us! Do we have to write separate methods for enumerating each kind of animal? Actually, no, we don't :)
And as it happens, wildcards help us with this!
We can solve the problem with one simple method using the following construct:
public static void iterateAnimals(Collection<? extends Animal> animals) {
for(Animal animal: animals) {
System.out.println("Another iteration in the loop!");
}
}
This is a wildcard. More precisely, this is the first of several types of wildcards. It is known as an upper-bounded wildcards and is expressed by ? extends.
What does this construct tell us? This means that the method accepts a collection of Animal
objects or a collection of objects of any class that descends from Animal
(? extends Animal).
In other words, the method can accept a collection of Animal
, Pet
, Dog
, or Cat
objects — it makes no difference.
Let's convince ourselves that it 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:
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!
We created a total of 4 collections and 8 objects, and there are exactly 8 entries on the console. Everything works great! :)
The wildcard allowed us to easily fit the necessary logic tied to specific types into a single method. We eliminated the need to write a separate method for each type of animal. Imagine how many methods we would have needed if our application was used by a zoo or a veterinary office :)
But now let's look at a different situation.
Our inheritance hierarchy remains unchanged: the top-level class is Animal
, with the Pet
class just below, and the Cat
and Dog
classes at the next level.
Now you need to rewrite the iterateAnimals()
method so that work with any type of animal, except for dogs.
That is, it should accept Collection<Animal>
, Collection<Pet>
or Collection<Car>
, but it should not work with Collection<Dog>
.
How can we achieve this?
It seems that we again face the prospect of writing a separate method for each type :/
How else do we explain to the compiler what we want to happen? It's actually quite simple! Once again, wildcards come to our aid here. But this time we will use another type of wildcard — a lower-bounded wildcard, which is expressed using 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!");
}
}
Here the principle is similar.
The <? super Cat>
construct tells the compiler that the iterateAnimals()
method can accept as input a collection of Cat
objects or any ancestor of the Cat
class as input.
In this case, the Cat
class, its parent Pet
, and the parent of its parent, Animal
, all match this description.
The Dog
class does not match our restriction, so an attempt to use the method with a List<Dog>
argument 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);
// Compilation error!
iterateAnimals(dogs);
}
We've solved our problem, and once again wildcards turned out to be extremely useful :)
With this, the lesson has come to an end.
Now you see how important generics are in your study of Java — we've had 4 whole lessons about them! But now you are well versed in the topic and you can prove your skills in job interviews :)
And now, it's time to get back to the tasks!
Best of success in your studies! :)
GO TO FULL VERSION