Generics

Generics has had a major impact on Java, it added new syntactical element and it changed many of the classes and methods to the Java API, the collections class uses generics to implement type safety for example. Generics is heavily used in Java so a good understanding of them is required. I hope to expand this section in the future as I become more experienced with Generics.

So what are Generics, they are parameterized types which allow you to create classes, interfaces and methods in which the type of data upon they operate is specified as a parameter. So putting it in simple terms you can create a class that can accept a number of different data types, before Generics you would use the Object class because everything in Java derives from Object

Lets see a very simple Generics example, in the below example when the class objects are created the T (parameterised type) is replaced with the specific data type you pass in the <>, the compiler does not actually create different versions of the class, instead the compiler removes all generic type information replacing it with the necessary casts, to make your code behave as if a specific version of the class were created. This process is called erasure which I will cover in detail later in this section. You cannot use primitive datatypes you will get a compiler error.

Simple Generics example
public class GenericTest1 {

    public static void main(String[] args) {
        GenericMethodTest<String> gmt1 = new GenericMethodTest<>();         // create class using parameterised <String>
        gmt1.showItem("Hello");

        GenericMethodTest<Integer> gmt2 = new GenericMethodTest<>();        // create class using parameterised <Integer>
        gmt2.showItem(1);

        GenericMethodTest<Float> gmt3 = new GenericMethodTest<>();          // create class using parameterised <Float>
        gmt3.showItem(3.14);
        
        // GenericMethodTest<int> gmt4 = new GenericMethodTest<>();         // you CANNOT use primitive types
        
        // GenericMethodTest<String> gmt5 = new GenericMethodTest<>();      // you cannot assigned different types
        // GenericMethodTest<Float> gmt6 = new GenericMethodTest<>();
        // gmt6 = gmt5;                                                     // compile error
    }
}

// notice the T, which when compiled will be replaced with the passed parameterised type
// so T will be replaced by String, Integer and Float
class GenericMethodTest<T> {

    public <T> T showItem(T t) {                             // the return type can also be generic
        System.out.println("The item is: " + t.toString());
        return t; 
    }
}

You can also pass multiple objects including your own objects

Passing multiple type parameters
public class GenericTest2 {

    public static void main(String[] args) {

        GenericMethodTest2<String, Integer> gmt1 = new GenericMethodTest2<>();         // using multiple parameters
        gmt1.showItem("Hello", 5);

        EmployeeGen paul = new EmployeeGen("Paul");

        GenericMethodTest2<String, EmployeeGen> gmt2 = new GenericMethodTest2<>();         // using you own objects
        gmt1.showItem("Hello", paul);
    }
}

class GenericMethodTest2<T, V> {

    public <T, V> void showItem(T t, V v) {
        System.out.println("Result = T: " + t + "\tV: " + v);
    }
}

class EmployeeGen {
    String name;

    public EmployeeGen(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name;
    }
}

Type Inference

Type Inference is the compiler's ability to look at each method invocation and corresponding declaration to determine the type argument/arguments ( such as T ) that make the invocation applicable, the inference algorithm determines the types of the arguments the type that the result is being assigned or returned if available, the inference algorithm tries to find the most specific type that works with all of the arguments. You can use a type witness which means that the compiler does not have to work out the type inference because you are already specifying one.

Type Inference
package uk.co.datadisk;

import java.util.ArrayList;
import java.util.List;

public class GenericTest7 {

    public static void main(String[] args) {

        List<Bucket<String>> list = new ArrayList<>();

        GenericTest7.addStore("Paul", list);

        // type witness
        GenericTest7.<String>addStore("Will", list);
    }

    public static <T> void addStore(T t, List<Bucket<T>> list) {
        Bucket<T> bucket = new Bucket<>();
        bucket.setItem(t);
        list.add(bucket);
        System.out.println(t.toString() + " has been added to the list...");
    }
}

class Bucket7<T> {

    private T item;

    public T getItem() {
        return this.item;
    }

    public void setItem(T item) {
        this.item = item;
    }
}

Bounded Types

