Java generics is a concept that provides a way to check type-safety during compile-time. Type safety is an important concept in catching program errors that might be caused by using a wrong type at compile time. Using generics, a Java class, interface or method can declare classes and interfaces that it operates on as parameters. This way, such classes, interfaces and methods can be reused with different kinds of input classes without requiring code changes or handling separate inputs in complicated if-else blocks. The parameter is always a type which corresponds to a Java class.
Generic Classes
Generic classes are defined as follows:
class <classname><Type1, Type2, …, TypeN> {…}
Here Type1, Type2 …TypeN are the parameterized types for the generic class. The number of parameters is optional. Using generic classes, the same class can be reused with different kind of parameters for different purposes.
For example, let’s consider the following class.
public class Container {
private Object object;
public void set(Object object) {
this.object = object;
}
public Object get() {
return object;
}
}
This class is non-generic and contains an object that can be operated on. Let’s consider it is used by another object to perform mathematical operations.
public class MyClass {
public int getDoubleContent(Object object) {
Container container = new Container();
container.set(object);
if (container.get() == null) {
container.set(new Integer(0));
}
return ((Integer) container.get()) * 2;
}
}
This method would be perfectly legal and would not cause any compile time issues. However, if we invoke the following code
MyClass myClass = new MyClass();
myClass.getDoubleContainer(“abc”);
then we would have a ClassCastException. Let’s see how this happens.
MyClass first initializes a Container object and sets the member variable object. Following this, Container object’s get() method is called to get the object. If the object is null, its value is initialized to 0 and set. At the end of the method, double the object’s value is returned. This method expects get() method of Container to return an Integer object. But in this example, the input to the method is a String object that cannot be cast to an Integer. This is how the problem occurs and in the end, the input causes a runtime bug.
If we had a compile-time way to check for the type, we would not be running into this issue. Java generics were developed to prevent runtime bugs like this by turning them into compile-time bugs and catching them before the program is run.
Using generics, we can make the Container object a generic container.
public class Container<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
Declaring a parameterized class is called generic type declaration. Now, Container is a generic class. A generic class is initialized the same way a non-generic class is initialized except that its parameter type is replaced with a Java class. This concept is called generic type invocation. For example, a Container for an Integer would be Container<Integer> and a Container for a Person would be Container<Person>. Using generic type invocation, an interface, method or class operating on Container can initialize a Container object with the appropriate type.
Now, let’s change our example to use the generic Container class.
public class MyClass {
public int getDoubleContent(Object object) {
Container<Integer> container = new Container<Integer>();
container.set(object);
if (container.get() == null) {
container.set(0);
}
return ((Integer) container.get()) * 2;
}
public static void main(String [] args) {
MyClass myClass = new MyClass();
myClass.getDoubleContent(1);
}
The output would be 2.
In this example, if we tried passing “abc” as an argument to getDoubleContent() method, we would get a compiler error. Therefore, the runtime bug that might occur is turned into a compile-time error and prevented before the code is run.
Type parameters are typically named with single uppercase letters, like T.
What is type erasure?
Java generics were introduced for compile-time checks. During compilation, Java erases all the generic information and compiles the code as if no parameter types were declared. The metadata about the generic class is used in the code for cast operations that will be used at runtime. This is known as type erasure.
For example, let’s take the generic Container class we defined earlier. This class would be compiled into something as follows.
Public class Container {
private Object object;
public void set(Object object) {
object = (Integer) object;
}
public Object get() {
return (Integer) object;
}
}
As seen in the example, the type information is erased and the metadata information about the class is used to cast the object variable to an Integer object when necessary.
Generic Methods
A method can be parameterized with types the same way a class is parameterized. Such methods are called generic methods. In a generic method declaration, the parameter list inside angle brackets becomes a separate component of the method signature and appears before the return type of the method. A generic method does not have to be declared in a generic class. A non-generic class may have generic methods and a generic class may have non-generic methods.
An example generic method signature is typically as follows.
public void <K> getContent(K k) {…}
Let’s see how this works with an example.
public class MyClass {
public <K> String getType(K k) {
return k.getClass().getName();
}
public static void main(String [] args) {
MyClass myClass = new MyClass();
String s = “Hello”;
Integer i = new Integer(2);
System.out.println(s + “, type: ” + myClass.getType(s));
System.out.println(i + “, type: ” + myClass.getType(i));
}
}
The output would be
Hello, type: java.lang.String
2, type: java.lang.Integer
Here, in this example, MyClass has a generic method named getType. The getType() method gets the type of any object that it accepts as an argument. In the main method, a new instance of MyClass is initialized and through this instance, getType method is invoked twice: once with a String and then once with an integer as an argument.
Generic Classes and Inheritance
Generic classes can be extended by their subclasses, just like non-generic classes, using extends keyword. The syntax for extending a generic class is as follows:
<class name> extends <generic class><parameters list>
The child class automatically becomes parameterized with the same parameters.