Skip to content

Advanced Generics

1. Non-Generic Approach (Problem Illustration)

  • Usage:

  • Using Object type to store and retrieve values when the specific type is not known at compile time or needs to be flexible.

  • Points to Note:

  • The Print class (non-generic version) uses an Object field value.

  • Object is the superclass of all other classes in Java.
  • setPrintValue(Object value) can accept any object type (e.g., Integer, String) because they are all subclasses of Object.
  • getPrintValue() returns an Object. To use it as its actual type, explicit typecasting is required (e.g., (int)printValue, (String)printValue).
  • Without typecasting, direct use might lead to compile-time errors or require operations only available on Object.
  • Typecasting can lead to ClassCastException at runtime if the cast is incorrect.
  • Examples:
// Non-generic Print class
public class Print {
    Object value;
    public Object getPrintValue() {
        return value;
    }
    public void setPrintValue(Object value) {
        this.value = value;
    }
}

// Usage
public class Main {
    public static void main(String args[]) {
        Print printObj1 = new Print();
        printObj1.setPrintValue(1); // Here i am passing Integer 1
        Object printValue = printObj1.getPrintValue();
        // we can not use printValue directly, we have to typecast it, else it will be compile time error
        // Because it is a object, it can be anything, we need to typecast
        if (((int) printValue == 1)) {
            // do something
        }

        // Continuing with comments above, if we code setPrintValue("Hello");
        // then we need to typecast (String)printValue
    }
}

2. Generic Class

  • Usage:

  • To create classes that can work with different data types without sacrificing type safety.

  • The type parameter (e.g., <T>) acts as a placeholder for the actual type that will be specified when an object of the generic class is created.
  • Points to Note:

  • Syntax: public class ClassName<T> { ... } or public class ClassName<K, V> { ... } for multiple type parameters. The syntax is Dam Diamond syntax <>.

  • T (or any other valid identifier) is the type parameter.
  • The type parameter T can be used to declare instance variables, method parameters, and return types.
  • When instantiating a generic class, you specify the actual type argument: Print<Integer> p = new Print<Integer>();.
  • The "Diamond syntax" <> can be used for type inference if the compiler can determine the type: Print<Integer> p = new Print<>();.
  • Generic Type (in above example <T>) can be any non-primitive object type. (e.g., Integer, String, not int).
  • Using generics eliminates the need for explicit typecasting when retrieving values, providing compile-time type safety. Then we no longer need typecasting.
  • Examples:
// Generic Print class
public class Print<T> {
    T value;
    public T getPrintValue() {
        return value;
    }
    public void setPrintValue(T value) {
        this.value = value;
    }
}

// Usage
public class Main {
    public static void main(String args[]) {
        Print<Integer> printObj1 = new Print<Integer>(); // or new Print<>();
        printObj1.setPrintValue(1);
        Integer printValue = printObj1.getPrintValue(); // No cast needed
        if (printValue == 1) {
            //do something
        }
    }
}

// 3. More than one Generic Types Example
// Class with multiple generic types
public class Pair<K, V> {
    private K key;
    private V value;
    public void put(K key, V value) {
        this.key = key;
        this.value = value;
    }
    // ... getters for key and value
}

// Usage of Pair
public class Main {
    public static void main(String args[]) {
        Pair<String, Integer> pairObj = new Pair<>();
        pairObj.put("hello", 1243);
    }
}

3. Subclassing Generic Classes

3.1. Non Generic Subclass

  • Usage:

  • A non-generic class extends a generic class by providing a specific type argument for the superclass's type parameter.

  • Points to Note:

  • The subclass itself is not generic. It "fixes" the type parameter of the generic superclass.

  • Examples:
public class Print<T> {
    T value;
    // ... methods
}

public class ColorPrint extends Print<String>{
    // ColorPrint will always work with String for the Print superclass part
}