In the last couple of examples you are wondering that we can pass all sorts of data types which could cause issues, for example a method that expects numbers but gets a String instead, so how do we make sure that a specific data types are passed, this is were Bounded Types come in, it allows you to specify the upper bound of the data type you want, so for example you want only numbers then you can use the Number data type, basically when specifying a type parameter you create the upper bound by using the extends keyword that declares the superclass from which all type parameters must be derived, you can only use the class itself or subclasses of that type.

Bounded type example
public class GenericTest3 {

    public static void main(String[] args) {
    
        Integer n = 5;                                                        // you can change this to float, double, any subclass of Number or Number itself 

        GenericMethodTest3<Integer> gmt1 = new GenericMethodTest3<>();
        gmt1.showItem(n);

        // GenericMethodTest3<String> gmt2 = new GenericMethodTest3<>();      // This is will cause a compiler error, cannot use String

    }
}

// Notice the extends keyword specifying the upper bound, which limits you to using numbers only
class GenericMethodTest3<T extends Number> {

    public <T> void showItem(T t) {
        System.out.println("You can only pass numbers: " + t);
    }
}

Using Wildcard Arguments

Now that we understand Generics how do we pass different types of objects to methods, unfortunately we are unable to use the <T> operator as the type cannot be determined, so Java created a wildcard argument <?> that specifies an unknown type, the wildcard simply matches any valid object.

You can use upper or lower bound wildcards

Wildcard type example
public class GenericTest4 {

    public static void main(String[] args) {

        GenericMethodTest4<Integer> gmt1 = new GenericMethodTest4<>(5);
        GenericMethodTest4<Double> gmt2 = new GenericMethodTest4<>(10.345);

        displayValues(gmt1);
        displayValues(gmt2);
    }

    //    static void displayValues(GenericMethodTest4<T> t){           // this won't compile, you cannot use T
    //        System.out.println(t.getI());
    //    }

    // Notice the wildcard ? instead of T, we can now pass gmt1 (Integer) and gmt2 (Double)
    static void displayValues(GenericMethodTest4<?> t){                 // saves you having to create methods for Integer and Double
        System.out.println(t.getI());
    }
}

// Simple Generics class, notice all the T's dotted around
class GenericMethodTest4<T> {

    T i;

    public GenericMethodTest4(T i) {
        this.i = i;
    }

    public T getI() {
        return i;
    }
}

Now we can take this further and use upper bounded wildcard or lower bounded wildcard, although I use built-in Object you can also use your own classes

Upper bounded wildcard example
public class GenericTest4 {

    public static void main(String[] args)
    {

        // Integer list
        List<Integer> list1= Arrays.asList(4,5,6,7);
        System.out.println("Total sum is:"+sum(list1));

        // Double list
        List<Double> list2=Arrays.asList(4.1,5.1,6.1);
        System.out.print("Total sum is:"+sum(list2));

        // String list
        List<String> list3=Arrays.asList("Paul", "Will", "Moore", "Graham");
        // System.out.print("Total sum is:"+sum(list3));                        // String won't compile
    }

    // Number is the upper bound so will accept Integer, Float, Double
    private static double sum(List<? extends Number> list)                      // change the upper bound to restrict different data types
    {
        double sum=0.0;
        for (Number i: list)
        {
            sum+=i.doubleValue();
        }

        return sum;
    }
}
Lower bounded wildcard example
package uk.co.datadisk;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

public class GenericTest8 {

    public static void main(String[] args) {

        List<Serializable> list = new ArrayList<>();
        list.add("Adam");
        list.add("Joe");
        list.add("Joel");
        GenericTest8.show(list);
    }

    // use super for lower bounded wildcard
    public static void show(List<? super Number> list) {  // Object and Serializable only

        list.add(new Float(1));

        for(Object o : list)                // can only use Object or Serializable
            System.out.println(o);

    }
}

Once you start to fully understand you can create complex code

Using Multiple parameters and bounds example
public class GenericTest5 {

    // Determine if an object is in an array.
    static <T extends Comparable<T>, V extends T> boolean isIn(T x, V[] y) {

        for(int i=0; i < y.length; i++)
            if(x.equals(y[i])) return true;

        return false;
    }

