When it comes to generics, I think most of us would have to confess that we usually don’t make a great use of all the potential of this feature (available as of Java 5), apart from using it as a mere doorman to control what goes inside our collections of objects. And that’s just probably because the Collections API usually provides us with everything we need on a daily basis.
As you know, generics were introduced as a way of making the Java language type-safer. This is, before generics, there was no way for the compiler of preventing, say, a String to sneak in a container that was originally meant for Integers:
List myFavouriteNumbers = new ArrayList();
myFavouriteNumbers.add(1);
myFavouriteNumbers.add(2);
myFavouriteNumbers.add("Groucho"); //ooops
System.out.println(42 + (Integer)myFavouriteNumbers.get(2)); //java.lang.ClassCastException
Thus, when such things happened, you had to wait until runtime to find out that there was a bug in your code (sorry, Groucho, for comparing you with a bug: you know I’ll always love you).
This compiler’s negligence occured because, without generics, the compiler was treating the list as a container of Objects (java.lang.Object -with capital O-, the root ancestor of any object regardless of its type), so you as a programmer were responsible to check if the object that was going to get in the collection was of the right type.
To add insult to injury, you had to make the necessary castings in order for the compiler to let you call a non-Object method of the object.
List myFavouriteNames = new ArrayList();
myFavouriteNames.add("Groucho");
myFavouriteNames.add("Chico");
myFavouriteNames.add("Harpo");
for (int i = 0 ; i< myFavouriteNames.size() - 1; i++){
if (((String)myFavouriteNames.get(i)).endsWith("o")){
System.out.println("Hey, you got a cool name!!");
}
}
Take a look at the if’s condition: why do we have to include that String casting? Because the compiler doesn’t know that you’re dealing with Strings, so have to tell him first in order to use a String’s specific method such as endsWith.
Generics’ main benefits
So, when using generics, we get these two things for free:
- Type safety: now you can be sure that all the objects in your container will be of the right type. Compiler guaranteed:
- They’ll save you a lot of explicit castings.
Generics | Examples |
Type-safe collections | var myFavouriteNumbers = new ArrayList<Integer>(); myFavouriteNumbers.add(“Groucho”) // compilation ERROR |
Less castings | var myGenericFavouriteNames = new ArrayList<String>(); for (String name : myGenericFavouriteNames){ if (name.endsWith(“o”)) { // NO CASTING NEEDED //… } } |
Going a bit further
Imagine that, for any good reason, you want to build a specific container of objects, that fit your specific needs. How would we do it? Let’s see if Groucho can help us:
Just for fun, let’s create a club. But not just a club, but one that would meet even Groucho’s standards.
Our club is going to be one of the most arbitrary, unfair, surrealist and elitist clubs in the history of clubs. We are going to set a policy of admission to join our club. This policy is going to be based on that wonderful Douglas Adams’ book called The hitchhiker’s guide to the Galaxy, which endowed some kind of magic powerful meaning to the number 42. So, if the candidate’s hashcode is less than 42, you’ll be welcome in our club. Otherwise, you won’t be allowed to join our elite.
So first, let’s create a generic class for our club:
public class MyClub<T> {}
What was that T about? Well, it’s just a convention, so you can call it whatever you want. Just remember: in real code is preferable that you stick to the convention of a letter, such as T , given that other programmers are probably used to this naming habit.
This parameterized type, T, is just a placeholder of an actual type that our class MyClub will use inside itself. In our example, whenever the compiler finds the T type inside the MyClub class, it will replace it by a real Java type, defined on instantiation, as so:
var myClub = new MyClub<String>(); //all appearances of T in MyClub will be replaced by String
var myOtherClub = new MyClub<Integer>(); //all appearances of T in MyClub will be replaced by Integer
From now on, the compiler will know that everytime he’s dealing with myClub reference, it’s a MyClub container of String candidates. Likewise, it will also know that myOtherClub is a container of Integers.
Let’s write some more code for our dear old club:
package com.makeitsimpletoday;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class MyClub<T> {
private List<T> clubMembers = new ArrayList<>();
private final int COOLEST_NUMBER= 42;
public void join(T toBeAdmitted){
if (toBeAdmitted.hashCode()<COOLEST_NUMBER){
System.out.println("Welcome, Mr. " + toBeAdmitted + ":
now you're part of our club");
clubMembers.add(toBeAdmitted);
}else{
System.out.println("How do you dare, Mr." +
toBeAdmitted + ", trying to enter our club with a
hashcode of " + toBeAdmitted.hashCode() + "? Get
out of here!");
}
}
public Optional<T> kickOut(T toBeKickedOut){
if (clubMembers.contains(toBeKickedOut)){
System.out.println("Sorry, Mr. " + toBeKickedOut +":
you've offended the ruler of the universe:" +
"I'm afraid we'll have to let you go.");
var poorGuy = clubMembers.stream().filter(
m->m.equals(toBeKickedOut))
.findFirst();
clubMembers.remove(toBeKickedOut);
return poorGuy;
}else {
System.out.println(toBeKickedOut + " is not a member
of our club");
return Optional.empty();
}
}
}
As you can see in the code above, generics are very flexible, allowing you to use the parameterized type wherever a regular type should be expected:
- In the class definition: public class MyClub<T>.
- In a method signature, both as a parameter or as a return type.
- In a method, as a local variable type.
- As an instance member of the class.
There is more to talk about generics, for sure, but I’ve run out of time and space today. Maybe some other time. But, for today, let’s enjoy our clubs!