Java Wildcards
Wildcards are a specific Java tool that allows the implementation of some compatibility between different generic objects. In essence, the wildcard is the "?" sign, used to indicate that a class, method, or field is compatible with different type parameters.
Why Wildcards?
As many object-oriented programming languages, Java relies on the concept of inheritance. But since generics are type-safe structures, it is impossible to introduce inheritance for generic objects.
To illustrate this problem, let's consider two classes:
class Book {}
class Album extends Book {}
We might assume that a list of albums can be treated as a list of books because Album
is a subclass of Book
. But the compiler thinks differently:
List<Album> albums = new ArrayList<>();
List<Book> books = albums; // compile-time error
The root of the problem lies in the fact that List<Album>
is not a subclass of List<Book>
: inheritance does not apply to generic classes. Such behavior is known as invariance. It doesn't matter that Album
extends Book
, because containers like List<T>
, Set<T>
and others are treated like independent classes.
The example above is exactly where wildcards could help. A generic class or a method declared with wildcards can avoid inheritance issues. To implement wildcards, use "?" inside angle brackets (<?>
). Let's use it to address the compiler error:
List<Album> albums = new ArrayList<>();
List<? extends Book> albumsAndBooks = albums; // it is ok
or
List<Album> albums = new ArrayList<>();
List<? super Album> albumsAndBooks = albums; // it is ok as well
Wildcards are commonly used with type bounds. In the type bounds article, we learned how to use the extends
keyword; now we will also take a look at the super
keyword. Since wildcards are used with type bounding, they can be divided into three groups: unbounded wildcards, upper bounded wildcards, and lower bounded ones.
Upper Bounded Wildcards
Upper Bounded Wildcards are used when we want to set an upper bound. It is set with the extends
keyword:
? extends ReferenceType
This code can be read as "any type that is a subtype of ReferenceType
". In other words, if S
is a subtype of T
, then type List<S>
is considered to be a subtype of List<? extends T>
. This feature is known as covariance.
Suppose that our program represents a library where we want to store only different types of books: normal books, booklets, photo albums, and so on. How would we avoid storing other media types such as audio recordings? Let's say that we have two more classes:
public class Booklet extends Book {}
public class AudioFile {}
We can use wildcards to create a list that stores only different types of books:
List<? extends Book> storage = new ArrayList<>();
List<Album> albums = new ArrayList<>();
storage = albums; // it works, Album is a subtype of Book
List<Booklet> booklets = new ArrayList<>();
storage = booklets; // it works, Booklet is a subtype of Book
List<AudioFile> recordings = new ArrayList<>();
storage = recordings; // compile-time error, AudioFile is not a subtype of Book
By using an upper-bounded wildcard, we made sure that the storage variable can only be set to subtypes of Book
.
Now let's consider another limitation of upper bounding.
/*
*Hierarchy: Book -> Album
* -> Booklet
* Allowed types: List<Book>, List<Album>, List<Booklet>
*/
public void upperBoundedMethod(List<? extends Book> books) {
Book book = books.get(0); // It is fine
books.add(new Album()); // compile-time error
books.add(new Book()); // compile-time error
books.add(null); // also fine, because null is a special type-independent value
}
Surprisingly, some lines of upperBoundedMethod
won't compile. Upper bounded wildcards are able to read content as a Book
type, but they are not able to write any content except for a null value.
Let's explain the logic behind these read and write permissions. Because the method accepts lists parameterized by Book
or any of its subtypes (List<Book>
, List<Album>
or List<Booklet>
), any argument can be read as an object of type Book
. Writing to a wildcard argument, however, is prohibited to avoid runtime errors. To see why, suppose that a List<Album>
was passed in, but then we try to add an instance of Book
to this list. This Book
object will be treated as an Album
object in the future, which will likely lead to a runtime error.
Lower Bounded Wildcards
Lower bounded Wildcards are introduced with the super
keyword followed by the lower bound:
? super ReferenceType
This means "any type that is a supertype of ReferenceType
". For example, if S
is a supertype of T
, then List<S>
is considered to be a supertype of List<? super T>
. This feature is called contravariance.
Let's think of books again. Now we would like to write code that will enable a list of Albums
and its superclasses to be added to a general library.
Take a look at the following code:
List<? super Album> storage = new ArrayList<>();
List<Album> albums = new ArrayList<>();
storage = albums; // it works
List<Book> books = new ArrayList<>();
storage = books; // it works, Book is a supertype for Album
List<Booklet> booklets = new ArrayList<>();
storage = booklets; // compile-time error, Booklet is not a supertype for Album
Here we made sure that only supertypes of the Album
class can be added to the storage.
Now let's consider some limitations of lower bounding.
/**
* Hierarchy: Album <- Book <- Object
* Allowed types: List<Album>, List<Book>, List<Object>
*/
public void lowerBoundedMethod(List<? super Album> albums) {
Object object = albums.get(0); // it is ok. Object is upper bound of Album
Book book = albums.get(0); // compile-time error
Album album = albums.get(0); // compile-time error
albums.add(new Object()); // compile-time error
albums.add(new Book()); // compile-time error
albums.add(new Album()); // OK
albums.add(null); // OK, null is type-independent
}
Similarly to upper-bounded wildcards, certain actions involving lower-bounded wildcards lead to compile-time errors. Since any of List<Album>
, List<Book>
, List<Object>
can be passed to lowerBoundedMethod
, we can't assert that the object being read has the type Album
or Book
. We can only be certain that it has type Object
, since all classes inherit from Object
.
While Object
is the only type that can be read from albums, Album
is the only type that can be added. The reason is that only an instance of Album
can also be treated as an instance of Book
and Object
. If we tried to add an instance of Book
, this instance would be treated as Album
in the future. The compiler prevents such errors by issuing a compiler-time warning.
Get and Put Principle
To decide whether extends or super should be used, it is worth remembering the Get and Put principle:
- Use upper bounded wildcards when you only get values out of a structure (i.e. when you use only getters or similar methods).
- Use lower bounded wildcards when you only put values into a structure (i.e. when you use only setters or similar methods).
- Use Unbounded Wildcards (simply <?>) when you both get and put values (i.e. when you need to use both getters and setters).
Note that putting values may require an extra step to avoid errors — we discuss this in the following section.
To memorize this principle, you can also use PECS: Producer Extends, Consumer Super. This means that if you get a value from a generic class, method, or any other object that produces what you need, you use extends. If you put or set a value into a generic class, method, or any other object that consumes what you put in, you use super.
Remember that it is not possible to put anything into a type declared with the extends
wildcard except for the null value, which can represent any reference type. Similarly, it is not possible to retrieve anything from a type declared with the super wildcard except for an instance of Object
, the parent of every reference type.
You cannot use both a lower and an upper bound in wildcards or type bounds.
Note that a class or interface used with the extends
or super
keyword is itself included in the inheritance. For example, Box<T>
is compatible and covariant with Box<? extends T>
and Box<? super T>
.
Also note that the unbounded wildcard ?
is equivalent to ? extends Object
.
Remember that the purpose of inheritance prohibition in generics is to prevent run-time errors; otherwise, generics would lose their type safety.
Wildcard capture
Let's consider the example:
public static void reverse(List<?> list) {
List<Object> tmp = new ArrayList<Object>(list);
for (int i = 0; i < list.size(); i++) {
list.set(i, tmp.get(list.size() - i - 1)); // compile-time error
}
}
What causes the compile-time error? Recall that <?>
is equivalent to <? extends Object>
. The compiler signals an error because it does not know the type of object being written to the list. The scenario is known as the wildcard capture problem and can be solved by the trick:
public static void reverse(List<?> list) {
reverseCaptured(list);
}
private static <T> void reverseCaptured(List<T> list) {
List<T> tmp = new ArrayList<T>(list);
for (int i = 0; i < list.size(); i++) {
list.set(i, tmp.get(list.size() - i - 1));
}
}
Here we introduced a helper method reverseCaptured
which has a parameter of a certain type T
for all elements of a list. The method is error-free because it is merely a generic method; it does not face any restrictions due to wildcards.
Conclusion
Wildcards are a convenient and safe way of implementing an equivalent of inheritance in Java Generics. They are declared with the <?>
symbol and are widely used with upper or lower bounds to restrict type parameters.