Week 5 Java Generics
Week 5 Java Generics
What is generics?
Basically, generics is the use of type parameterisation, which is to allow types to be parameters to not only methods, but classes and interfaces (classes and interfaces can
have parameters too!)
The types in this sense ONLY applies to reference types but not primitive types.
Normally, polymorphism is displayed when an object from a parent class, without knowing which classes it would reference, is able to invoke method behaviours
according to whichever class (subclass) that it would eventually be referencing.
In generics, polymorphism is displayed when a generic class like List<E> can readily bind to any reference types like Integer , Double that is specified upon instantiation
through type parameterisation. (Parametric polymorphism)
Auto-boxing / Unboxing
Wrapper types (eg. Integer , Double ) wraps around its corresponding primitive types (eg. int , double ) to make it a reference type.
In java, we can do this operation: Integer x = 1. Even though x is a reference type and 1 is a primitive type, java has an auto-boxing feature which boxes the primitive 1 of
type int into an object of its wrapper type, Integer . Thus the assignment is possible.
Operations like list.add(1) also has auto-boxing features that will box the primitive 1 into an Integer object with a value of 1.
Auto-unboxing also happens in operations like int x = list.get(0) , when the Integer object returned from list.get(0) is unboxed to the primitive type int and assigned to
x.
Mutable ArrayList<E>
The symbol E is a generic type. Thus, ArrayList<E> refers to an ArrayList that contains elements of a particular type E .
Whenever we create a new instance of an ArrayList , we would have to specify this generic type. Thus, it can be ArrayList<Integer> , ArrayList<String> etc. This action of
specifying the type is called type parameterisation, which refers to the binding of the generic type ( E ) to the specified type (Eg. Integer ) that happens during compilation
The mutable ArrayList class is a property that is encapsulated inside the immutable ImList class, in other words, the client will not know the mutable implementation of the
ImList due to the private keyword.
All the method implementations of ImList will be delegated to the ArrayList encapsulated within ImList , EXCEPT for the method that triggers state change, which is
the list.add() method. Thus, for all other methods, ImList will call ArrayList 's method implementation.
For the list.add() method, we will need to create a new ImList object that has the exact same elements in order to maintain immutability. Then, we will need to access
the ArrayList property of the new ImList and call ArrayList 's list.add() method on the ArrayList within the new ImList . Then, we will return the new ImList with the
added element, and the original ImList will still remain the unchanged.
We cannot do the following operation List list = new ImList() , this is because ImList is NOT a subclass of List . This is due to the fact that the list.add() exists in
BOTH List and ImList , but the implementation in ImList CANNOT override the implementation in List due to a different return type (method signature) of the
list.add() method.
Generic classes
The new generic implementation ImList<E> is shown below (substitute all “Integer” with “<E>”):
All the instances of <E> within the code of the generic class definition ImList<E> will be “bound” to the generic type <E> in the generic class definition, this generic type
will then be “bound” (type parameterised) to the specific reference type that the client actually specifies upon usage.
Note that all the ImList appearances in the code should be changed to ImList<E> instead.
If there is a compilation error when compiling a generic class, we will add the command -xlint:unchecked behind to see the what are the compilation errors:
An important thing to note is that during the class definition of ImList<E> , writing something like this:
class ImList<String> {
...
}
This will STILL enable the code to compile but it is very confusing as the implementer appears to be type parameterising even before the client attempts to type
parameterise. Thus, NEVER do class ImList<Integer> or class ImList<Double> etc, ALWAYS keep it as class ImList<E> .
Generic methods
Generic methods contains all the common elements of a normal method (method name, arguments, body and return type), but it has an additional component which is the
declaration of the type parameter.
This type declaration is necessary as the generic method will return an object of a generic class. This object will need a reference type, specified by the user, to bind to. The
declaration will be done at the beginning of the method. One example is the code below:
The above code can only create empty immutable lists. To specify elements to be added into the immutable list, we would have to make some changes to the method like
below:
jshell> of(1)
==> [1]
jshell> of('one')
==> [one]
Thus, this will allow type parameterisation to occur when a user specifies the reference type of the elements to be added into the immutable list upon calling the method.
(The type of the immutable list will depend on the reference type of the argument supplied to the method).
Static factory methods are alternatives to creating an object without the use of the new keyword and the constructor. The keyword static means that the method is not
called with respect to any object, but is called with respect to the class itself. This is how we can implement the of() method under the ImList<E> class:
class ImList<E> {
...
static <E> ImList<E> of() { // Called by ImList.<Reference_type>of()
return new ImList<E>();
}
Take note that the <E> in the of() method is not bound to the the generic type <E> in the generic class definition. Instead, the <E> in ImList<E> in the of() method is
bound to the type declaration <E> at the beginning of the method. This type declaration <E> at the beginning of the method will then be bound to the reference type
specified during the method call itself via type witnessing.
Usually, for a generic class, we will make all the constructor methods private as we want to hide the implementation of the constructor away from the client. Thus, clients
will not be able to use new to instantiate an object, they will use the static factory method instead.
// Type witnessing
jshell> ImList.<Integer>of(List.of(1, 2, 3))
==> [1, 2, 3]
jshell> ImList.of(List<Integer>of(1, 2, 3))
==> [1, 2, 3]
// Type inferencing
jshell> ImList.of(List.of(1, 2, 3))
==> [1, 2, 3] // ImList<Integer>
jshell> ImList.of(List.of(1, 2.0, 3))
==> [1, 2.0, 3] // ImList<Number>
Thus, as opposed to substitutability of normal parent and child classes, doing the statement below with generic classes will not make the code compile
The reason why generic classes are invariant is because of something called heap pollution. Heap pollution occurs in mutable reference types like Array . Here is an
example showing how heap pollution occurs with the mutable Array type:
Using the Array type, we can let an array containing elements of type Shape reference an array containing elements of type Circle .
However, since both Circle[] circles and Shape[] shapes are referencing the same Circle[] array object, when we try to set an element in the array to an object of type
Rectangle , we will notice that the heap space referenced by Circle[] circles , used to store the Circle[] array object, will contain an element of Rectangle class. Thus
this results in heap pollution.
Since there is also an addAll() method defined under the ArrayList class, we can call ArrayList 's addAll() method within the ArrayList property of the other ImList<E>
object. However, this code does not allow for an immutable list containing elements of a superclass to call addAll() on another immutable list containing elements of a
subclass. For example:
To enable a immutable list containing elements of a superclass to add another immutable list containing elements of a subclass, we will have to modify the addAll() method.
Currently, then <E> in ImList<E> others will be bound to the type Shape , thus we will have to change it to the following:
Thus the extends E refers to an upper bounded wildcard, which in this case is upper bounded by the Shape abstract class. This means that the method addAll() can be
called to any immutable list containing elements of the subclass and not of a more generic class ( ImList<Object> cannot be passed into addAll() when ImList<Shape> is