Bits of Java – Episode 24: Bounded and Unbounded Generics in Java
Last week we talked about generics in Java. Today we will remain on this topic and discuss a few more things you can do with them!
At the end of the last post, we said that generics are basically just java.lang.Object
for the compiler, and that, indeed, after compilation, what for you read like this:
List<String> names = new ArrayList<String>();
for the compiler simply looks as:
List<Object> names = new ArrayList<Object>();
So, let’s say, you want to define a method that behaves the same for any kind of list you have, namely a list of String
, a list of Integer
, etc. You could do that in two ways:
public <T> void firstWay(List<T> list) {
for(T t : list) {
System.out.println(t);
}
}
public void secondWay(List<?> list) {
for(Object obj : list) {
System.out.println(obj);
}
}
The first method is something we already saw last week. We defined a generic type T
and use it as type for our input List
parameter.
The second method, instead, is something new. It is called a wildcard generic type, and is used when you want to represent any generic type.
You may have thought also to another possibility, something like this, for instance:
public void wrongWay(List<Object> list) {
for(Object obj : list) {
System.out.println(obj);
}
}
I called this method wrongWay
for a reason: this is wrong!! I mean, it compiles, of course, but it does not do what we wanted to achieve in the first place. Namely, if you try to pass to this method a List<String>
, for instance, you will get a compiler error!
Why? String
is a subclass of Object
, after all. Well, yes, but when you define a List<String>
you are promising the compiler that you will insert only String
in that list, and so the compiler prevents you to ever assign such list to something which is not a list of String
.
List<String> names = new ArrayList<>();
List<Object> objList = names; //DOES NOT COMPILE!!
Instead, if you use the wildcard generic type, the compiler knows that it should expect really any type, and so this will compile:
List<String> names = new ArrayList<>();
List<?> whateverList = names; //OK!!
The wildcard generic type can be used alone, as we have just seen, but it can also be used in two other ways, which allow you to restrict the possible acceptable types. Let’s start with the first one.
List<? extends Number> numberList = new ArrayList<Integer>();
This is used to specify that you want to build a List
which is allowed to contain any type that extends the Number
class, or the Number
type itself. It is called the upper-bounded wildcard.
So, for instance, in our example, we have then assign to numberList
an ArrayList
of Integer
, which is a sub type of Number
.
What happens if we try to add something to this list? Well, it will not compile, no matter what you try to insert. This is because, when you define a list with the upper-bounded wildcard, as well as with the unbounded wildcard, the list becomes immutable!
List<? extends Number> numberList = new ArrayList<Integer>();
numberList.add(7); //DOES NOT COMPILE!!
List<String> names = new ArrayList<>();
List<?> whateverList = names;
whateverList.add("Loki"); //DOES NOT COMPILE!!
So, in the end, the common use for those kind of generics is in method declaration, where you want to specify which types you expect as input.
The second bounded wildcard generic is the lower-bounded:
List<? super Integer> numberList = new ArrayList<Number>();
With this one, we are specifying to the compiler that the element type of the list is Integer
or whatever other type that is a super type of Integer
, for instance, Number
or Object
.
In the case of lower-bounded generics the defined list is not immutable, but still, I think it’s worth mentioning what kind of objects you can actually insert, because when I first studied them, I was a bit confused!
So, let’s try to add some elements to our numberList
:
List<? super Integer> numberList = new ArrayList<Number>();
Integer i = 7;
Number n = 6;
Object o = n;
numberList.add(i); //OK!
numberList.add(n); //DOES NOT COMPILE!!
numberList.add(o); //DOES NOT COMPILE!!
So, adding an Integer
is fine, but adding a Number
or an Object
will cause a compiler error. The thing here is that the compiler only knows to what you have assigned the reference, and not the actual type of the object in memory. So, we have defined our numberList
as a List<? super Integer>
, meaning that, for the compiler, we could have on the right side, a list of Integer
, Number
, Object
, plus all the interfaces that those implement.
So, when we try to add an Integer
we are fine, because, no matter what was on the right, an Integer
would fit in that.
When we try to add the other two types, instead, Number
and Object
, we have a problem. What happens if on the right side of our numberList
we would have had ArrayList<Integer>
? Neither Number
nor Object
would fit in that! That’s why the compiler prevents us to insert them in the list!
This concludes our excursion into generics in Java! I had a hard time understanding them, so I hope this post would help other people getting a bit more familiar and confident with this topic!!
Stay tuned for some new episodes, next year! I wish you a Merry Christmas, hoping that you can enjoy it, at least a bit, despite these weird times we are living in, and let’s hope for a better New Year for everyone!!
by Ilenia Salvadori