Earlier, when we were discussing type bounds, we mentioned Wildcards as a feature that serves a similar purpose and has wide application.

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 an object-oriented language, 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 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 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> or 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 Generics. They are declared with the <?> symbol and are widely used with upper or lower bounds to restrict type parameters.

Leave a Reply

Your email address will not be published.