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 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; } } |
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); } } |
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 |
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.
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 |
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 errors occurs when erasure causes two seeming distinct generic declarations to resolve to the same erased type, causing a conflict
Ambiguity error | class MyGenClass |
I am just going to bullit-point some of the Generic restrictions you should know about