The basics of Generics are explained in the article Generic Classes in Java by sreekandank. However, there is more to learn than explained in that article. Here I will talk about one specific feature of Java Generics called wildcards.
So, this has been a detailed explanation of what Wildcards in the world of Java Generics are. The full code can be downloaded below. So now it's time for you to do something with this new knowledge!
- Basic wildcards
Generics can be very useful, but sometimes they can be limiting. For example, say you have a generic class Wildcard<T> which has an List<T> object. This so far is no problem.
However, say we want to now write a function which will compare the length of the internal list with another List which is of course also generic. This should happen within the Wildcard<T> class and it should not depend on this second List having the same generic Type as the first one.Code:public class Wildcards<T> { private List<T> list; public void setList(List<T> list) { this.list = list; } }
A first attempt could be to make the Wildcard class more generic: Wildcard<T1,T2>. Now we can write our function.
That should work. Let’s test it:Code:public class Wildcards<T1,T2> { private List<T1> list; public void setList(List<T1> list) { this.list = list; } public boolean hasMoreElementsThan(List<T2> otherList) { return list.size() > otherList.size(); } }
Great, that should work. But are there any problems? Well, yes. Say we want to also compare the internal list to a third List. We would have to either create a new Wildcard instance for each type we want to compare our list to or make the list of types longer each time we want to add a new comparison. That’s not very efficient. Another idea might be to cast the second List to a more general type, such as LinkedList<Obje ct>. Trying that however will result in the following error message:Code:public static void main(String[] args) { // Create a new ArrayList of Strings and fill it with "Hello", "World" and "!" ArrayList<String> list1 = new ArrayList<String>(); list1.add("Hello"); list1.add("World"); list1.add("!"); // Create a new Wildcard<String> object and set list1 as the internal list Wildcards<String,Integer> w1 = new Wildcards<String,Integer>(); w1.setList(list1); // Create a new LinkedList of Integers and fill it with the Numbers 0 through 9 ArrayList<Integer> list2 = new ArrayList<Integer>(); for(int i=0; i<10; i++) { list2.add(new Integer(i)); } // Use the <i>hasMoreElementsThan</i> function if(w1.hasMoreElementsThan(list2)) { System.out.println("The internal list is bigger than the one it was compared to"); } else { System.out.println("The internal list is not bigger than the one it was compared to"); } }
Why so? Well, if that kind of cast were allowed, one could also do something like this:Code:Exception in thread "main" java.lang.Error: Unresolved compilation problem: Cannot cast from LinkedList<Integer> to LinkedList<Object>
I hope you see the problem.Code:((LinkedList<Object>) list2).add(42);
Anyway, this is why there is the concept of wildcards in Java; rather than adding a further generic type for comparison we tell the hasMoreElements Than function ”I don’t really care, what sort of list it is I’m comparing to.” And here’s how that works:
The question mark means that we don’t care about the type. It is not the same as casting it to a List<Object>, because in this case we have no information whatsoever about what kind of Object may be in the List. We don’t have any information about the generic type through the call.Code:public boolean hasMoreElementsThan(List<?> otherList) { return list.size() > otherList.size(); }
Note: It is still possible to do stuff likeCode:if(otherList.get(0) instanceof String) { … } - Upper-bound wildcards
Though this can be useful, sometimes we want more control over what kinds of parameters can be handed to a function. Say for example, we want to create a further function in the Wildcards class that will check whether one of the numbers in a List of numbers is close to the size of our internal list. This could look something like this:
But wait – that only works if our List is actually a List<Number> but not if it’s a List<Integer> or a List<Double>. What to do?Code:public boolean closeToSize(List<Number> otherList) { for(Number item : otherList) { if(item.intValue() == list.size()) return true; } return false; }
Luckily, there are methods of limiting wildcards. Upper-bound wildcards are one of the two methods that are offered to do that. Basically, we say ”allow any generic object that extends Number”. And writing this is luckily very easy:
Note: The extends X syntax doesn’t just work for wildcards – if you want access to the generic type, you can also have something likeCode:public boolean closeToSize(List<? Extends Number> otherList) { // Same as before }
This should only be used however if the type is actually needed.Code:public <S extends Number> boolean closeToSize2(List<S> otherList) { for(int i=0; i<otherList.size(); i++) { S item = otherList.get(i); if(item.intValue() == list.size()) return true; } return false; } - Lower-bound wildcards
If there are upper-bound wildcards then intuition tells us that there are probably lower-bound ones too. And indeed, they exist! The reason they are needed is known as the PECS principle: Producer Expands, Consumer Super. But what does that mean?
In the upper-bound example, the List produced items with which we could do things. Because of that, we needed some information on what kind of objects the contents may be. If however we want to create a new List and have this new List consume Objects (= add Objects to the List), we need to know what can be put into it. An example:
withCode:public void unite(List<? extends Form> listA, List<? extends Form> listB) { for(Form item : listA) { listB.add(item); } }If we try to compile this, and call it with w1.unite(list4, list5) (the Lists being of the types ArrayList<Ellip se> and ArrayList<Circl e> respectively) the following RuntimeExceptio n is thrown:Code:abstract class Form { protected double area; public double getArea() { return area; } } class Ellipse extends Form { public Ellipse(int rx, int ry) { super(); area = Math.PI * rx * ry; } } class Circle extends Ellipse { public Circle(int r) { super(r,r); } } class Rectangle extends Form { public Rectangle(int x, int y) { area = x * y; } } class Square extends Rectangle { public Square(int x) { super(x,x); } }
The reason for this is that the program has no way of knowing, whether the two generic types are compatible. After all, from what it knows we may be trying to add Squares to a List of of Circles! For this reason, we can limit wildcards in the other direction too:Code:Exception in thread "main" java.lang.Error: Unresolved compilation problem: The method add(capture#4-of ? extends Form) in the type List<capture#4-of ? extends Form> is not applicable for the arguments (Form)
This means, that the items of listB can be anything that is in the inheritance chain of Ellipse; this is of course only Form (which is abstract) and Ellipse itself. But, as we’re not reading from listB but only writing to it and Circles can be interpreted as Ellipses, this will work.Code:public void unite(List<? extends Circle> listA, List<? super Ellipse> listB) { for(Circle item : listA) { listB.add(item); } }
So, this has been a detailed explanation of what Wildcards in the world of Java Generics are. The full code can be downloaded below. So now it's time for you to do something with this new knowledge!
Comment