1) Introduction
The feature of Generics in Java allows Applications to create classes and objects that can operate on any defined types. Programmers can now make use of the Generics feature for a much better code. There is no need for un-necessary casting when dealing with Objects in a Collection. This article provides a detailed overview of Generics and its usage in different context with samples. To start with, it illustrates the need for Generics and the difficulties faced by the Developers before its origin. It will explain in detail on how to write Generic Classes, Generic Methods and so on. Then the various aspects of Bound Constraints and Wild-cards will be discussed.
also read:
Java Generics
In this section let us explore the basis of Generics programming. As mentioned in the introductory section, Generics programming enables Classes and Methods to operate on well defined parametric types allowing clients to substitute a suitable Java type at the compile time. This prevents un-necessary casting being done in the Application code and to prevent any wrong data-type being used. To make things clear, consider the following statements.
Map contacts = new HashMap(); contacts.put(new Long(9912345678L), "Jenny"); contacts.put(new Long(9912345679L), "Johny"); Set contactValues = contacts.entrySet(); Iterator contactIterator = contactValues.iterator(); while (contactIterator.hasNext()) { Map.Entry anEntry = (Map.Entry)contactIterator.next(); // Line A Long number = (Long)anEntry.getKey(); // Line B String name = (String)anEntry.getValue(); // Line C System.out.println(number + ":" + name); }
The above code populates a Map
, keyed with mobile phone for a person name. The next pieces of code try to iterate over the Map
, thereby printing the data within it. Look at the casts down at lines A, B and C. Even though we know that we are going to add phone numbers (which is of type Long) and a person name (probably a String) into the Map, we are doing an explicit cast to get the appropriate data. The other problem we find here is, what if the client put some other data other than the Long
data-type for the Key.
contacts.put(new String("9912345678L"), "Jenny");
The above code will definitely raise an exception at the run-time. So, we find two major dis-advantages in the older code (code compiled with java 1.4 compiler or before). One is the need to have absurd cast that is being spread across the code. The other thing is that there is no procedural mechanism through which we can prevent wrong data-type being added to the above Collection.
The solution to the above problems is having Generics in the programming code. Let us see how the above code is re-written using Generics.
Map contacts = new HashMap(); contacts.put(new Long(9912345678L), "Jenny"); contacts.put(new Long(9912345679L), "Johny"); Set<Map.Entry> contactValues = contacts.entrySet(); Iterator<Map.Entry> contactIterator = contactValues.iterator(); while (contactIterator.hasNext()) { Map.Entry anEntry = contactIterator.next(); Long number = anEntry.getKey(); String name = anEntry.getValue(); System.out.println(number + ":" + name); }
Let us analyze what has happened in the above code. The declaration Map contacts = new HashMap();
has now changed to Map contacts = new HashMap();
The former is called a Raw Map and the latter is an example of a Generic Map. If we look at the declaration of the Map
and the HashMap
classes, we will find something similar to the following,
public interface Map { ... }
The above declaration can be interpreted as : Map
has two parametric types called K
(meaning Key) and V
(meaning Value). The name of the parametric types can be anything, that doesn’t matter. These changes to Map
and all its related collection classes is there right from Java 5.0. So, whenever a clients references a Map
interface, it cannot plainly do like the following,
Map mapObject = new HashMap();
The compiler (Java 5.0) as soon as encountering this statement will issue a warning telling that, the declaration of Map
is a raw-type and its references should be parameterized. Since the declaration of the Map
interface is now parameterized with Keys and Values in the form of , the client referencing the Map
should provide a suitable type for the parametric types. Going back to our code,
Map contacts = new HashMap();
We want the key for the Map
to be of type Long
and the value for the corresponding Key to be of type String
. We also have parameterized the HashMap
class with Long and String, since the class declaration for HashMap
has also changed.
public class HashMap implements Map { … }
The usage of parametric types has not only affected Map
and HashMap
but all the collection related classes and interfaces in the java.util
package. In our code, contacts are made to populate in the map object by calling the Map.put()
method. Now, the call to Map.entrySet()
will return a Set
containing entries which is of type Map.Entry
. Now let us have a look over the Map.entrySet()
method,
Set<Map.Entry> entrySet();
The return type of Map.entrySet()
is a Set
which is parameterized with Map.Entry
. Map.Entry
is a class that will store an entry, which is nothing but a combination of Key and value. Note that Map.Entry
is again parameterized with K
(for Key) and V
(for Value). In our case, the key is of type Long
and the value is of type String
, we have something like the following in the later part of the code,
Set<Map.Entry> contactValues = contacts.entrySet();
The same thing applies for Iterator
which is parameterized with Map.Entry
which is again parameterized with Long
and String
. While traversing over the elements within the while loop, we have statements like the following,
... Map.Entry anEntry = contactIterator.next(); Long number = anEntry.getKey(); String name = anEntry.getValue(); ...
Since we well know that the Iterator
is typed with Map.Entry
, there is no need for an explicit type-cast. Same is the case for Map.getKey()
and Map.getValue()
. Since the Map
has been parameterized with Long
and String
, it is not possible to add types other than the defined types. So now the following code will raise a compilation error.
contacts.put(new String(""), new Long(1L));
This ensures type-safe programming and it prevents the client code from adding any wrong data-type to the Collection.
3) Writing Generic Classes
Let us see how to create our own classes using Generics. Let us keep the purpose of the class as simple as holding some Object, the Object Holder class. This Object Holder class can hold any type of Java object. It provides method for getting and setting the current Object. The following shows the class declaration of the Object Holder class.
ObjectHolder.java
package generics.classes; public class ObjectHolder { private O anyObject; public O getObject() { return anyObject; } public void setObject(O anyObject) { this.anyObject = anyObject; } public String toString() { return anyObject.toString(); } }
Note the following syntax,
public class ObjectHolder
The above statement essentially says that we wish to make the Object Holder class as a Generic Class. Technically, Object Holder is now a parametric class and O
is called a type parameter. O
serves as a place-holder for holding any type of Object. Note the usage of the type parameter within the class declaration. Now, the clients can use the above class by substituting any kind of object for the place-holder O. Consider the following Client program that makes use of the Object Holder class.
ObjectHolderClient.java
package generics.classes; import java.net.URL; public class ObjectHolderClient { public static void main(String[] args) throws Exception { ObjectHolder stringHolder = new ObjectHolder(); stringHolder.setObject(new String("String")); System.out.println(stringHolder.toString()); ObjectHolder urlHolder = new ObjectHolder(); urlHolder.setObject(new URL("https://javabeat.net")); System.out.println(urlHolder.toString()); } }
Note how the Clients instantiates an instance for the Object Holder class.
ObjectHolder stringHolder = new ObjectHolder();
This is called type substitution. For the type parameter O
, the type String is substituted. And now the calls to ObjectHolder.setObject(O anyObject)
and O ObjectHolder.getObject()
can be imagined as ObjectHolder.setObject(String anyObject)
and String ObjectHolder.getObject()
.
Now, let us see another example of Generic classes having two or more parametric types. Assume that we want to represent a class that holds a Composite object along with the elements of the composite object. We can see this kind of Composiste – children relation-ship in a number of places. For example, a Folder containing multiple files or a Window containing a number of UI Components and so on.
Following is the representation of the Container With Elements (ContainerWithElements
) class holding a Container object with its children. Note that the class typed parameters have the names Container and Elements respectively for holding the Container object and its child elements.
ContainerWithElements.java
package generics.classes; import java.util.List; public class ContainerWithElements { private Container outerObject; private List innerObjects; public ContainerWithElements(Container outerObject, List innerObjects) { this.outerObject = outerObject; this.innerObjects = innerObjects; } public Container getOuterObject() { return outerObject; } public void setOuterObject(Container outerObject) { this.outerObject = outerObject; } public List getInnerObjects() { return innerObjects; } public void setInnerObjects(List innerObjects) { this.innerObjects = innerObjects; } }
The Client program that makes use of the above class is given below. Note the usage of the classes Folder, File, Window and Button are from the generics.classes package and they have nothing to do with the genuine java.* classes.
ContainerWithElementsClient.java
package generics.classes; import java.util.Arrays; public class ContainerWithElementsClient { public static void main(String[] args) { ContainerWithElements folderWithFiles = new ContainerWithElements( new Folder(), Arrays.asList(new File(), new File())); ContainerWithElements windowWithButtons = new ContainerWithElements( new Window(), Arrays.asList(new Button(), new Button())); } }
For code completeness, the declaration of the classes Folder, File, Window and Button is given below.
class Folder{} class File{} class Window{} class Button{}
4) Writing Generic Methods
In the previous section, we saw how to write Parameterized Classes. Now, let us spend time in exercising Parameterized Methods or Generic methods in this section. A Generic class containing a type parameter affects the entire class, but a generic method containing one or more type parameters affects that particular method only. So it means that a non-generic class can contain a mixture of generic and non-generic methods.
Following code snippet shows how to declare a Generic method.
GenericMethods.java
package generics.methods; public class GenericMethods { static void printType(T anyType) { System.out.println(anyType.getClass().getName()); } public static void main(String[] args) { GenericMethods.printType(String.class); GenericMethods.printType(new String("")); } }
If we look at the way in which a Generic method is declared, we find that the static method printType()
has a return type void and it takes a single parameter called T
. Here T
stands for any parametric type which can be substituted with any of the Java types by the Clients. Since we have introduced a parameter T
, it should be defined. But where? It should be defined in the method definition itself just before the return type of the method ().
The moral is whenever we have different type parameters in a method, it should be defined in the method definition. For example, consider the following method that has two type parameters A and B and they are defined before the return type of the method separated by commas.
void aGgenericMethod(A aType, B bType) { // Something here. }
But the same type parameter can de used multiple number of times in the parameter list. For example, the type paramter A
is defined once but used multiple times in the following code,
void aGgenericMethod(A aType, A anotherType, B bType) { // Something here. }
5) Wildcards
The following sections explain the usage of Wild-card character in Generics. For example, consider the following,
strObjects.add(new String("A String")); strObjects.add(new String("Another String"));
The above line declares an Array List with a type being the String type. The declaration says that the list (strObjects
) in this case, can hold objects only of type java.lang.String
. No other type is permitted other than java.lang.String
. So, the following statements are legal as they are adding objects of type java.lang.String
.
List anotherStrObjects = strObjects;
Now, consider the following assignment statement which assigns the object reference strObjects
to anotherStrObjects
. Now anotherStrObjects
is pointing to strObjects
reference.
List
The above is a perfectly legal statement as the assignment happens between the same type. Now, consider the following,
LObject someObject = new String("");
The above statement will lead to a compiler error telling that “Cannot convert from List to List<Object”. Though String
is a sub-class of the Object class, this type of conversion is not possible in Generics, but the following code compiles well.
List objects = strObjects;
The reason why it is not possible to assign the List containing strings to a List that can hold Objects can be given justification as follows. The main goal of having Generic code in a program is to ensure type-safety. It means that the expected Application types and the types sent by the Clients should match. There should not be any deviation or mis-match in their types. If we go back to the code, on the left-hand side, we have a List
that can hold Objects represented by List
objects. This tells to the compiler that this List can hold objects of type java.lang.Object
. Now we are making this object List to point to some other list whose parametric type is String
. Though String
is a concrete sub-class of Object
, this assignment is not possible.
Let us see how to get over this problem. Consider the following code,
Object someObject = new String("");
The character '?'
is a wild-card character and it stands for any Java type. It can be java.lang.Object
type or some other type. It is just a place-holder that tells that it can be assigned with any type. Considering this case, the following are now valid syntaxes.
List anyObjects = null; List integers = new ArrayList(); anyObjects = integers; List doubles = new ArrayList(); anyObjects = doubles;
The above code snippet tries to assign a list of integers and a list of doubles to the reference anyObjects
. This is perfectly legal. Another strange thing has to be considered is while adding elements into the collection. Since the type parameter for the reference anyObjects is '?'
, no objects can be added to the collection, not even java.lang.Object
. The only exception is that only null
can be added to these type of collection.
anyObjects.add(new Integer(1)); // Wont compile anyObjects.add(new Double(1.0)); // Wont compile anyObjects.add(null); // This will compile.
6) Bounding the Parameterized Types
Generics won’t be complete if this section is not covered. It is all about bounding parametric types. Till now, we have seen parametric types operate on a single java type like Object
or String
. Now, let us see how the parametric types can be restricted by applying some constraints over them. For this to be illustrated, let us take a sample application called Animal Actions.
Animal Actions class performs various operations on a given Animal like: making them eat, sleep and run. The first constraint that we see here is that only an Animal
object can be passed to the Animal Actions class. Let us represent the Animal class as follows.
Animal.java
package generics.bounds; public abstract class Animal { // Some common functionalities here. }
Note that the Animal
class is declared as abstract, meaning that some other concrete class is going to extend this Animal
class. Another restriction that we see in Animal Actions class is that they will make the Animals to sleep, eat and run. Since these are behaviors and we may give different representations for the same, let them be modeled as interfaces. Following code shows the interface design for the behaviors,
package generics.bounds; interface Sleepable { public void sleep(); } interface Runnable { public void run(); } interface Eatable { public void eat(); }
Let us give implementation of the above behaviors for some animal, say Dog
. The following code snippet is for the implementation of Dog
which conforms to eating, sleeping and running behavior.
Dog.java
package generics.bounds; public class Dog extends Animal implements Sleepable, Runnable, Eatable { public void sleep() { System.out.println("Dog is sleeping"); } public void run() { System.out.println("Dog is running"); } public void eat() { System.out.println("Dog is eating"); }
Now, let us design the Animal Actions class. The restriction we have on Animal Actions class is that, we should operation on any type of object that is an Animal which can eat, sleep and run. Look into the following Animal Actions class,
AnimalActions.java
package generics.bounds; public class AnimalActions<a> { private A animal; public AnimalActions(A animal) { this.animal = animal; } public A getAnimal() { return animal; } public void setAnimal(A animal) { this.animal = animal; } public void doActions() { animal.sleep(); animal.run(); animal.eat(); } }</a>
The declaration of the parameterized class looks like the following,
public class AnimalActions
Let us break down the pieces in the above declaration. The first trivial stuff that has to be noted is the declaration of the typed parameter A
(which is for Animal
). The next set of expressions are imposing restrictions on the typed parameter A
. The phrase 'A extends Animal'
tells that whatever type we pass for the substitution parameter must extend/implement the Animal class/interface. The type that comes after extends can either be an interface or a class. It is illegal to mention something like the following,
public class AnimalActions
Only extends
keyword is used for both class as well the interface and not the implements
keyword. If we want the parametric type to confirm by more than one classes or interfaces, then every types should be separated by the symbol &
. For example, in our case, we want some Animal to eat, sleep and run by implementing the Eatable
, Sleepable
and Runnable
interface. So, we have declared something like AnimalActions<A extends Animal & Sleepable & Runnable & Eatable
. To sum up things, the generic class declaration can be interpreted like this; it can be passed with any type that implements/extends Animal
, Sleepable
, Runnable
and Eatable
types.
Following code snippet makes use of the above Animal Actions class. In the below code, a new instance of Dog
object is created and passed on to the constructor of the Animal Actions class. This is perfectly valid as the Dog class extends the Animal class and it also implements Sleepable
, Eatable
and Runnable
interfaces. It then makes a call to AnimalActions.doActions()
, thereby the execution gets directed towards Dog.sleep()
, Dog.eat()
and Dot.run()
.
AnimalActionsTest.java
package generics.bounds; public class AnimalActionsTest { public static void main(String[] args) { AnimalActions animal = new AnimalActions(new Dog()); animal.doActions(); } }
7) More on parametric bounds and wild-cards
The restriction on parametric types that is applied on class definition is also applicable to method definition. Let us move towards assignment now. Consider the following statement,
List animals = new ArrayList();
The above is a declaration of list that essentially tells to the compiler that it can hold Animal
objects. So the following statements is perfectly valid.
animals.add(new Animal()); // Fine.
Assuming that Animal
class is not abstract. Not only it is possible to add Animal
objects but also any types that extends the Animal
class. By having this rule in hand, it is perfectly possible to have the following statements,
animals.add(new Dog()); // This will work too. animals.add(new Cat()); // This also.
Even though, it is possible to add any type that extends the Animal
class, it is not possible to have the following statement.
List dogs = new ArrayList(); dogs.add(new Dog()); dogs.add(new Dog()); animals = dogs; // This wont compile.
The compiler will warn you telling that it is not possible to convert List
to List
. Even a type-casting on the above statement doesn’t work.
animals = (List<Animal>)dogs; // This won't work.
Since type-casting from one type to another type is a run-time operation and during run-time there is no such existence of the types List
or List
because of erasures. All the typed-parameters won’t be available in the class-file and the above code doesn’t work.
7.1) Upper Bound
The solution to this situation is the usage of wild-cards along with parametric bounding. Have a look over the following declaration.
List animals = new ArrayList();
It tells that a list is being declared with type being anything (?) that is extending the Animal
class. Though it looks very similar to the above declaration it has some differences. The first thing is that it is now possible for the animals reference to point to a list that is holding any sub-type of Animal
objects.
List dogs = new ArrayList(); dogs.add(new Dog()); dogs.add(new Dog()); animals = dogs;
One important difference is that, it is not possible to add elements to the animals list, though the only exception is adding null
elements. This is called the Upper Bound for the animals list.
7.2) Lower Bound
Similar to Upper Bounds, we have Lower Bounds with the following syntax,
List dogs = new ArrayList();
This declaration tells that along with Dog
type, all other sub-types of Dog
is also allowed. Not any of the super-type of Dog
can be added including java.lang.Object
. So, if we have classes like GoodDog
and BadDog
, both extending the Dog
class, then the following statements are legal.
dogs.add(new Dog()); dogs.add(new GoodDog()); dogs.add(new BadDog());
8) Conclusion
One of the major language and syntax change in Java 5.0 is Java Generics. Through Generics, it is now possible to have a programming model that operates on some parametric type and the same model can be re-used with different types. It is important to note that there will be only one class file for the whole Generic type, unlike other programming languages (in the name of templates in C++). Generics makes use of the concept of Erasure which erases all the Generic type information during the compilation process.