08 Java - Classes (Part II - Generic Class)

Chetan DattaChetan Datta
7 min read

Introduction to Generic Classes

If we have an object with a field of type Object, then this object can hold any type of class because Object is the parent class of all classes. Therefore, when accessing the field of this class, we should be careful because it can potentially throw compile-time errors. Hence, we need to perform type checking or type casting before using the value. This is why we use generic classes to address this problem.

package learn.generics;

public class Print {
    Object value;

    public Object getValue() {
        return value;
    }

    public void setValue(Object value) {
        this.value = value;
    }
}
package learn.generics;

public class Main {
    public static void main(String[] args) {
        Print printObj = new Print();
        printObj.setValue(1);
        Object printValue = printObj.getValue();

        // We cannot use printValue directly,
        // we have to typecast it, else it will be compiled time error
        if ((int) printValue == 1){
            //do something
        }
    }
}

Usage of Generic Classes

package learn.generics;

public class Print<T> {
    T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

Generic Type (in above example <T>) can be any non-primitive object

package learn.generics;

public class Main {
    public static void main(String[] args) {
        Print<Integer> printObj = new Print<>();
        printObj.setValue(1);
        Integer printValue = printObj.getValue();

        if (printValue == 1){
            //do something
        }
    }
}

/*
we can also define like this also

Print<Integer> printObj = new Print<Integer>();

this will also work but redundant
*/

Inheritance with Generic Classes

Non Generic Class

package learn.generics;

public class Print<T> {
    T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

Inherited to ColorPrint

package learn.generics;

public class ColorPrint extends Print<String>{
}

Usage

package learn.generics;

public class Main {
    public static void main(String[] args) {
        ColorPrint colorPrint = new ColorPrint();
        colorPrint.setValue("hello");
    }
}

Generic Sub class

package learn.generics;

public class ColorPrint<T> extends Print<T>{
}

Usage

package learn.generics;

public class Main {
    public static void main(String[] args) {

        ColorPrint<String> colorPrint = new ColorPrint<>();
        colorPrint.setValue("hello");

    }
}

More than one of Generic types

class className<T>
class className<T1, T2, T3...Tn>
package learn.generics;

public class Pair<K,V> {

    private K key;
    private V value;

    public void put(K key, V value){
        this.key = key;
        this.value = value;
    }
}

usage

package learn.generics;

public class Main {
    public static void main(String[] args) {
        Pair<String, Integer> pairObj = new Pair<>();
        pairObj.put("Hello", 1);
    }
}

Generic Method

What if we conly wants to make Method Generic, not the complete Class, we can write Generic Methods too.

  • Type Parameter should be before the return type of the method declaration.

  • Type Parameter scope is limited to method only.

package learn.generics;

public class Pair<K,V> {

    private K key;
    private V value;