3.2. Generic Subclass

  • Usage:

  • A generic class can extend another generic class.

  • The subclass can pass its own type parameter(s) to the superclass or specify a concrete type.
  • Points to Note:

  • Syntax: public class SubClass<T> extends SuperClass<T> { ... }

  • The subclass uses the same type parameter T (or a different one) and passes it to the generic superclass.
  • The comment "//Then we don't need to specific the type" implies that the subclass remains generic.
  • Examples:
public class Print<T> {
    T value;
    public void setPrintValue(T value) { this.value = value; }
    // ... other methods
}

public class ColorPrint<T> extends Print<T>{ //Then we don't need to specific the type
    // This subclass is also generic and uses the same type T as Print
}

// Usage
public class Main {
    public static void main(String args[]) {
        ColorPrint<String> colorPrintObj = new ColorPrint<>();
        //Note that ColorPrint<String> colorPrintObj = new ColorPrint<String>();
        //is also correct
        colorPrintObj.setPrintValue("2");
    }
}

4. Generic Method

  • Usage:

  • Methods that have their own type parameters, independent of any class-level type parameters.

  • Useful when a method's logic is generic, but the class itself doesn't need to be generic, or the method needs different type parameters than the class.
  • What if we only wants to make Method Generic, not the complete Class, we can write Generic Methods too.
  • Points to Note:

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

  • Type Parameter scope is limited to method only.
  • The syntax is like `public void setValue(T object ){}
  • Can be static or non-static.
  • Examples:
// public class Pair<K, V> { private K key; public K getKey(){return key;} ... } // Assumed Pair class

public class GenericMethod {
    public <K, V> void printValue(Pair<K, V> pair1, Pair<K, V> pair2) {
        // Example usage: assuming Pair has getKey()
        if (pair1.getKey().equals(pair2.getKey())) { // Original OCR has pair2.getKey() twice. Corrected to pair1.
            //do something
        }
    }
}

// Another example from the IDE screenshot
// package Generics;
public class Print{ // Could be non-generic or generic itself
    public <T> void setValue(T busObject){ // T is a type parameter for this method
        //do something
    }
}

// Usage in Main (from IDE screenshot)
// package Generics;
// class Bus {} // Assumed
// class Car {} // Assumed
public class Main {
    public static void main(String args[]) {
        Print printObj = new Print();
        printObj.setValue(new Bus()); // Type T inferred as Bus
        // Here is 9th-line, we can change the parameter to new Car() as well, since setValue is a generic method
        printObj.setValue(new Car()); // Type T inferred as Car
    }
}

5. Raw Type

  • Usage:

  • Using a generic class or interface without specifying any type arguments.

  • It's a name of the generic class or interface without any type argument.
  • Points to Note:

  • Essentially bypasses generic type checking, behaving like pre-generics code (uses Object).

  • Leads to loss of type safety and requires manual casting, potentially causing ClassCastException at runtime.
  • Generally discouraged; generics were introduced to avoid this.
  • The comment //internally it passes Object as parametrized type. for Print<String> parametrizedTypePrintObject = new Print<>(); (when using diamond operator for inference) is distinct from a raw type instantiation.
  • For a raw type Print rawTypePrintObject = new Print(); //Raw type, i didn't pass the type, all Ts are treated as Object.
  • Examples:
public class Print<T> {
    T value;
    public void setPrintValue(T value) { this.value = value; }
    public T getPrintValue() { return value; }
}

public class Main {
    public static void main(String args[]) {
        // Parameterized type
        Print<String> parametrizedTypePrintObject = new Print<>();
        //internally it passes Object as parametrized type. (This comment is a bit confusing, it infers String)
        parametrizedTypePrintObject.setPrintValue("test");

        // Raw type
        Print rawTypePrintObject = new Print(); //Raw type, i didn't pass the type
        rawTypePrintObject.setPrintValue(1);
        rawTypePrintObject.setPrintValue("hello");

        // Object val = rawTypePrintObject.getPrintValue(); // Requires casting
    }
}

6. Bounded Generics

It can be used at generic class and method since sometimes we want to bound the type of T.

6.1. Upper Bound (<T extends Number>)

  • Usage:

  • Restricts the type parameter T to be a specific type (BoundType) or a subtype of BoundType.

  • BoundType can be a class or an interface.
  • <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.
  • Points to Note:

  • Syntax: public class ClassName<T extends Number> { ... }

  • Here Number is a superclass in java like String which has the type: Integer, Double, BigInteger, BigDecimal,... (Note: String is not a superclass of Number types; Number is the superclass of Integer, Double etc.)
  • So now T only can be the type of Number can be.
  • If BoundType is an interface, extends is still used (not implements).
  • Notice that there are three similar things:

    1. If we create a superclass and a subclass, then we use subclassName extends superclassName
    2. If we create a interface and implement it in a class, then we use className implements interfaceName
    3. If we create a generic class and bound it, then we use genericClassName<T extends BoundedClassName>
    4. Examples:
// Upper Bound
// Class with upper bounded type parameter
public class Print<T extends Number> {
    T value;
    public void setPrintValue(T value) { this.value = value; }
    public T getPrintValue() { return value; }
}

public class Main {
    public static void main(String args[]) {
        // Now here is a correct example
        // Correct
        Print<Integer> parametrizedTypePrintObject = new Print<Integer>();

        // Incorrect example (if T is bounded by Number)
        // Print<String> parametrizedTypePrintObject = new Print<>(); // Compile error if String is not a Number
    }
}

6.2. Multi Bound (<T extends ClassA & InterfaceB & InterfaceC>)

  • Usage:

  • Restricts the type parameter T to be a subtype of a class AND implement one or more interfaces.

  • Points to Note:

  • Recall that Java don't allowed a subclass extends more than one superclass, but it do allow a class to implements more than one interface by override.

  • Multi Bound: <T extends Superclass & interface1 & interface N>
  • The first restrictive type should be concrete class.
  • 2,3 and so on... can be interfaces.
  • The class (if present) must be listed first in the bound list.
  • You can have at most one class in the bound.
  • You can have multiple interfaces.
  • Examples:
class ParentClass {}
interface Interface1 {}
interface Interface2 {}

// Class 'A' conforming to bounds
public class A extends ParentClass implements Interface1, Interface2{
}

// Generic class with multi-bound
public class Print<T extends ParentClass & Interface1 & Interface2> {
    T value;
    public T getPrintValue() { return value; }
    public void setPrintValue(T value) { this.value = value; }
}

public class Main {
    public static void main(String args[]) {
        //This is allowed
        A obj = new A();
        Print<A> printObj = new Print<>();
        printObj.setPrintValue(obj);
    }
}

//Now if class A be this
//public class A extends ParentClass implements Interface1 {
//}
//Then in the Main function it will have error since print class need to implement two interface

7. Wildcards

7.1. Wildcards Introduction & Invariance

  • Usage:

  • Used to create more flexible generic methods or variables when the exact type argument is unknown or needs to vary.

  • Represented by ?.
  • Points to Note:

  • Generics are invariant. This means List<A> is not related to List by subtyping, even if A is a subtype of B.

  • Therefore, List<Vehicle> vehicleList = busList; //NO is invalid if Bus extends Vehicle.
  • Similarly, busList = vehicleList; //no is invalid.
  • However, direct object assignment Vehicle vehicleob = busObj; // ok is valid due to polymorphism.
  • Line 26 (vehicleob = busObj;) is valid because parent class Vehicle can keep the object of a child class Bus.
  • Line 17 (vehicleList = busList;) and 19 (busList = vehicleList;) are not valid since list of vehicle is not a parent of list of bus also because vehiclelist can have object Bus and Car.
  • Examples:
// package Generics;
import java.util.ArrayList;
import java.util.List;

class Vehicle {}
class Bus extends Vehicle {}
class Car extends Vehicle {}

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

        List<Bus> busList = new ArrayList<>();

        // vehicleList = busList; //NO (invariance)
        // busList = vehicleList; //no (invariance)

        Vehicle vehicleob = new Vehicle();
        Bus busobj = new Bus();
        vehicleob = busobj; // ok (polymorphism)
    }
}
// Futhermore
// public class Print{
//     public void setPrintValues(List<Vehicle> vehicleList){}
// }
// Here we fix the type of list be Vehicle, then we cannot setPrintValues(busList) since busList is List<Bus>

7.2. Upper Bounded Wildcard (<? extends Type>)

  • Usage:

  • Represents an unknown type that is a subtype of Type (or Type itself). i.e. class Name and all its child class below.

  • Useful when you want to read items from a generic collection but don't need to add to it. (Producer Extends - PECS).
  • Points to Note:

  • List<? extends Vehicle> means a list of some specific type that extends Vehicle.

  • You can safely get elements out of such a list and treat them as Vehicle.
  • You generally cannot add elements to such a list (except null), because the compiler doesn't know the exact subtype.
  • Examples:
import java.util.List;
class Vehicle {}
// class Bus extends Vehicle {} // Assumed

public class Print {
    // This method can accept List<Vehicle>, List<Bus>, List<Car>, etc.
    public void setPrintValues(List<? extends Vehicle> vehicleList) {
        for (Vehicle v : vehicleList) { // Can safely read as Vehicle
            System.out.println(v);
        }
        // vehicleList.add(new Bus()); // Compile Error: not safe
    }
}

7.3. Lower Bounded Wildcard (<? super Type>)

  • Usage:

  • Represents an unknown type that is a supertype of Type (or Type itself). i.e. class Name and all its super class above.

  • Useful when you want to add items to a generic collection. (Consumer Super - PECS).
  • Points to Note:

  • List<? super Vehicle> means a list of some specific type that is a supertype of Vehicle (e.g., List<Vehicle>, List<Object>).

  • You can safely add instances of Vehicle or its subtypes (like Bus) to this list.
  • When you retrieve elements, you can only be sure they are Object.
  • The example //printObj.setPrintValues(busList); //But this doesn't work (page 8 line 15) for List<? super Vehicle> is because List<Bus> is not a "list of a supertype of Vehicle". List<Object> or List<Vehicle> would work.
  • Examples:
import java.util.List;
import java.util.ArrayList; // Added for main example
class Vehicle {}
class Bus extends Vehicle {}

public class Print {
    public void setPrintValues(List<? super Vehicle> vehicleList) {
        vehicleList.add(new Vehicle());
        vehicleList.add(new Bus());
    }
}
public class Main { // From page 8, lines 10-15
    public static void main(String[] args) {
        Print printObj = new Print();
        List<Object> objList = new ArrayList<>();
        List<Vehicle> vehicleList = new ArrayList<>(); // Assuming vehicleList is initialized
        List<Bus> busList = new ArrayList<>(); // Assuming busList is initialized

        //Then in the main function the following will works
        printObj.setPrintValues(objList);
        printObj.setPrintValues(vehicleList);
        //But this doesn't work
        //printObj.setPrintValues(busList);
    }
}

7.4. Unbounded Wildcard (<?>)

  • Usage:

  • Represents a list (or other generic type) of an unknown type.

  • Used when the method's logic does not depend on the specific type of elements.
  • Unbounded wildcard <?> only you can read.
  • Points to Note:

  • Use this when we know that our method can mostly work on your objects. All the methods available in the Object class are applicable, as we know that every class in Java is a subclass of the Object class.

  • You can only read elements as Object.
  • You cannot add elements to a collection of type List<?> (except null).
  • Examples:
import java.util.List;

public class PrintUtils {
    //wild card method
    public void computeList(List<?> source) {
        Object ob = source.get(0); // Can get as Object
        // source.add("new element"); // Compile Error
    }
}

7.5. Wildcard Method vs. Generic Type Method

  • Wildcard Method:

  • Allows different concrete types for different parameters if they all conform to their respective wildcard bounds.

  • Example: public void computeList(List<? extends Number> source, List<? extends Number> destination)

    • source could be List<Integer> and destination could be List<Float>.
    • Can use super keywords in wild card method: public void computeList(List<? super Number> source, List<? extends Number> destination)
    • Generic Type Method:
  • Requires a relationship between the types of the parameters if they share the same type parameter.

  • Example: public <T extends Number> void computeList1(List<T> source, List<T> destination)

    • Both source and destination must be List of the same type T.
    • Cannot use super keyword for type parameter bounds (e.g., <T super Number> is illegal). But we cannot do this in generic type method.
    • Generic type can have more type: public <K,V,T,Z> void computeList1(List<T> source, List<T> destination)
    • From here we now the difference between generic type method and wild card method is that we can have different type.
    • Examples:
import java.util.List;
import java.util.ArrayList;
class Number {} // Simplified for example
class Integer extends Number {}
class Float extends Number {}

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[]) {
        Print printObj = new Print();
        List<Integer> wildCardIntegerSourceList = new ArrayList<>();
        List<Float> wildCardIntegerDesitanationList = new ArrayList<>(); // Corrected spelling from OCR

        printObj.computeList(wildCardIntegerSourceList, wildCardIntegerDesitanationList);
        // This would cause an error for computeList1 if T is bound to be the same for both
        // printObj.computeList1(wildCardIntegerSourceList, wildCardIntegerDesitanationList);
        // Correct usage for computeList1 would be:
        // printObj.computeList1(wildCardIntegerSourceList, wildCardIntegerSourceList);
    }
}

8. Type Erasure

Generic Class Erasure

  • Usage:

  • The process by which the Java compiler translates generic code into non-generic Java bytecode.

  • Points to Note:

  • //Byte code

  • //T will be replaced by object
  • For an unbounded generic class Print<T>, T is replaced by Object.
  • Examples:
// Original Generic Class (Unbounded)
public class Print <T> {
    T value;
    public void setValue(T val) {
        this.value = val;
    }
}
// After Type Erasure (Bytecode equivalent)
/*
public class Print {
    Object value;
    public void setValue(Object val) {
        this.value = val;
    }
}
*/

Generic Class Bound Type Erasure

  • Usage:

  • For bounded generic classes, the type parameter is replaced by its first bound.

  • Points to Note:

  • For Print<T extends Number>, T is replaced by Number.

  • Examples:
// Original Generic Class (Bounded)
public class Print<T extends Number> {
    T value;
    public void setValue(T val) {
        this.value = val;
    }
}
// After Type Erasure (Bytecode equivalent)
/*
public class Print {
    Number value;
    public void setValue(Number val) {
        this.value = val;
    }
}
*/

Generic Method Erasure

  • Usage:

  • For generic methods, type parameters are replaced by Object if unbounded.

  • Points to Note:

  • public <T> void setValue(T val) becomes public void setValue(Object val).

  • Examples:
// Original Class with Generic Method (Unbounded)
public class Print {
    public <T> void setValue(T val) {
        System.out.println("do something");
    }
}
// After Type Erasure (Bytecode equivalent)
/*
public class Print {
    public void setValue(Object val) {
        System.out.println("do something");
    }
}
*/

Generic Bound Type Method Erasure

  • Usage:

  • For bounded generic methods, type parameters are replaced by their first bound.

  • Points to Note:

  • public <T extends Bus> void setValue(T val) becomes public void setValue(Bus val).

  • You create generic class, but the type code internally remove those.
  • Examples:
class Bus {} // Assumed
// Original Class with Generic Method (Bounded)
public class Print {
    public <T extends Bus> void setValue(T val) {
        System.out.println("do something");
    }
}
// After Type Erasure (Bytecode equivalent)
/*
public class Print {
    public void setValue(Bus val) {
        System.out.println("do something");
    }
}
*/