Bits of Java – Episode 23: Generics in Java
Today we will discuss a bit about the use of generics in Java, which have been part of Java since its version 5.
But what exactly are generics? So, when you build a List
object, for instance, you would do something like this:
List<String> names = new ArrayList<>();
where, in between <>
you specify the type of the elements you want to put inside your list.
You could also do something like this:
List names = new ArrayList();
but, in this case, you would get a warning from the compiler, since you are not specifying the type of the elements. It would still run, as the compiler will take java.lang.Object
as type, which the widest we have in Java!
This is because, if we look at the definition of the List
interface we would find something like:
public interface List<E> {
//missing code...
}
where E
is our generic element type. This is what allows you to create a List
of whatever object you want!
Why is that useful? Well, in the end, a list is a list, right? You do not want to have a separate List
interface for every possible element type you put in there, right? Generics allow you to avoid exactly that! They allow you to define a unique structure that can be then applied to different kinds of types!
You can also create your own generic class
, for instance.
public class Box<T> {
}
Here, I am defining a class
Box
of a generic type T
. I can have a box of whatever I want, right? Let’s say I have defined somewhere two other classes Book
and Chocolate
, for instance. Then I can create a Box
for each one of these two types:
Box<Book> booksBox = new Box<Book>();
Box<Chocolate> chocoBox = new Box<Chocolate>();
Of course, in the generic class, you can define fields and methods, using generics as well.
public class Box<T> {
private List<T> contents = new ArrayList<T>();
public void add(T element) {
this.contents.add(element);
}
public T getElement(int index) {
return this.contents.get(index);
}
}
In here, I have defined a private
instance field, which is nothing else than a List
, containing the same type of objects as the type of Box
.
Then I have defined two methods, which are just wrapper methods of the add
and get
of a List
, so nothing too fancy here, but just to give you an idea.
You can also use generics in a non-generic class
! In particular, they can be quite useful when you work with static
helper methods.
public class HelperClass {
public static <T> void print(T t) {
System.out.println("Printing " + t);
}
}
In this case, HelperClass
is not a generic class, but its method print
uses generics. As you can see, contrary to what we did for the methods of Box
, here we have to put a <T>
before the return type of the method. This is mandatory here because the class itself does not use generics.
We could have also done it in the Box
class. However, if we did, in this case, the generic type of the class and the generic type of the methods are not automatically considered the same!! This is why the following would produce some compiler errors:
public class Box<T> {
private List<T> contents = new ArrayList<T>();
public <T> void add(T element) {
this.contents.add(element); //DOES NOT COMPILE!!
}
public <T> T getElement(int index) {
return this.contents.get(index); //DOES NOT COMPILE!!
}
}
Let’s examine what’s going on here! So, our Box
class
is a generic class of type T
. The contents
fields is a List
of the same generic type T
. The two methods, however, in this case, define their own generic type, through <T>
, which is not necessarily the same as the one declared by the class. So, when we try to add an element of that type to the contents
list, or we try to retrieve one, the compiler gives as an error! We need to add some cast operators to make it compile, and also change a bit the naming so the compiler does not get confused!
public class Box<T> {
private List<T> contents = new ArrayList<T>();
public <U> void add(U element) {
this.contents.add((T) element); //OK!!
}
public <U> U getElement(int index) {
return (U) this.contents.get(index); //OK!!
}
}
Now it is clear which generic refers to what! T
is the generic type of the class and of the contents
field, while U
is the generic type of the methods. To add an element of type U
to contents
we need first to cast it to T
, while to retrieve an element of type U
from the list, we have to cast it to U
.
As last remark for today, remember when we talked about overloading methods? We said that these are methods with the same name but with a different parameter list. So, you may argue that something like this should be valid:
public void printElements(List<String> names) {
for(String name : names) {
System.out.println(name);
}
}
public void printElements(List<Integer> numbers) {
for(Integer number : numbers) {
System.out.println(number);
}
}
But actually is not! What happens here is that we are using generics, and the generic type is the only thing that differs in the parameter list of the two methods. However, generics are there only till compile time! When this code is compiled, it will look like:
public void printElements(List<Object> names) {
for(Object name : names) {
System.out.println(name);
}
}
public void printElements(List<Object> numbers) {
for(Object number : numbers) {
System.out.println(number);
}
}
So, under the hood, the compiler removes all your generics reference and substitutes them with Object
, placing the right casting where necessary. And so, the two methods do not look anymore like valid overloading methods, and you get a compiler error!!
That’s it for today! In the next post we are going even deeper into this topic and we will discuss bounded and unbounded generics types! Stay tuned!!
by Ilenia Salvadori