How to write generic classes and methods in Java
- Details
- Written by Nam Ha Minh
- Last Updated on 14 June 2019   |   Print Email
This tutorial helps you write your own generic stuffs i.e. generic classes and generic methods in Java.
Why should we write generic code?
The reason I tell you to write generic code is that generic code helps you save time and effort, in terms of avoiding redundancy and creating more reusable and more general API.
Keep reading and you will see the power and robustness of writing generic code.
1. Writing Generic Classes in Java
Suppose that we are developing a database library called Data Access Object (DAO) for a program that manages resources in a university. We would write a DAO class for managing Students like the following code:
public class StudentDAO { public void save(Student student) { // code to save student details to database } public Student get(long id) { // code to get student details from database... // ...and return a Student object } }
This looks pretty fine, as this class does its job well: persisting Student objects to database.
Next, we need to persist Professor objects to database. We would write another DAO class like this:
public class ProfessorDAO { public void save(Professor professor) { // code to save professor details to database } public Professor get(long id) { // code to get professor details from database... // ...and return a Professor object } }
Hey, is this ProfessorDAO class similar to the StudentDAO class, isn’t it? What if new entity classes added to the system: class, course, faculty, etc? It looks like we will continue creating DAO classes whose code is almost identical. This creates redundancy and needs time to write similar classes each time a new entity added to the system.
So how will generics help us to avoid this redundancy and time-wasting?
Using generics, we can write a more general DAO class like the following:
public class GeneralDAO<T> { public void save(T entity) { // code to save entity details to database } public T get(long id) { // code to get details from database... // ...and return a T object } }
Here, T is called type parameter of the GeneralDAOclass. T stands for any type. The GeneralDAO class is said to be generified. The following code illustrates how to use this generic class:
GeneralDAO<Student> studentDAO = new GeneralDAO<Student>(); Student newStudent = new Student(); studentDAO.save(newStudent); Student oldStudent = studentDAO.get(250);
With this generic class, we can manage other entities easily without writing additional DAO classes, for example:
GeneralDAO<Professor> professorDAO = new GeneralDAO<Professor>(); Professor newProfessor = new Professor(); professorDAO.save(newProfessor); Professor oldProfessor = professorDAO.get(100);
As you have seen, we can write one generic class that works with different types. This removes redundancy and makes the code more readable, more reusable and more general. Also, when a new type is added, the generic class can be reused without writing additional code, thus saving times.
Here are some noteworthy points with regards to writing generic classes in Java:
- T is just a name for a type parameter, like a variable name. That means you can use any name you like for the type parameter. However, there are some conventions for naming type parameters in Java: T for type; Efor element; K for key; Vfor value, etc. And we should follow this convention.
- The type parameter can be used as an actual type in the body of the class. As you see in the GeneralDAO class, T is used as parameter type in the save() method and as a return type in the get() method.
- Type parameters are never added to the names of constructors or methods. They are used only in the names of classes and interfaces.
* Writing generic classes with more than one type parameter
Just like the generic Map<K, V>, we declare more than one type parameter when generifying a class. The type parameters are separated by commas.
For example, the following class is declared with two type parameters to represent a pair of things:
public class Pair<T, U> { T first; U second; public Pair(T first, U second) { this.first = first; this.second = second; } public T getFirst() { return first; } public U getSecond() { return second; } }
And the following class is generified with 3 type parameters:
public class Color<R, G, B> { R red; G green; B blue; public Color(R red, G green, B blue) { this.red = red; this.green = green; this.blue = blue; } }
* Using bounded type parameters when writing generic classes
In the above examples, we use the type parameter T which can be of any type. How can we restrict the type parameter to be subtypes of a concrete type? For example, we want to design the GeneralDAO class to work with only types that are subtypes of the Entity class (suppose Entity is the base type of all entities in the system). That means no one can declare GeneralDAO<Integer> or GeneralDAO<String> which we don’t expect.
Fortunately, Java generics provide the concept bounded type parameters that allow us to use the syntax <T extends X> to specify restrictions on definition of generic classes (X is a concrete type).
So we modify the GeneralDAO class as shown below:
public class GeneralDAO<T extends Entity> { public void save(T entity) { // code to save entity details to database } public T get(long id) { // code to get details from database... // ...and return a T object } }
Here, Entity is called the upper bound, which can be any class or interface. Remember the extendskeyword is used for both class and interface in the cased of bounded type parameter.
Now, with this bounded type definition, the GeneralDAO class can be used only work with sub types of Entity, not with every type. Hence the following code becomes illegal:
GeneralDAO<Integer> dao = new GeneralDAO<Integer>(); dao.save(new Integer());
because Integer is not a sub type of Entity.
Let’s see some more examples.
The following class is generified to work with only Swing components (JComponent is supertype of all Swing components):
public class Box<T extends JComponent> { private List<T> list; public void add(T comp) { list.add(comp); } }
The following generic class is designed to work with only types that are sub types of Shape:
public class Painter<T extends Shape> { public void draw(T shape) { // draw the shape } }
And the following generic class is a restricted version of a map. The key is restricted to only Number types, and value is restricted to only Runnabletypes:
public class MapThread<T extends Number, U extends Runnable> { private Map<T, U> map = new HashMap<T, U>(); public void put(T num, U thread) { map.put(num, thread); } }
* Using multiple bounds
We can use the syntax <T extends X & Y & Z> to define a generic class whose type parameter can be sub types of multiple types. Here, X, Y, Z can be classes and interfaces. Remember if there is a class, the class must be the first in the list.
For example, the following generic class is designed works with only types that are sub types of Runnableand JFrame:
public class WindowApp<T extends JFrame & Runnable> { T theApp; public WindowApp(T app) { theApp = app; } }
I recommend you to read the book Java Generics and Collections by Maurice Naftalin and Philip Wadler to understand deeply about generics in Java.
2. Writing Generic Methods
Like generic classes, we can write generic methods that are highly general and reusable. There are also some differences when writing generic methods.
First, let’s see how a non-generic method is converted to a generic one.
The following method counts number of occurrences of a Stringin an array of Strings:
public static int count(String[] array, String item) { int count = 0; if (item == null) { for (String s : array) { if (s == null) count++; } } else { for (String s : array) { if (item.equals(s)) { count++; } } } return count; }
Here’s an example usage of this method:
String[] helloWorld = {"h", "e", "l", "l", "o", "w", "o", "r", "l", "d"}; int count = count(helloWorld, "l"); System.out.println("#occurrences of l: " + count);
Output:
#occurrences of l: 3
Now, we need to count the occurrence of an element in an array of any type. To generify this method, replace the concrete type String with a type parameter T, hence the generic version looks like this:
public static <T> int count(T[] array, T item) { int count = 0; if (item == null) { for (T element : array) { if (element == null) count++; } } else { for (T element : array) { if (item.equals(element)) { count++; } } } return count; }
With this generic version, we can count occurrence of a number in an array of integers like this:
Integer[] integers = {1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0}; int count = count(integers, 0); System.out.println("#occurrences of zeros: " + count);
Result:
#occurrences of zeros: 8
Here are some noteworthy points with regard to writing generic methods in Java:
- The <T> is always placed before the return type of the method. It indicates that the T identifier is a type parameter, to distinguish it with concrete types.
- Of course we can use any name for the type parameter. However, T is a convention in Java and we should follow.
- Note that if the type parameter of a non-static generic method is same as the enclosing class, the indicator <T> is not required. The following class illustrates this point:
public class Util<T> { public int count(T[] array, T item) { // code... } }
The following code shows you the differences between the type parameters in the class and in the method:
public class Util<E> { public static <T> int count(T[] array, T item) { // code... } }
* Using bounded type parameters in generic methods:
Like generic classes, we can use bounded type parameters to restrict the types which can be accepted by the generic method.
Let’s refactor the count() method above to work with a collection instead of an array like this:
public static <T> int count(Collection<T> col, T item) { int count = 0; if (item == null) { for (T element : col) { if (element == null) count++; } } else { for (T element : col) { if (item.equals(element)) { count++; } } } return count; }
The following code shows how to use the bounded type parameter <T extends X> to update the method to accept only collection of sub types of Number:
public static <T extends Number> int count(Collection<T> col, T item) { // code... }
Here, Number is upper bound of the type parameter T. Remember the upper bound can be a class or an interface or both (the extendskeyword is also used for interface here).
Testing code:
List<Integer> integers = Arrays.asList(0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1); int count = count(integers, 1); System.out.println("#occurrences of 1s: " + count);
Result:
#occurrences of 1s: 6
The following code shows the count() method now accepts only types that are sub types of JComponent (class) and Runnable(interface):
public <T extends JComponent & Runnable> int count(Collection<T> col, T item) { }
And the following code shows how to write a generic method with two type parameters:
public static <T, U> void paring(T first, U second) { Map<T, U> map = new HashMap<T, U>(); map.put(first, second); }
* Using type wildcards in generic methods:
Do you remember the generic wildcards <? extends X> and <? super X> which you learned in the tutorial Generics with extends and super Wildcards and the Get and Put Principle? I mean that you can also use wildcards as same as bounded types. The count() method above can be rewritten using wildcards instead of bounded types like this:
public static int count(Collection<? extends Number> col, Number item) { int count = 0; if (item == null) { for (Number element : col) { if (element == null) count++; } } else { for (Number element : col) { if (item.equals(element)) { count++; } } } return count; }
A generic method can also use the wildcard <? super X>. Kindly refer to this tutorial to recall the differences between extendsand superwildcards. And remember that bounded types use only the extendskeyword.
Generally we can replace bounded types with wildcards in generic methods, but not in every case. Why? In the wildcard <? extends X>, ?is the type parameter to which cannot be referred in the method’s body; whereas in the bounded type <T extends X>, Tis the type parameter to which can be referred in the method’s body. So if we want to use the type parameter in the method’s body, use bounded types.
References:
Related Java Generics Tutorials:
- What are Generics in Java
- Generics with extends and super Wildcards and the Get and Put Principle
- Generics with Subtyping and the Substitution Principle
Other Java Collections Tutorial:
- Java List Tutorial
- Java Set Tutorial
- Java Map Tutorial
- Java Queue Tutorial
- Java Stream API Tutorial
- Understand equals and hashCode in Java
- Understand object ordering in Java
Comments