Generics

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.

follow us on