DEV Community

Kyle Carter
Kyle Carter

Posted on • Updated on • Originally published at blog.scaledcode.com

Effective Java! Use Bounded Wildcards to Increase API Flexibility

Today we take on a bit of a complicated topic but one that, when done correctly, can really make your code much more flexible in its usage. Previously we have talked about how parameterized types are invariant. This means that for two types, TypeA and TypeB, Collection<TypeA> can not be either a subtype or super type of Collection<TypeB>. Making this more concrete, List<String> is not a subtype of List<Object>. This is because any object can go into List<Object> but not any object can go into List<String>. While this may follow logically, sometimes we want/need more flexibility and that's what the chapter we are reviewing today covers.

Let's consider a previous example we looked at.

public class Stack<E> {
  public Stack();
  public void push(E e);
  public E pop();
}
Enter fullscreen mode Exit fullscreen mode

Above is the currently exposed API. Let's say we want to add a new method to add many items to the stack at once. We may consider writing it as such:

public void pushAll(Iterable<E> newItems) {
  for (E e : newItems) {
    push(e);
  }
}
Enter fullscreen mode Exit fullscreen mode

This will compile and work fine but we do lose some flexibility that we may want. Let's consider an example where this may lead to frustration:

Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers);
Enter fullscreen mode Exit fullscreen mode

While the above feels like it should work (Integer being a subtype of Number) it actually won't compile because of the invariants. This is where our bounded wildcard types come in to save the day. If we simply change the function signature to the following the above code will work.

public void pushAll(Iterable<? extends E> newItems) {
  for (E e : newItems) {
    push(e);
  }
}
Enter fullscreen mode Exit fullscreen mode

So what does the above signature, ? extends E, mean? It basically means it can take any subtype of E (as well as E itself which can feel a little strange with the extends keyword). This indeed fits exactly into what we were after.

Now let's consider a similar but different case. Let's create the cousin to the pushAll function and create a popAll function. How this one will work is we will give the function a collection and it will pop all the contents of the Stack into it. Personally I'm not a huge fan of using parameters as output from a function but it serves to illustrate this point well. So our initial implementation may be something like:

public void popAll(Collection<E> destination) {
  while(!isEmpty()) {
    destination.add(pop());
  }
}
Enter fullscreen mode Exit fullscreen mode

Once again this will compile with no issues and is usable but isn't as usable as it could be. Also again we get in trouble with a very similar example that we ran into with the pushAll function.

Collection<Number> numberCollection = ...;
Stack<Integer> integerStack = ...;
integerStack.popAll(numberCollection);
Enter fullscreen mode Exit fullscreen mode

This feels like it should work just fine but it doesn't because again we get bit by the invariants. The difference with this one is rather than looking for a subtype of E we are wanting a collection passed in that is type of E or a supertype of E. Java provides a way to do this like the following:

public void popAll(Collection<? super E> destination) {
  while(!isEmpty()) {
    destination.add(pop());
  }
}
Enter fullscreen mode Exit fullscreen mode

With this new signature the client code with compile and execute fine as well as the Stack code.

The rule of thumb that we are invited to follow is as follows. If an input parameter is something that we will be reading values from or writing to we should consider the use of wildcard types.

In the above two examples we used extends in one and super in another. You may ask, when should I use which one. This is indeed a great question. The mnemonic that the book suggests is PECS - producer-extends, consumer-super. Honestly this mnemonic doesn't really work for me as the producer/consumer terminology is a bit weird for me in this case. The way that I think of it is GEPS - get-extends, put-super. As in, if I'm getting values from it, I should use extends and if I'm putting values into it I should use super. Use whatever mnemonic works for you or come up with your own! So let's apply this to our examples above. In pushAll we were getting values out of the provided parameter to put somewhere else, so we used extends. In popAll we were putting values into the provided parameter, therefore super. This can take some practice to get the hang of thinking about. Let's go through some examples from previous examples in our generic section of the book.