    public void put(K key, V value){
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

Example of generic method

package learn.generics;

public class GenericMethod {

    public <K,V> void printValue(Pair<K,V> pair1, Pair<K,V> pair2){
        if (pair2.getKey().equals(pair2.getKey())){
            //do something
        }
    }
}

/*
public class Test{
    public <T> void setValue(T value)
}

*/

Raw Type

It's a name of the generic class or interface without any type argument.

package learn.generics;

public class Print<T> {
    T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

usage

package learn.generics;

public class Main {
    public static void main(String[] args) {
        Print<Integer> parameterizeTypePrintObject = new Print<>();

        //Internally it passes object as parameterized type.

        Print rawTypePrintObject = new Print();
        rawTypePrintObject.setValue(1);
        rawTypePrintObject.setValue("hello");
    }
}

Bounded Generices

It can be used at generic class and method

  • Upper Bound (<T extends Number>) means T can be of type Number or its Subclass only. Here superclass (in this example Number) we can have interface too.

  • Multi Bound

Upper Bound

(Here the extends indicated the type of the class/Interface T can be not the actual inheritance)

package learn.generics;

public class Print<T extends Number> {
    T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

Multi Bound

<T extends Superclass & interface 1 & interface N>

  • The first restrictive type should be concrete class

  • 2, 3 and so on... can be interfaces

package learn.generics;

public class A extends ParentClass implements Interface1, Interface2{
}

multi bound usage

package learn.generics;

public class Print<T extends ParentClass & Interface1 & Interface2> {
    T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}
package learn.generics;

public class Main {
    public static void main(String[] args) {
        Print<A> obj = new Print<>();
        obj.setValue(new A());
    }
}

Negative scenario:

package learn.generics;

public class A extends ParentClass implements Interface1{
}
package learn.generics;

public class Print<T extends ParentClass & Interface1 & Interface2> {
    T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

This is Compilation error since A didn't implement interface 2.

package learn.generics;

public class Main {
    public static void main(String[] args) {
        Print<A> obj = new Print<>();
        obj.setValue(new A());
    }
}

Wildcards:

  • Upper bounded wildcard: <? extends UpperBoundClassName> i.e. class Name and below.

  • Lower bounded wildcard: <? Super LowerBoundClassName> i.e. class Name and above

  • Unbounded wildcard <?> only you can read. The type is nothing but Object.class

Problem without wildcard

public class Print {

    public void setPrintValues(List<Vehicle> vehicleList){

    }
}

public class Main {
    public static void main(String[] args) {
        List<Vehicle> vehicleList = new ArrayList<>();
        List<Bus> busList = new ArrayList<>();

        Print printObj = new Print();
        printObj.setPrintValues(vehicleList); //allowed
        printObj.setPrintValues(busList); //Not allowed
    }
}
// Vehicle is the parent class of Bus
List<Vehicle> vehicleList = new ArrayList<>();
List<Bus> busList = new ArrayList<>();

vehicleList = busList; 
vehicleList.add(new Car()); // Adding a Car to a list of Buses
Bus firstBus = busList.get(0); // This would cause ClassCastException

Solution using wild card

public class Print {

    public void setPrintValues(List<? extends Vehicle> vehicleList){

    }
}


public class Main {
    public static void main(String[] args) {
        List<Vehicle> vehicleList = new ArrayList<>();
        List<Bus> busList = new ArrayList<>();

        Print printObj = new Print();
        printObj.setPrintValues(vehicleList); //allowed
        printObj.setPrintValues(busList); //allowed
    }
}

Example

public class Print {

    public void setPrintValues(List<? super Vehicle> vehicleList){

    }
}

public class Main {
    public static void main(String[] args) {
        Print printObj = new Print();

        List<Object> objects = new ArrayList<>();
        printObj.setPrintValues(objects);
    }
}

Difference between Wild card (?) and Generic Type (T)?

  • In generic type it will force all the parameters of the same where as wild card it is flexible.

  • We cannot use super keyword in generic type method

  • In wild card we cannot have more question marks ?,?,? but in generic type we can have more no of types K,V,T

public class Print {

    //wild card method
    public void computeList(List<? extends Number> source, List<? extends Number> destination){

    }

    //Generic type method
    public <T extends Number> void computeList1(List<T> source, List<T> destination){

    }
}
public class Main {
    public static void main(String[] args) {

        List<Integer> wildCardIntegerSourceList = new ArrayList<>();
        List<Float> wildCardIntegerDestinationList = new ArrayList<>();

        Print printObj = new Print();

        printObj.computeList(wildCardIntegerSourceList, 
                                    wildCardIntegerDestinationList);

        // Error forcing us the parameters to be of same type 
        //hence compilation error
        printObj.computeList1(wildCardIntegerSourceList,
                                    wildCardIntegerDestinationList);
    }
}

Type Erasure

Whenever the byte code generated for Generic class all the generic types are replaced with actual types

Generic Class Type erasure

java class

public class Print<T> {
    T value;
    public void setValue(T val){
        this.value = val;
    }
}

byte code

public class Print {
    Object value;
    public void setValue(Object val){
        this.value = val;
    }
}

Generic Class Bound Type erasure

java class

public class Print<T extends Number> {
    T value;
    public void setValue(T val){
        this.value = val;
    }
}

byte code

public class Print {
    Number value;
    public void setValue(Number val){
        this.value = val;
    }
}

Generic Method Erasure

java class

public class Print {
    public <T> void setValue(T val){
        System.out.println("do something");
    }
}

byte code

public class Print {
    public void setValue(Object val){
        System.out.println("do something");
    }
}

Generic Bound Type Method Erasure

java class

public class Print {
    public <T extends Bus> void setValue(T val){
        System.out.println("do something");
    }
}

byte code

public class Print {
    public void setValue(Bus val){
        System.out.println("do something");
    }
}
10
Subscribe to my newsletter

Read articles from Chetan Datta directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Chetan Datta
Chetan Datta

I'm someone deeply engrossed in the world of software developement, and I find joy in sharing my thoughts and insights on various topics. You can explore my exclusive content here, where I meticulously document all things tech-related that spark my curiosity. Stay connected for my latest discoveries and observations.