    public static void main(String args[]) {

        // Use isIn() on Integers.
        Integer nums[] = { 1, 2, 3, 4, 5 };

        if(isIn(2, nums))
            System.out.println("2 is in nums");

        if(!isIn(7, nums))
            System.out.println("7 is not in nums");

        System.out.println();

        // Use isIn() on Strings.
        String[] strs = { "one", "two", "three",
                "four", "five" };

        if(isIn("two", strs))
            System.out.println("two is in strs");

        if(!isIn("seven", strs))
            System.out.println("seven is not in strs");

        // Opps! Won't compile! Types must be compatible.
        // if(isIn("two", nums))
        //    System.out.println("two is in strs");
    }
}

To summarize upper and lower bounds regarding lists, to read and write to a list don't use wildcards use List<Double>

Producer extends
if we want to read from a list we have to declare it with extends
        
    List<? extends T>      // we can read T values from this list but can not add T values to this list
Consumer super
if we want to add (write) to a list we should use super
        
    List<? super T>        // we can add T values to this list but can not read from this list because we do not know the type

Generics Usage

I am not going to cover every where you can use Generics but provide a list, you can create some very complex code using Generics but as always try to keep things simple so that the next developer who pickups your code can understand it.

Generics Hierarcy and Casting

In this section I want to cover Generic hierarcy and casting, you can use the instanceof operator to check at a generic type can be casted

Using instanceof
public class GenericTest6 {

    public static void main(String[] args) {

        // Create some objects
        Gen iOb = new Gen(88);
        Gen2 iOb2 = new Gen2(99);
        Gen2 strOb2 = new Gen2("Generics Test");

        // See if iOb2 is some form of Gen2.
        if (iOb2 instanceof Gen2)
            System.out.println("iOb2 is instance of Gen2");

        // See if iOb2 is some form of Gen.
        if (iOb2 instanceof Gen)
            System.out.println("iOb2 is instance of Gen");

        System.out.println();

        // See if strOb2 is a Gen2.
        if (strOb2 instanceof Gen2)
            System.out.println("strOb is instance of Gen2");

        // See if strOb2 is a Gen.
        if (strOb2 instanceof Gen)
            System.out.println("strOb is instance of Gen");

        System.out.println();

        // See if iOb is an instance of Gen2, which it is not.
        if (iOb instanceof Gen2)
            System.out.println("iOb is instance of Gen2");

        // See if iOb is an instance of Gen, which it is.
        if (iOb instanceof Gen)
            System.out.println("iOb is instance of Gen");

        // The following can't be compiled because
        // generic type info does not exist at run-time.
        // if(iOb2 instanceof Gen2)
        //    System.out.println("iOb2 is instance of Gen2");
    }
}

// Use the instanceof operator with a generic class hierarchy.
class Gen {
    T ob;

    Gen(T o) {
        ob = o;
    }
}

// A subclass of Gen.
class Gen2 extends Gen {
    Gen2(T o) {
        super(o);
    }
}

Erasure

When Generics were added it had to be compatible with previous versions of Java, it had to work with non-generic code. The way Java implements generics is through erasure, when the code is compiled, all type information is removed (erased). This means replacing the typed parameters with their bound type which would be Object if no explicit bound is specified, otherwise the bounded type is used, and then applying the appropiate casts to maintain compatibility with the types specified by the type arguments. This approach means that no type parameter exists at runtime, they are simply a source-code mechanism. The compiler uses bridge methods to handle situations in which the type erasure of an overriding method in a subclass does not produce the same erasure as the method in the superclass, this type of code is only created by the compiler and not very used outside by developers.

Ambiguity Error

Ambiguity errors occurs when erasure causes two seeming distinct generic declarations to resolve to the same erased type, causing a conflict

Ambiguity error
class MyGenClass {  
  T ob1;  
  V ob2;  
 
  // ... 
 
  // These two overloaded methods are ambiguous 
  // and will not compile. 
  void set(T o) { 
    ob1 = o; 
  } 
 
  void set(V o) { 
    ob2 = o; 
  } 
}

Generic Restrictions

I am just going to bullit-point some of the Generic restrictions you should know about