paint-brush
What are Generics in Java and How do they work? Explained with Examplesby@shahzaibkhan
432 reads
432 reads

What are Generics in Java and How do they work? Explained with Examples

by Shahzaib KhanOctober 12th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Generics were introduced in version J2SE 5.0 of Java in 2004. It is a pure compile-time concept. Generics allow developers to sort an array of objects and then invoke integer, double, and sting arrays to sort the elements. The Generics concept is called 'Generics' and is called a 'pure compile time concept' Generics offer us a cleaner, less generic code, no compile time safety, no downcast of objects, no runtime exceptions like 'classCastException'

Company Mentioned

Mention Thumbnail
featured image - What are Generics in Java and How do they work? Explained with Examples
Shahzaib Khan HackerNoon profile picture


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.

Why do we need Generics?

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.

Generic Type and parameterized Type

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.

Diamond notation

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.


How the compiler treats Generics

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.

Restrictions in Generics

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.

How Generics in java is useful for developers?

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.