Java 5 introduces Generics
to deal with Type Safety
, reduce bugs and an extra layer of abstraction over existing code.
it allows Types
(classes, interface) to be parameterised while defining the interface, class and method. The Type
parameter allows reusing the same code with different input types. They provide the compile time safety and catch invalid input at compile time only.
Generics
were primarily introduced for the collection framework to provide compile-time checking and avoid the very common ClassCastException. The whole collection framework was rewritten to make it compatible with Generics
and Type Safety
.
Java Generics same as Templates in C++
Generics provide 3 major benefits
- Type Safety
Generics
allows the Type
of object that needs to be saved or processed, so it checks at compile time only. for example, without Generics
, it was not possible to define and force List to take input of a particular Type
only, which might result in a Runtime Exception
.
package org.wesome.generics;
import java.util.ArrayList;
import java.util.List;
class Fruit {
public static void main(String args[]) {
List fruits = new ArrayList();
fruits.add("Fuji");
fruits.add(1); // ClassCastException
}
}
with Generics
, Java forces to define the input Type
of collection it checks the input at compile time only.
package org.wesome.generics;
import java.util.ArrayList;
import java.util.List;
class Fruit {
public static void main(String args[]) {
List<String> fruits = new ArrayList();
fruits.add("Fuji");
fruits.add(1); // ClassCastException
}
}
- Type Casting is not Required
Since Java had no way to define the input Types
, it used to consider everything as an object. in order to get the element back, a Typecasting
was required.
package org.wesome.generics;
import java.util.ArrayList;
import java.util.List;
class Fruit {
public static void main(String args[]) {
List list = new ArrayList();
list.add(1);
list.add(2);
for (Object integer : list) {
int temp = (int) integer;
System.out.println("integer is = " + temp);
}
}
}
since Generics
forces to validate input at compile time, no Typecasting
is required.
package org.wesome.generics;
import java.util.ArrayList;
import java.util.List;
class Fruit {
public static void main(String args[]) {
List<Integer> list = new ArrayList();
list.add(1);
list.add(2);
for (Integer integer : list) {
int temp = integer;
System.out.println("integer is = " + temp);
}
}
}
- compile-time type validation
Java could not validate the input Types
at Compiletime
, which result in a Runtime Exception
.
package org.wesome.generics;
import java.util.ArrayList;
import java.util.List;
class Fruit {
public static void main(String args[]) {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add("Apple");
}
}
since List has been forced to take input of Integer Type
only hence will throw Compiletime Exception
.
package org.wesome.generics;
import java.util.ArrayList;
import java.util.List;
class Fruit {
public static void main(String args[]) {
List<Integer> list = new ArrayList();
list.add(1);
list.add(2);
list.add("Apple");
}
}
Generics with Map
The map is not part of the collection framework, but it allows the Generics Types
to make sure compile-time input validation.
package org.wesome.generics;
import java.util.HashMap;
import java.util.Map;
class Fruit {
public static void main(String args[]) {
Map<Integer, String> appleMap = new HashMap();
appleMap.put(1, "Macintosh");
appleMap.put(2, "Fuji");
appleMap.put(3, "Gala");
appleMap.put(4, "Jonagold");
for (Map.Entry<Integer, String> entry : appleMap.entrySet()) {
System.out.println("key is = " + entry.getKey() + " value is " + entry.getValue());
}
}
}
Generic Type Parameters
The Generic
framework has some predefined characters to denote different Types
. it's not mandated to use these, but are adopted among programmers, so it's good to stay on the same page.
- T - Type of Interface or Class
- E - Element of return type or iteration
- K - Key of key-value pair
- N - Number
- V - Value of key-value pair
- S, U, V - more generic types
Generic Class
A class that can take different Types
of input is known as the Generic
class. symbol T is used to denote Generic Type
.
package org.wesome.generics;
class GenericClass<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public static void main(String[] args) {
GenericClass<String> stringGeneric = new GenericClass<>();
stringGeneric.setValue("Apple");
System.out.println("stringGeneric = " + stringGeneric.getValue());
GenericClass<Integer> integerGeneric = new GenericClass<>();
integerGeneric.setValue(1);
System.out.println("integerGeneric = " + integerGeneric.getValue());
}
}
User-defined Generic
classes also do compile-time input validation
package org.wesome.generics;
class GenericClass<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public static void main(String[] args) {
GenericClass<String> stringGeneric = new GenericClass<>();
stringGeneric.setValue(1); //compile time exception
}
}
There is no limitation on Generic Types
, a class can accept any number of Generic Types Parameter
as required.
package org.wesome.generics;
class GenericClass<T1, T2> {
private T1 t1Value;
private T2 t2Value;
public void setT1Value(T1 value) {
this.t1Value = value;
}
public void setT2Value(T2 value) {
this.t2Value = value;
}
public void show() {
System.out.println("T1 value = " + t1Value + " T2 value " + t2Value);
}
public static void main(String[] args) {
GenericClass<String, Integer> generic = new GenericClass<>();
generic.setT1Value("Apple");
generic.setT2Value(1);
generic.show();
}
}
A Generic
class can extend the class and implement an interface at the same time. in Multiple Bounds
, the Generic
class cannot have more than 1 class, so in GenericClass<T extends Apple & Fruit & Tree
Apple is a class whereas Fruit and Tree is an interface.
package org.wesome.generics;
interface Tree {
void show();
}
interface Fruit extends Tree {
void show();
}
class Apple implements Fruit {
public void show() {
System.out.println("i am an Apple");
}
}
class GenericClass<T extends Apple & Fruit & Tree> {
T type;
public T getType() {
return type;
}
public static <T extends Apple & Fruit & Tree> void show(T type) {
type.show();
}
public static void main(String[] args) {
GenericClass<Apple> appleGeneric = new GenericClass();
appleGeneric.show(new Apple());
appleGeneric.getType();
}
}
Generic Method
Just like the Generic Class
, methods can also accept Generic Arguments
. both static
and non-static
method allows Generic Parameters
. The Generic
method is processing the element hence denoted by E
.
package org.wesome.generics;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
class Apple {
public static <E> void show(List<E> elements) {
for (E element : elements) {
System.out.println("element is = " + element);
}
}
public <E> void show(E element) {
System.out.println("element is = " + element);
}
public static <E> List<E> listFromArrayUtility(E[] arr) {
return Arrays.stream(arr).collect(Collectors.toList());
}
public static void main(String[] args) {
List<String> appleList = Arrays.asList("Macintosh", "Fuji", "Gala", "Jonagold");
System.out.println("*------------Generic Instance Method------------*");
Apple apple = new Apple();
appleList.forEach(apple::show);
IntStream.rangeClosed(1, 5).forEach(apple::show);
System.out.println("*------------Generic Static Method------------*");
show(appleList);
show(IntStream.rangeClosed(1, 5).boxed().collect(Collectors.toList()));
System.out.println("*------------Generic Static Method------------*");
List<String> stringList = listFromArrayUtility(new String[]{"Macintosh", "Fuji", "Gala", "Jonagold"});
List<Integer> intList = listFromArrayUtility(new Integer[]{1, 2, 3, 4, 5});
}
}
Wildcard in Generics
Sometimes the requirement is to create a Generic Method
which will accept all the children or parent classes of a Type Class
. those Types
of classes are called wildcards and are denoted by ?
symbol. the wildcard allows us to define the Upper Bound
and Lower Bound
of the class.
Bound Type Parameters
There are some seniors where need to limit the data Type
of Generics
, for example, a list should accept arguments of certain Types
of subclasses of the Type
.
Unbound Wildcard
There are some situations where the Generic Method
should accept any Type
of parameter, in those cases, an Unbound
wild card is used. it's the same as
package org.wesome.generics;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
class GenericClass {
public static void show(List<?> type) {
type.forEach(System.out::println);
}
public static void main(String[] args) {
GenericClass generic = new GenericClass();
generic.show(IntStream.rangeClosed(1, 5).boxed().collect(Collectors.toList()));
}
}
Upper Bound
The Upper Bound
wild card makes sure all the accepted Type
parameters must follow the Upper Bound
. the Subtype T
should extends
or implements
the Upper Bound
. its syntax is ? extends SuperType
Upper Bound with Interface
Upper Bound
can be used with the interface, it defines that all the accepted parameters should implement the interface, its child interface or child class.
package org.wesome.generics;
interface Tree {
}
interface Fruit extends Tree {
}
class Apple implements Fruit {
}
class GenericClass<T extends Tree> {
public static void main(String[] args) {
GenericClass<Tree> treeGeneric = new GenericClass<>();
GenericClass<Fruit> fruitGeneric = new GenericClass<>();
GenericClass<Apple> appleGeneric = new GenericClass<>();
}
}
Upper Bound with Class
Upper Bound
can be used with class, in the below example, the only allowed argument for GenericClass
must have an extended Tree
class.
package org.wesome.generics;
class Tree {
}
class Fruit extends Tree {
}
class Apple extends Tree {
}
class GenericClass<T extends Tree> {
public static void main(String[] args) {
GenericClass<Tree> treeGeneric = new GenericClass<>();
GenericClass<Fruit> fruitGeneric = new GenericClass<>();
GenericClass<Apple> appleGeneric = new GenericClass<>();
}
}
Upper Bound with method
Generic Upper Bound
can also be used with methods as well. in the below example, the GenericClass
show method will only take those class Types
who has extends Tree
class.
package org.wesome.generics;
import java.util.Arrays;
import java.util.List;
class Tree {
void show() {
System.out.println("i am Tree");
}
}
class Fruit extends Tree {
void show() {
System.out.println("i am Fruit");
}
}
class Apple extends Fruit {
public void show() {
System.out.println("i am an Apple");
}
}
class GenericClass {
public void show(List<? extends Tree> type) {
type.forEach(Tree::show);
}
public static void main(String[] args) {
GenericClass generic = new GenericClass();
List<? extends Tree> fruits = Arrays.asList(new Tree(), new Fruit(), new Apple());
generic.show(fruits);
}
}
Lower Bound
The Lower Bound
wild card makes sure all the accepted Type Parameters
must follow the Lower Bound
. the accepted parameter will be a superclass
of Type T
. its syntax is ? super SubType
Lower Bound with Method
Upper Bound
can be used with class, in the below example, the GenericClass
show method will only take those class Types
whose super
is Tree
class.
package org.wesome.generics;
import java.util.Arrays;
import java.util.List;
class Tree {
@Override
public String toString() {
return "i am Tree";
}
}
class Fruit extends Tree {
@Override
public String toString() {
return "i am Fruit";
}
}
class Apple extends Fruit {
@Override
public String toString() {
return "i am Apple";
}
}
class GenericClass {
public void show(List<? super Tree> type) {
type.forEach(System.out::println);
}
public static void main(String[] args) {
GenericClass generic = new GenericClass();
List<? super Tree> fruits = Arrays.asList(new Tree(), new Fruit(), new Apple());
generic.show(fruits);
}
}
Type Erasure
Generics
were introduced to make sure the Type Safety
, so at the compile time, the compiler will remove all the Generic Parameters
to check Types
, this process is called Type Erasure
.
Type Erasure
will remove all the Generics Types
with their actual Object of the Type Bound
. hence the byte code of the program will contain the actual classes and interfaces.