In this first part of a series of Java Generics tutorials, we’ll look at basic usage of generics with collection framework. This is the most common use case of generics. In later parts of the tutorial, you’ll learn advanced topics like how to create your own generic classes in Java.
What is Generics
Generics is a powerful mechanism which results in cleaner code which is robust and easy to understand. Generics allows you to bind classes and objects to certain types. The most common use of generics is with collection classes like ArrayList, HashMap etc.
If you’ve learned Java programming after Java version 5 or 6 then you might already be using generics and don’t know about it. If you’ve written this type of code then you are already using generics,
ArrayList<String> userNames = new ArrayList<String>();
The <String>
part is generics syntax. If you are not familiar with this syntax then we’ll see what it means and what advantages it provides.
If you are not familiar with collections framework in Java, its a set of Interfaces and Classes in the Java API. These classes provide you the ability to store multiple values like arrays but unlike arrays, the size of collections can change if more elements are added to them. Collection classes also provide you the ability to store key-value pairs.
Type Safe Collections
Using generics provides you type safety while using collections. Type safety in very simple terms is avoiding runtime exceptions by finding errors at compile time.
Let’s see an example of what the problem is and how generics solves it. Here is a simple piece of code that creates a List
adds some elements to the list and then retrieves the elements from the list to get their total.
When you run this code you’ll get a nice looking stack trace of a ClassCastException
as output.
The second element in the List is a String so when our code tries to type cast it to Integer, then JVM throws this exception. Generics provides you the power to ensure this doesn’t happen. The compiler will flag such a problem to you instead of an exception at runtime. Here’s how the code will look when generics is included.
numbers.add(10);
number.add("20"); //compile time error
int total = 0;
for(int i = 0; i < numbers.size(); i++) {
Integer number = numbers.get(i); //no cast needed
total += number.intValue();
}
This time the compiler knows that the numbers
list can only contain elements of Integer type. So when you try to add a String to this list the compiler will report that error. As you might have noticed, you don’t need a type-cast when you get an element from the list at line no. 6. This is because again the compiler knows that elements in numbers
List are of type Integer so the type-cast is automatically done by the compiler.
With an untyped List, you can add any type of elements to the list, and the compiler won’t complain about it. With generics the compiler would only let you add only objects of the type which you used while creating the list. Lets see a simple example,
//any object can be added to untypedList
untypedList.add("Hello World");
untypedList.add(123);
List<String> stringList = new ArrayList<String>();
//only String objects can be added to stringList
stringList.add("Hello World");
stringList.add(123); //compile time error, cannot add Integer to String List
The above code demonstrates that with untyped lists, the programmer has to ensure he/she is adding the right type of objects to a List. If a wrong element gets added to the List, you can get a ClassCastException
while retrieving elements from the List. But with generics, the compiler takes care of that. Like at line no. 8 we tried to add an Integer
object to a List which takes only String
objects. The compiler would disallow this making your life easier.
Using generics syntax makes your code more readable and avoids accidental addition of wrong objects into a collection. Like lets say you were writing a piece of code which took a List of Employee
class objects and prepared two lists of employee names (String
objects) and ids (Long
objects). Here’s the code without generics,
private String name;
private Long id;
public String getName() {
return name;
}
public Long getId() {
return id;
}
//name and id setters
}
..
public void manageEmployees(List employees) {
List empNames = new ArrayList(); //raw list
List empIds = new ArrayList(); //raw list
for(int i = 0; i < employees.size(); i++) {
Employee employee = (Employee)employees;
empNames.add(employee.getName());
empIds.add(employee.getName()); //did I do something wrong here?
}
//do something with empNames and empIds lists
}
All is fine with this code except that I accidentally added employee names to employee ID list. So I’ll get an exception at runtime when I try to use the information stored in the empIds
list as I’ll be expecting Long
object but I’ll get a String
object. If I were using generics, the compiler would have caught this. Lets see a generic version of this code,
List<String> empNames = new ArrayList<String>(); //List of Strings
List<Long> empIds = new ArrayList<Long>(); //List of Longs
for(int i = 0; i < employees.size(); i++) {
Employee employee = employees;
empNames.add(employee.getName());
empIds.add(employee.getName()); //compile time error
}
//do something with these two lists
}
In this code the compiler would flag an error at line no. 7 that you cannot add a String
object to a List
which only takes Long
objects. Such small things can save you hours of debugging.