In this post, we will see about Java Generics tutorial with a lot of examples.
Table of Contents
Generics in java
‘Generics’ is a term that is quite common nowadays. Grammatically, it a collective of the word ‘generic’ which means a certain characteristic which is not very specific and can be used to denote a certain range or class of things. As we know that Java, being an object oriented programming language, is greatly influenced by concepts drawn from real life examples, let us try to bring the concept of Generics with respect to Java.
Let us take the idea of generics from a simple real life example. I am sure all of us have had the misfortune of opening a box of cookies and discovering a bunch of sewing supplies, at least once. So, let us say that the cookie box is a generic instance that can take different parameters(cookies or sewing supplies) and behaves differently(as a cookie box or a box of sewing supplies) based on the type of parameter received from the user.
Generics are used mostly for compile type safety and avoid chances of ClassCasteException.
Let’s say we have below code which does not use Generics.We need to create a list which should contain only Integer type value.
1 2 3 4 5 6 7 |
List integerList=new ArrayList(); integerList.add(new Integer(20)); Integer i=(Integer) integerList.get(0); integerList.add(new String("Twenty")); // Valid Integer i2=(Integer) integerList.get(1); |
At line 4, when we retrieve value from list, we need to cast it again to integer. This is kind of annoying for a programmer to cast every time when you retrieve the value from list.
At line 5, we are able to add String to integerList!! that’s bad, we did not want that.When you retrieve value integerList at line 6, you will give us ClassCastException.
Let’s implement generics in above code.
1 2 3 4 5 6 7 |
List<Integer> integerList=new ArrayList<Integer>(); integerList.add(new Integer(20)); Integer i=integerList.get(0); integerList.add(new String("Twenty")); // compile time error Integer i2=(Integer) integerList.get(1); |
As you can see, we don’t need to cast at line no.4. We are not able to add String to integerList, that’s great.That was the requirement.As a result of this, we won’t get classCastException anymore.
Why Generics?
Let us say, that we must sort a list of numbers, characters, and strings based on the input provided by the user. In this situation, the “primitive” or “non-generic” approach would be to write three functions with the same sorting technique(say Bubble sort) for three different inputs, or to Type cast all the inputs as String and sort as a String array, which would again become an inefficient process as we have to type cast every input to String before comparison.
Would it not be far more efficient and less time consuming, if we could use one sorting function for all those inputs without having to type cast every time?
Well that is exactly what Generics in Java is designed to do – allow the programmer the freedom of implementing more by writing less lines of code.
Advantages of using Generics over non-generic practices
- Strong Type Safety – The Java Compiler performs stringent checks on the generic type code at compile time, and evaluates whether the type assignment to a particular generic code is allowed or not. This prevents a large number of possible runtime errors and ClassCastExceptions which could halt the processing of the application. As Compile Time checks are easier to track and resolve, using Generics reduces the work of a programmer.
- No need of Type Casting – Without the use of Generics, using different types of data for processing would require type casting multiple times. Generics solves this issue by providing a common field for all types to be used.
- Creation of efficient generalized algorithms – As discussed in the previous section, the use of generics allows the programmer to write lesser lines of code and achieve much more. Writing reusable code for different types using generics is a giant leap forward from non-generic or primitive algorithms.
How does Generics in Java work?
Generics in Java works on two main principles – “Type Safety” and “Type Erasure”. In the previous sections, we have already discussed that Generics ensures type safety by performing stringent type checks at compile time rather than allowing the code to compile and allowing run time errors or exceptions like “ClassCastException” to occur.
Type Erasure on the other hand, works in a way similar to using an eraser. During compilation, the source code which contains generics, is removed from the generated Byte Code and replaced with the declared type itself, similar to legacy code prior to java 1.5 where generics was introduced as a concept. This allows backward compatibility with legacy non-generic code.
Generics in Java and their classification
Generics in Java can be classified under two headers –Classes and Methods. We shall discuss each type further in detail in the following sections.
Classes as Generics
Classes form the base of all structures in Java and encapsulate all the attributes and behaviors into a single unit. Generally, we declare classes as a type themselves which we can use as object variables. However, what if there was a need for us to create such a class that behaves differently with different types of data.
Let’s say, a different behavior with Strings than with Integers. It is for this purpose that classes can be declared as a generic and made to behave differently with different type parameters.
How do we declare a Generic Class?
- Generic Classes can be declared like any other class declaration in Java, with the addition of the generic type arguments after the class name declaration. Similar to the type parameters in Generic methods, these too can contain multiple type arguments separated by commas.
- The Generic classes are often called ‘Parameterized Classes’ because these classes accept the type parameters during object creation.
The declaration formation for the Generic class is as follows:
class
A sample declaration is:
1 2 3 |
public class GenClassTest<T>{} |
Let us use the above declaration in an example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class GenClassTest<T> { private T data; public T getData() { return data; } public void setData(T data) { this.data = data; } public static void main(String[] args) { GenClassTest<Integer> gcInt = new GenClassTest<Integer>(); GenClassTest<String> gcStr = new GenClassTest<String>(); System.out.println("Invoking the class with Integer"); gcInt.setData(5); System.out.println("Integer Data: "+gcInt.getData()); System.out.println("Invoking the class with String"); gcStr.setData("Five"); System.out.println("Integer Data: "+gcStr.getData()); } } |
On executing the above code, the following output is generated:
Integer Data: 5
Invoking the class with String
Integer Data: Five
Methods/Functions as Generics
As we discussed in the previous section, a single generic method can be written which can be invoked using different types of arguments. Depending on the type of arguments passed to the method, the compiler handles each method call as a separate call.
Writing Generic Methods in Java is not as an insurmountable task as it seems. On the contrary, it is quite simple. It is just necessary to remember certain key points while declaring and using generics.
How do we declare a Generic Method?
A Generic Method can be declared like any other method declaration in Java, keeping in mind the following rules:
- They must have a type parameter part of the method signature that is marked by a pair of angular brackets ( < and > ) and this part is placed right before the method return type declaration
- Every type parameter section of the declaration can contain more than one type variables and they are delimited by the use of commas. It is an identifier that defines a generic type name.
- Type parameters can be used as the return type of the method too and tend to funtion like Placeholders based on the type of arguments passed to the method
- Generic method type parameters deal with Reference Types(Objects) only, and cannot support primitive type data(int, long, float, double etc)
So, a sample method prototype for a Generic Method would be:
1 2 3 |
public static <E> void printData(E inputList) |
Let us see the above method prototype as a complete example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package org.arpit.java2blog; public class GenMethodTest{ public static <E> void printData(E[] inputList){ for(E current:inputList){ System.out.print(current+" "); } System.out.println("\n"); } public static void main(String[] args) { Integer aInt[]={1,2,3}; String aStr[]={"Hello","World"}; Character aChar[]={'G','O','O','D'}; System.out.println("Integer Data:"); printData(aInt); System.out.println("String Data:"); printData(aStr); System.out.println("Character Data:"); printData(aChar); } } |
On executing the above code, the following output is obtained:
1 2 3String Data:
Hello WorldCharacter Data:
G O O D
Generics WildCards
You can use ? with generics. It represents unknown type.
Let’s first understand why do you require it.
1 2 3 4 |
List<Integer> integerList=new ArrayList<Integer>(); List<Number> numberList=integerList; |
Do you think above code will compile. It is quite intuitive to say "OfCourse" but you will get compile time error at line number 2.
Let’s understand why.
If above code is allowed, you will be able to add Double, Float to numberList that means you are able to add Double and Float to integerList and this will break the concept of generics.
1 2 3 4 5 6 |
public static void printList(List<Object; list) { System.out.println(list.toString()); } |
and calling function has below code.
1 2 3 4 5 6 |
List<Integer> integerList=new ArrayList<Integer>(); integerList.add(4); integerList.add(6); printList(integerList); |
You will get compilation error at line number 5 in above code.As you can not pass List to the function which takes List<Object> as parameter.You can use wildcard(?) to solve above problem.When you use wildcard, you can pass any type of list to the function.
1 2 3 4 5 |
public static void printList(List<?> list){ System.out.println(list.toString()); } |
You can pass all types of list to printList function.Types of wildcards
Unbound wildcards
This is called unknown type wildcard.Above example is example of Unbound wildcards only.
Upper bounded wildcard
You want to write a method that can take List<Integer>, List<Double> and List<Long> but not List<String>. Below diagram will it clearer
You can write this method as below.
1 2 3 4 5 |
public static void printList(List<? extends Number> list){ System.out.println(list.toString()); } |
It means you can pass all types of list which are subclass of Number class such as Integer, Double and Long but you can not pass any other type of List.
Syntax:
1 2 3 |
CollectionType<? extends A> |
Lower bounded wildcard
You want to write a method that can take List<Integer>, List<Number> but not List<Double>.Below diagram will it clearer
You can write this method as below.
1 2 3 4 5 6 |
public static void printList(List<? super Number> list) { System.out.println(list.toString()); } |
It means you can pass all types of list which are super class of Integer class such as Number, Object but you can not pass any other type(Such as Double) of List.
Syntax:
1 2 3 |
CollectionType<? super A> |
Working with ‘Bounded Type Parameters’
Now that we have discussed Generics and Type Parameters, we have understood that using Generic Type Parameters allows the programmer to set any reference type to a type parameter. But what if we wanted to restrict that freedom and only allow certain types?That is taken care of by using Bounded Type Parameters.Since Java is governed by objects, each reference type has a parent object to which it belongs. To bind the type of parameters, we declare the type parameter followed by the extends keyword (or implements in case of interfaces ) followed by the upper bound class or interface.Let us see an example of using bounded type with the following code which finds the minimum of two numbers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class FindSmaller { public static <T extends Comparable<T>> T findSmaller(T a,T b){ //accepts only comparable objects if(a.compareTo(b)<0) return a; else return b; } public static void main(String[] args) { // TODO Auto-generated method stub System.out.println("The Smaller between 5 and 6 is: "+findSmaller(5,6)); System.out.println("The Smaller between Hello and World is: "+findSmaller("Hello","World")); } } |
The Output for the above code is:
Limitations of Generics
Throughout this article we have seen that Generics is a glorified savior that was introduced to rescue us from writing longer pieces of source code. However, like every good thing, even Generics has certain limitations.
- A Generic Type member or variable cannot be declared as static.
- A Generic Type cannot be instantiated by itself- i.e. – We cannot instantiate the generic type.
1234567public class Example<E>{Example(){new E(); //NOT ALLOWED}} - Generics only work with Objects and Wrapper Classes of Primitive Types but not the primitive types themselves
- Generic Types cannot be used for creating Custom Exceptions.
- Generics do not allow sub-typing
123List<Dog> = new ArrayList<Animal>(); //NOT ALLOWED - Generic Arrays cannot be instantiated/created but can be declared.
123E[] arr; // ALLOWEDE[] arr = new E[10]; // NOT ALLOWED
That’s all about Java Generics tutorial.