First up, the Chooser function originally written as public Chooser(Collection<T> choices);. This takes in a selection of choices to be chosen from. Stop reading and thinking about which keyword should we use? Did you say extends? Great! Since we will be getting values from the choices collection (or in other words, if it's a producer) we use the extends keyword.

Next, public static <E> Set<E> union(Set<E> s1, Set<E> s2); which takes two sets in and provides a set that is the union of both the sets together. What keywords would we use for our parameters here? Did you say extends again? Perfect! Again we are grabbing values out of the provided Sets and therefore we want to use extends so the new function would look like public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2); But wait, what do we do with the return type, it still has no wildcards, is that OK? Indeed it is, in fact, it's preferred to be that way. Return types shouldn't use wildcards as it forces the use of wildcards on the receiver of those values. As a rule of thumb, if the user of your function needs to think about the wildcards you are using, you are likely doing it wrong. With the above changes we can now write code like:

Set<Integer> integerSet = Set.of(1,3,5);
Set<Double> doubleSet = Set.of(2.0,4.0,6.0);
Set<Number> numberSet = union(integerSet, doubleSet);
Enter fullscreen mode Exit fullscreen mode

The above code is great and quite clean in that the compiler is capable of inferring the type that we want. While there were great steps in improving the type inference of Java in Java 8 and beyond we still do run into cases where the compiler is still unable to infer the types for us. In these cases we will get fairly hairy error messages but, thankfully, there is a way to help the compiler along via an explicit type argument. Let's see what our above union call would need to look like prior to Java 8.

Set<Number> numberSet = Union.<Number>union(integerSet, doubleSet);
Enter fullscreen mode Exit fullscreen mode

Definitely not the prettiest code I have ever seen but at least it's possible and is something you can put in the back of your mind if you ever do run into this type of issue.

Ok, let's return from that detour to our practice game of Super or Extends. Let's consider the max function from the previous chapter. public static <T extends Comparable<T>> T max(List<T> list). It's a workout even to type that thing but let's think about how we would write this when in search of greater flexibility. Consider each type parameter individually. What we end up with is public static <T extends Comparable<? super T>> T max(List<? extends T> list); That has to be up there with one of the more complicated type signatures I have ever written. Let's consider the two parts. With our Comparable part we are providing a value to something there so in that area we are going to want to use super whereas in the list parameter we are grabbing items from that List and thus we want to use extends. Rule of thumb is that Comparables always are consumers and thus will always use super. This added complexity doesn't come without it's benefits though. We are now able to find the max of types such as ScheduledFuture which doesn't implement Comparable<ScheduledFuture> but instead implements Comparable<Delayed> the super type of ScheduledFuture.

There is one last thing to consider. When to use wildcards, and when to use type parameters. Consider the following two function signatures that implement a swap function:

public static <E> void swap(List<E> list, int from, int to);
public static void swap(List<?> list, int from, int to);
Enter fullscreen mode Exit fullscreen mode

So which one should be preferred? Effective Java says a rule to follow is, if a type parameter only shows up in a method signature once that we should go with the wildcard option (the second option). This also does seem like the simpler type signature. However, before we jump to that solution let's consider how the implementation would work.

public static void swap(List<?> list, int from, int to) {
  list.set(from, list.set(to, list.get(from));
}
Enter fullscreen mode Exit fullscreen mode

This actually won't compile because the only thing allowed to be put into a wildcard collection is null. There is a way around this though, with a helper function that can capture the type. So if we change it to the following it will work.

public static void swap(List<?> list, int from, int to) {
  swapHelper(list, from, to);
}

private static <E> void swapHelper(List<E> list, int from, int to){
  list.set(from, list.set(to, list.get(from));
}
Enter fullscreen mode Exit fullscreen mode

Because the swapHelper function can capture the type of E and verify that the operation is safe to do this will work. However, take a look at it again, didn't we just write the first option? We did indeed, so in this case I would definitely vote in favor of going with the type parameter option rather than the wildcard option here.

This has been a fairly heavy chapter. It will likely take more practice and exposure to these wildcard parameters to really get your mind around them, I know it did for me and I continue to get confused about them at times. As you use these in the wild you will likely find that you use the extends option a lot more than you use the super option. This is just the nature of the fact that we usually pass parameters in to read from more than we pass them in to put values into. As we use these capabilities of bounded wildcard parameters in our code we can indeed make our code much more flexible and this is very much something that I think is worth the effort.

Top comments (0)