Generics were introduced in version J2SE 5.0 of Java in 2004. It is a pure compile-time concept. Before the Generics were not introduced, we had to downcast the <object type>
and specify it. There were no compile time safety and runtime exceptions like classCastException
comes into play. To remove these problems Java developers introduced generics. Simply put, by using the Generics concept, a developer can sort an array of objects and then simply invoke integer, double, and sting arrays to sort the elements.
Let’s find out why we need Generics with practical examples. Consider a class Java that has a private member of Type Object (Object class is the mother of all classes as every class is a child class of Object Class).
class Java{
private Object obj1;
public Object getObj1() {
return obj1;
}
public void setObj1(Object obj) {
this.obj1 = obj;
}
}
Let’s assume, a developer Adam creates an instance of the Java class and stores a Date object in its variable. The object of Date is taken from Java.sql.Date
Adam:
Java temporary = new Java();
temporary.setObj1(new Date(someTime)); // java.sql.Date
And another developer David sets a Date instance in the same project from Java.util.Date
instance.
David:
temporary.Obj1(new Date()); // java.uitil.Date
**
**
Now, if Adam tries to fetch date from the same temporary instance and tries to assign it to the Date variable of java.sql.Date
(since he used it earlier), he will get a classCastException
at run time.
Adam:
Date date = (Date)temp.get(); // java.sql.Date
We had to face these problem before Generics were introduced. Developers had to use an explicit downcast of objects and runtime exceptions like classCastExceptions
came into play.
To tackle these sorts of problems Java language designers were looking for a solution that can help them to deal with such exceptions at compile time. They eventually found the solution and added the Generics concept into Java version5.
Consider the same class Java defined in terms of Generics.
class Java<T>{
private T obj1;
public T getObj1() {
return obj1;
}
public void setObj1(T obj) {
this.obj1 = obj;
}
}
**
**
Here <T>
is a parameterized type.**
**
Now when Adam creates an instance of class Java and stores an object of Date from java.sql.Date.
Adam:
Java<Date> temporary = new Java<Date>(); // java.sql.Date
temporary.setObj1(new Date(someTime));
And later David tries to set the same instance (temporary) with a new Object of Date from java.util.Date
David:
temporary.setObj1(new Date()); // java.util.Date
If David tries to do so he will get a compile-time error. As temporary can store only objects of Date from java.sql.Date
In this way, Generics offer us compile-time safety, much cleaner, less generic code, no downcast of objects, and no runtime exceptions. This is why Generics is called a pure compile-time concept.
Class Java<T> { …. } is a generic type where T in Java<T>
is a Type parameter of this Generic type and it is a formal type parameter.
Now if we create an instance of the Temp class.
Java<Date> temporary = new Java<Date>(); // java.sql.Date
Here in this statement Java<Date>
is parameterized and Date is an actual parameter that ensures that temporary (instance of Java) can hold only Date-type objects which come from java.sql.Date
If anyone tries to store any other object in temp, they will get a compilation error.
We can also create an instance of the Java class as follows:
Java<Date> temporary = new Java<>();
In this line of code, we do not have to specify the actual type parameter on the right side of creation. The compiler does that implicitly for us and this is known as diamond notation.
As we learned, Generics are tackled by the compiler at compile time. When we compile our code, the compiler does two things for us: 1- Type erasure and 2- implicit cast. Let's look at the same example to understand what they are:
public class main {
public static void main(String[] args) {
Java<String> temporary = new Java<>(); // Diamond notation
temporary.setObj1("java version 5");
System.out.println(temporary.getObj1());
}
}
class Java<T>{
private T obj1;
public T getObj1() {
return obj1;
}
public void setObj1(T obj) {
this.obj1 = obj;
}
}
When we compile this code, the compiler uses a term called Type Erasure.
As the name suggests type erasure basically removes all the generic notation and replaces all class-level type parameters with Object Type. Like in our case when we compile the code, all generic notation will be removed and class-level type parameters will be replaced with Object.
Here is what the code looks like after the Type erasure:
public class main {
public static void main(String[] args) {
Java temporary = new Java();
temporary.setObj1("java version 5");
System.out.println((String)temporary.getObj1());
}
}
class Temp{
private Object obj1;
public Object getObj1() {
return obj1;
}
public void setObj1(Object obj) {
this.obj1 = obj;
}
}
**
**
As we can see, the compiler does implicit cast in the print statement and uses a type eraser to remove and replace generics notation.
There are some restrictions in generics as well and we have to be familiar with them.
The first restriction is we can not use primitive types as Type arguments in Generics.
For example, we can not do
Java<int> temporary = new Java<int>();
Second restriction is that we can not use a class-level type parameter in a static context.
For example, we can not do the following:
class Java<T>{
private static T obj1;
public T getObj1() {
return obj1;
}
public void setObj1(T obj) {
this.obj1 = obj;
}
}
This will give a compile time error.**
**
Third restriction is that methods can not be overloaded and have the same reference type after Type erasure.
For example, we can not do the following:
void menu(LinkedList<String> l1){}
void menu(LinkedList<Integer> l1){}
This is not method overloading as after Type erasure comes into play this piece of code will look like this:
void menu(LinkedList l1){}
void menu(LinkedList l1){}
Type erasure removes all generic notation. Now as you can see both functions are the same and hence no overloading concept is required here. There are a few more restrictions but these are the most important ones.
We developed a strong understanding of generics. We know we can deal with runtime exceptions at compile time itself as generics provide us strong type-checking, and compile-time safety, and also we do not need to use explicit cast as the compiler knows what type is to be stored. So, with this, we have much cleaner, readable, and more generic code. Generics provide the ability for developers to develop generic algorithms. In Java, today use of many features would not be possible without generics.