Generics: How They Work and Why They Are Important
Gain a solid understanding of generics in Java SE 8.
This article begins by providing a simple explanation of generics, along with presenting some basic concepts. After taking a look at the basic concepts, we will dive into some scenarios demonstrating the use of generics. Lastly, we will see how generics are an important ingredient for some constructs that are new to Java SE 8.
What Are Generics?
Consider the following scenario: You wish to develop a container that will be used to pass an object around within your application. However, the object type is not always going to be the same. Therefore, you need to develop a container that has the ability to store objects of various types.
Given this scenario, the most obvious way to achieve the goal would be to develop a container that has the ability to store and retrieve the Object type itself, and then cast that object when using it with various types. The class in Listing 1 demonstrates development of such a container.
Listing 1
public class ObjectContainer {
private Object obj;
/**
* @return the obj
*/
public Object getObj() {
return obj;
}
/**
* @param obj the obj to set
*/
public void setObj(Object obj) {
this.obj = obj;
}
}
Although this container would achieve the desired result, it would not be the most suitable solution for our purpose, because it has the potential to cause exceptions down the road, since it is not type-safe and it requires you to use an explicit cast whenever the encapsulated object is retrieved. The code in Listing 2 demonstrates how you would use this container to store and retrieve values.
Listing 2
ObjectContainer myObj = new ObjectContainer();
// store a string
myObj.setObj("Test");
System.out.println("Value of myObj:" + myObj.getObj());
// store an int (which is autoboxed to an Integer object)
myObj.setObj(3);
System.out.println("Value of myObj:" + myObj.getObj());
List objectList = new ArrayList();
objectList.add(myObj);
// We have to cast and must cast the correct type to avoid ClassCastException!
String myStr = (String) ((ObjectContainer)objectList.get(0)).getObj();
System.out.println("myStr: " + myStr);
Generics could be used to develop a better solution using a container that can have a type assigned at instantiation, otherwise referred to as a generic type, allowing the creation of an object that can be used to store objects of the assigned type. A generic type is a class or interface that is parameterized over types, meaning that a type can be assigned by performing generic type invocation, which will replace the generic type with the assigned concrete type. The assigned type would then be used to restrict values being used within the container, which eliminates the requirement for casting, as well as provides stronger type-checking at compile time.
The class in Listing 3 demonstrates how to create the same container that was previously created, but this time using a generic type parameter, rather than the Object type.
Listing 3
public class GenericContainer<T> {
private T obj;
public GenericContainer(){
}
// Pass type in as parameter to constructor
public GenericContainer(T t){
obj = t;
}
/**
* @return the obj
*/
public T getObj() {
return obj;
}
/**
* @param obj the obj to set
*/
public void setObj(T t) {
obj = t;
}
}
The most notable differences are that the class definition contains
To use the generic container, you must assign the type of the container by specifying it at instantiation using the angle bracket notation. Therefore, the following code would instantiate a GenericContainer of type Integer and assign it to the field myInt.
GenericContainer<Integer> myInt = new GenericContainer<Integer>();
If we tried to store a different type of object within the container that we've instantiated, the code would not compile:
myInt.setObj(3); // OK
myInt.setObj("Int"); // Won't Compile
Benefits of Using Generics
We've already seen examples demonstrating some of the benefits that come from using generics. Stronger type-checking is one of the most important, because it saves time by fending off ClassCastExceptions that might be thrown at runtime.
Another benefit is the elimination of casts, which means you can use less code, since the compiler knows exactly what type is being stored within a collection. For example, in the code shown in Listing 4, let's look at the differences between storing instances of our Object container into a collection versus storing instances of the GenericContainer.
Listing 4
List myObjList = new ArrayList();
// Store instances of ObjectContainer
for(int x=0; x <=10; x++){
ObjectContainer myObj = new ObjectContainer();
myObj.setObj("Test" + x);
myObjList.add(myObj);
}
// Get the objects we need to cast
for(int x=0; x <= myObjList.size()-1; x++){
ObjectContainer obj = (ObjectContainer) myObjList.get(x);
System.out.println("Object Value: " + obj.getObj());
}
List<GenericContainer> genericList = new ArrayList<GenericContainer>();
// Store instances of GenericContainer
for(int x=0; x <=10; x++){
GenericContainer<String> myGeneric = new GenericContainer<String>();
myGeneric.setObj(" Generic Test" + x);
genericList.add(myGeneric);
}
// Get the objects; no need to cast to String
for(GenericContainer<String> obj:genericList){
String objectString = obj.getObj();
// Do something with the string...here we will print it
System.out.println(objectString);
}
Note that when using the ArrayList, we are able to specify the type of the collection upon creation by using the bracket notation (
The concept of using generics with the Collections API leads us to one of the other benefits that generics provide: They allow us to develop generic algorithms that can be customized to suit the task at hand. The Collections API itself is developed using generics, and without their use, the Collections API would never be able to accommodate a parameterized type.
Digging into Generics
The following sections explore more features of generics.
How Can You Use Generics?
There are many different use cases for generics. The first example in this article covered the use case of generating a generic object type. This is a good starting point to learn about the syntax of generics at a class and interface level. Examining the code, the class signature contains a type parameter section, which is enclosed within angle brackets (< >) after the class name, for example:
public class GenericContainer<T> {
...
Type parameters, also known as type variables, are used as placeholders to indicate that a type will be assigned to the class at runtime. There may be one or more type parameters, and they can be utilized throughout the class, as needed. By convention, type parameters are a single uppercase letter, and the letter that is used indicates the type of parameter being defined. The following list contains the standard type parameters for each use case:
E: Element
K: Key
N: Number
T: Type
V: Value
S, U, V, and so on: Second, third, and fourth types in a multiparameter situation
In the example above, the T indicates that a type will be assigned, so GenericContainer can be assigned any valid type upon instantiation. Note that the T parameter is utilized throughout the class to indicate the type that is specified at instantiation. When the following line is used to instantiate the object, each of the T parameters is replaced with the String type:
GenericContainer<String> stringContainer = new GenericContainer<String>();
Generics can also be used within constructors to pass type parameters for class field initialization. GenericContainer has a constructor that allows any type to be passed in at instantiation:
GenericContainer gc1 = new GenericContainer(3);
GenericContainer gc2 = new GenericContainer("Hello");
Note that a generic that does not have a type assigned to it is known as a raw type. For instance, to create a raw type of GenericContainer, you could use the following:
GenericContainer rawContainer = new GenericContainer();
Raw types can sometimes be useful for backward compatibility, but it is not a good idea to use them in everyday code. Raw types eliminate type-checking at compile time, allowing code to become error-prone at runtime.
Multiple Types of Generics
At times, it is beneficial to have the ability to use more than one generic type in a class or interface. Multiple type parameters can be used in a class or interface by placing a comma-separated list of types between the angle brackets. The class in Listing 5 demonstrates this concept using a class that accepts two types: T and S.
If we look back to the standard type-naming conventions listed in the previous section, T is the standard identifier for the first type, and S is the standard identifier for the second. These two types are used to generate a container using generics for storage of multiple values.
Listing 5
public class MultiGenericContainer<T, S> {
private T firstPosition;
private S secondPosition;
public MultiGenericContainer(T firstPosition, S secondPosition){
this.firstPosition = firstPosition;
this.secondPosition = secondPosition;
}
public T getFirstPosition(){
return firstPosition;
}
public void setFirstPosition(T firstPosition){
this.firstPosition = firstPosition;
}
public S getSecondPosition(){
return secondPosition;
}
public void setSecondPosition(S secondPosition){
this.secondPosition = secondPosition;
}
}
The MultiGenericContainer class can be used to store two different objects, and the type of each object can be specified at instantiation. The container can be utilized as shown in Listing 6.
Listing 6
MultiGenericContainer<String, String> mondayWeather =
new MultiGenericContainer<String, String>("Monday", "Sunny");
MultiGenericContainer<Integer, Double> dayOfWeekDegrees =
new MultiGenericContainer<Integer, Double>(1, 78.0);
String mondayForecast = mondayWeather.getFirstPosition();
// The Double type is unboxed--to double, in this case. More on this in next section!
double sundayDegrees = dayOfWeekDegrees.getSecondPosition();
Type Inference and the Diamond Operator
As mentioned previously, generics can eliminate the requirement for casting. For example, using the MultiGenericContainer example shown in Listing 5, if getFirstPosition() or getSecondPosition() is called, then the field used to store the result would have to be of the same type as the object that was originally stored within the container at that position.
In the example shown in Listing 7, we see that the types assigned to the container at instantiation eliminate the requirement for casting when retrieving the values.
Listing 7
MultiGenericContainer<String, String> mondayWeather =
new MultiGenericContainer<String, String>("Monday", "Sunny");
MultiGenericContainer<Integer, Double> dayOfWeekDegrees =
new MultiGenericContainer<Integer, Double>(1, 78.0);
String mondayForecast = mondayWeather.getFirstPosition(); // Works fine with String
// The following generates "Incompatible types" error and won't compile
int mondayOutlook = mondayWeather.getSecondPosition();
double sundayDegrees = dayOfWeekDegrees.getSecondPosition(); // Unboxing occurs
Consider the third line of code in Listing 7, where no casting is required since the result of getSecondPosition() is being stored into a field of type double. How is this possible, since the MultiGenericContainer was instantiated using MultiGenericContainer
Note:
It is not possible to use primitive types with generics; only reference types can be used. Autoboxing and unboxing make it possible to store and retrieve values to and from primitive types when working with generic objects.
Type inference makes it possible to exclude the explicit cast when assigning the result of a getFirstPosition() or getSecondPosition() call. According to the Oracle documentation, type inference is the Java compiler's ability to look at each method invocation and corresponding declaration to determine the type argument (or arguments) that make the invocation possible. In other words, the compiler determines which types can be used based upon the types assigned during object instantiation, namely
Taking a look at our instantiation of MuliGenericContainer, type inference can also be used to eliminate the requirement for duplicating the type declaration. Instead of specifying the types for the object twice, the diamond operator, <>, can be specified as long as the compiler can infer the types from the context. As such, the diamond operator can be used when instantiating the object, as seen in Listing 8.
Listing 8
MultiGenericContainer<String, String> mondayWeather =
new MultiGenericContainer<>("Monday", "Sunny");
MultiGenericContainer<Integer, Double> dayOfWeekDegrees =
new MultiGenericContainer<>(1, 78.0);
What's My Target?
A concept known as target typing allows the compiler to infer the type parameters of a generic invocation. The target type is the data type that the compiler expects, depending on the types used to instantiate a generic object, where the expression appears, and so forth.
In the following code line, the target type of the value is Double, because the getSecondPosition() method returns a value of type S, where S is a Double in this case. As mentioned previously, due to unboxing, we are able to assign the value of the call to a primitive of type double.
double sundayDegrees = dayOfWeekDegrees.getSecondPosition();
Bounded Types
Oftentimes there are cases where we need to specify a generic type, but we want to control which types can be specified, rather than keeping the gate wide open. Bounded types can be used to restrict the bounds of the generic type by specifying the extends or the super keyword in the type parameter section to restrict the type by using an upper bound or lower bound, respectively. For instance, if you wish to restrict a type to a specific type or to a subtype of that specific type, use the following notation:
<T extends UpperBoundType>
Similarly, if you wish to restrict a type to a specific type or to a supertype of that specific type, use the following notation:
<T super LowerBoundType>
In the example in Listing 9, we take the GenericContainer class that was used previously and restrict its generic type to Number or a subclass of Number by specifying an upper bound. Note that this new class, GenericNumberContainer, specifies that the generic type must extend the Number type.
Listing 9
public class GenericNumberContainer <T extends Number> {
private T obj;
public GenericNumberContainer(){
}
public GenericNumberContainer(T t){
obj = t;
}
/**
* @return
the obj
*/
public T getObj() {
return obj;
}
/**
* @param obj the obj to set
*/
public void setObj(T t) {
obj = t;
}
}
This class will work well for restricting its field type to Number, and if you attempt to specify a type that does not fit within the bounds, as shown in Listing 10, a compiler error will be raised.
Listing 10
GenericNumberContainer<Integer> gn = new GenericNumberContainer<Integer>();
gn.setObj(3);
// Type argument String is not within the upper bounds of type variable T
GenericNumberContainer<String> gn2 = new GenericNumberContainer<String>();
Generic Methods
It is possible that we might not know the type of an argument being passed into a method. Generics can be applied at the method level to solve such situations. Method arguments can contain generic types, and methods can also contain generic return types.
Consider the case where we wish to develop a calculator class that accepts Number types. Generics could be used to ensure that any Number type could be passed as an argument to the calculation methods of this class. For instance, the add() method in Listing 11 demonstrates the use of generics to restrict the types of both arguments, ensuring that they contain an upper bound of Number:
Listing 11
public static <N extends Number> double add(N a, N b){
double sum = 0;
sum = a.doubleValue() + b.doubleValue();
return sum;
}
By restricting the type to Number, you can pass as an argument any object that is a subclass of Number. Also, by restricting the type to Number, we can be sure that any argument that is passed to the method will contain the doubleValue() method. To see it in action, if you wished to add an Integer and a Float, the method could be invoked as follows:
double genericValue1 = Calculator.add(3, 3f);
Wildcards
In some cases, it is useful to write code that specifies an unknown type. The question mark (?) wildcard character can be used to represent an unknown type using generic code. Wildcards can be used with parameters, fields, local variables, and return types. However, it is a best practice to not use wildcards in a return type, because it is safer to know exactly what is being returned from a method.
Consider the case where we would like to write a method to verify whether a specified object exists within a specified List. We would like the method to accept two arguments: a List of unknown type as well as an object of any type. See Listing 12.
Listing 12
public static <T> void checkList(List<?> myList, T obj){
if(myList.contains(obj)){
System.out.println("The list contains the element: " + obj);
} else {
System.out.println("The list does not contain the element: " + obj);
}
}
The code in Listing 13 demonstrates how we could utilize this method.
Listing 13
// Create List of type Integer
List<Integer> intList = new ArrayList<Integer>();
intList.add(2);
intList.add(4);
intList.add(6);
// Create List of type String
List<String> strList = new ArrayList<String>();
strList.add("two");
strList.add("four");
strList.add("six");
// Create List of type Object
List<Object> objList = new ArrayList<Object>();
objList.add("two");
objList.add("four");
objList.add(strList);
checkList(intList, 3);
// Output: The list [2, 4, 6] does not contain the element: 3
checkList(objList, strList);
/* Output: The list [two, four, [two, four, six]] contains
the element: [two, four, six] */
checkList(strList, objList);
/* Output: The list [two, four, six] does not contain
the element: [two, four, [two, four, six]] */
Oftentimes wildcards are restricted using upper bounds or lower bounds. Much like specifying a generic type with bounds, it is possible to declare a wildcard type with bounds by specifying the wildcard character along with the extends or super keyword, followed by the type to use for the upper bound or lower bound. For example, if we wanted to alter the checkList method to accept only Lists that extend the Number type, we could write that as shown in Listing 14.
Listing 14
public static <T> void checkNumber(List<? extends Number> myList, T obj){
if(myList.contains(obj)){
System.out.println("The list " + myList + " contains the element: " + obj);
} else {
System.out.println("The list " + myList + " does not contain the
element: " + obj);
}
}
Using Generics in Java SE 8 Constructs
We've seen how to use generics and why they are important. Now let's look at the use case for generics with respect to a new construct in Java SE 8, lambda expressions. Lambda expressions represent an anonymous function that implements the single abstract method of a functional interface. There are many functional interfaces available for use, and lots of them make use of generics. Let's take a look at an example.
Suppose we wanted to traverse over a list of book titles (Strings), and compare the titles so that we could return all titles that contained specified search words. We could do this by developing a method that accepts the list of book titles, along with the predicate that we wanted to use for performing the comparison. The Predicate functional interface can be used for comparison purposes, returning a boolean to indicate if a given object satisfies the requirements of a test. The Predicate interface can be used with objects of all types, because it has the following generic signature:
@FunctionalInterface
public interface Predicate<T>{
...
}
If we wished to traverse over each book title and look for those that contained the text "Java EE," we could pass contains("Java EE") as the predicate argument. The method shown in Listing 15 can be used to traverse a given list of book titles and apply such a predicate, printing out those titles that match. In this case, the accepted arguments are using generics to indicate a List of Strings and a predicate that will test each String.
Listing 15
public static void compareStrings(List<String> list, Predicate<String> predicate) {
list.stream().filter((n) -> (predicate.test(n))).forEach((n) -> {
System.out.println(n + " ");
});
}
The code in Listing 16 could be used to populate a list of book titles, and then print out all the book titles that contain the text "Java EE."
Listing 16
List<String> bookList = new ArrayList<>();
bookList.add("Java 8 Recipes");
bookList.add("Java EE 7 Recipes");
bookList.add("Introducing Java EE 7");
bookList.add("JavaFX 8: Introduction By Example");
compareStrings(bookList, (n)->n.contains("Java EE"));
public static void compareStrings(List<String> list, Predicate<String> predicate) {
list.stream().filter((n) -> (predicate.test(n))).forEach((n) -> {
System.out.println(n + " ");
});
}
Conclusion
Generics enable the use of stronger type-checking, the elimination of casts, and the ability to develop generic algorithms. Without generics, many of the features that we use in Java today would not be possible.
In this article, we saw some basic examples of how generics can be used to implement a solution that provides strong type-checking along with type flexibility. We also saw how generics play an important role in algorithms, and such is the case with the Collections API and functional interfaces, which are used for the enablement of lambda expressions.