Relevant Reading Material
Chapters from How to Design Classes (HtDC)
-
Chapters 30, 31, 32, 33
Generics
List of
Abstract over Types.
Recall our List
implementation.
Our List
consits of Integer
elements. What if I want a list of String
, or Boolean
or some other type that we do not know yet.
List of integers |
List of strings |
List of booleans |
Let’s play "spot the differences"
-
In our interfaces
-
last()
returns a value whose type differs,Integer
,String
,Boolean
-
addToEnd()
takes an argument whose type differs,Integer
,String
,Boolean
-
elementAt()
returns a value whose type differs,Integer
,String
,Boolean
-
add()
-
takes an argument whose type differs,
Integer
,String
,Boolean
-
returns a value whose type differs, the interface is named
List
in all 3 cases but in each caseList
denotes something different.
-
-
-
In
Cons
-
first
stores a value whose type differs,Integer
,String
,Boolean
-
rest
stores a value whose type differs, the interface is namedList
in all 3 cases but in each caseList
denotes something different.
-
A parameter for our Classes and Interfaces
Java allows us to parameterize over our classes and interfaces. Much like we can have parameters for a method, we can also have parameter to a class or interface. Parameters on classes and interfaces are called type parameters.
The parameter however has a different meaning.
-
For a method parameter
-
we provide the type of the parameter
-
we provide a name for the parameter
-
the body of the method can use (manipulate/call methods) on the named parameter
-
we pass a value when calling the method
-
-
For a class or interface type parameter
-
we provide a name only
-
the body of the class or interface can use this name as a type not a value.
-
we pass a type when creating an instance or extending/implementing the class/interface
-
Recall from CS5001
|
|
|
|
In UML our generic list interface
The Java source for our generic list interface.
public interface List<X> {
/**
* Return the number of elements in this list.
*
* @return the number of elements in the list
*/
Integer size();
/**
* Given an element prepending to this list
*
* @param element the element to add
* @return the list with the element prepended
*/
List<X> add(X element);
/**
* Return true if this list is empty,
* and false otherwise.
*
* @return true if this list is empty
* and false otherwise
*/
Boolean isEmpty();
/**
* Given an index return the element
* at that index in the list.
*
* @param index index to use
* @return the element at that index
* @throws GenericListException
*/
X elementAt(Integer index) throws GenericListException;
}
The X
in List<X>
is called a type parameter. We are parameterizing
our class over a type. Like method parameters we can have more than 1,
for example if we are building a Map
ADT we can have one type parameter
for the key and one type parameter for the value of the map, Map<K,V>
.
Type paremeters range over reference types only.
Getting back are type specific lists from our generic interface
Now that we have a generic interface, how can we get back our Integer
, String
and Boolean
list implementations?
We can have the classes that implement List<X>
specify the concrete type that they specialize on.
package genericlist.v1.integerlist;
import genericlist.v1.List;
/**
* Created by therapon on 6/20/16.
*/
public abstract class AIntegerList implements List<Integer> {
@Override
public List<Integer> add(Integer element) {
return new Cons(element, this);
}
}
package genericlist.v1.integerlist;
import genericlist.v1.GenericListException;
/**
* Created by therapon on 6/20/16.
*/
public class Empty extends AIntegerList {
@Override
public Integer size() {
return 0;
}
@Override
public Boolean isEmpty() {
return true;
}
@Override
public Integer elementAt(Integer index) throws GenericListException {
throw new GenericListException("Called elementAt on empty");
}
}
package genericlist.v1.integerlist;
import genericlist.v1.GenericListException;
/**
* Created by therapon on 6/20/16.
*/
public class Cons extends AIntegerList {
private Integer first;
private AIntegerList rest;
public Cons(Integer first, AIntegerList rest) {
this.first = first;
this.rest = rest;
}
@Override
public Integer size() {
return 1 + getRest().size();
}
@Override
public Boolean isEmpty() {
return false;
}
@Override
public Integer elementAt(Integer index) throws GenericListException {
if (index < 0 || index > size() - 1) {
throw new GenericListException("Index out of bounds");
}
if (index == 0) {
return getFirst();
} else {
return getRest().elementAt(index - 1);
}
}
public Integer getFirst() {
return first;
}
public AIntegerList getRest() {
return rest;
}
}
How to re-create and use our integer list now
package genericlist.v1.integerlist;
import genericlist.v1.List;
/**
* Created by therapon on 6/20/16.
*/
public class Main {
public static void main(String[] args) {
List<Integer> empty = new Empty(); // Preferred!
AIntegerList empty2 = new Empty();
List<Integer> oneElement = empty.add(1);
}
}
We are exposing our constructors from our implementation classes. We will come back and update our design so that we can hide our concrete classes and their constructors.
You can easily repeat this pattern to provide implementations for
-
list of strings
-
list of integers
-
list of any reference type that you want.
Can we generalize further?
Observe that our list interface and its implementations for
-
integer list
-
string list
-
boolean list
Do not perform any operation(s) on their elements that is specific to the type of the lists element.
There are no calls to methods specific to
-
Integer
-
String
-
Boolean
In our first attempt to get back our behaviour for integer, string and boolean list we had to write 3 implementations. If we had to do this for more reference types we would have to write a new implementation for each concrete type that we want to create a list.
Can we do better?
Generic List
Our previous implementations took the type parameter X
and forced it to be a specific, known, type, i.e., Integer
, String
and Boolean
.
Since the implementation of our list does not rely on specific methods that are found on concrete types, we can parameterize our implementation further
and get a generic list for any reference type.
|
|
|
|
We can no use the name of the type parameter X
inside List
(as well as in Empty
and Cons
) in locations that expect/require a type, i.e., as a return type to a method signature, as a type for a method
formal parameter or the type of a class' field.
Given this new definition of List<X>
how do we now create lists of integers, strings, booleans etc.
List<Integer> integerList = new Empty<Integer>(); // OR
List<Integer> integerList = new Empty<>(); // if you are in Java 7 or above.
In fact we can create lists of lists as well.
List<List<Integer>> integerListList = new Empty<>();
List<List<List<Integer>>> integerListListList = new Empty<>();
integerListList.add(new Empty<Integer>().add(3)); // OK
integerListList.add(4); // ERROR: compiler rejects this line!
Generic Methods
Notice that our implementation of generic list List<X>
is missing the public static
method that we used to create instances on List
.
Java allows us to have methods (static
or otherwise) that introduce their own type parameters. Here is the factory method but this time with
generics
public static <Y> List<Y> emptyList() {
return new Empty<Y>();
}
The first <Y>
introduces a new type variable; similar to the <X>
on the definition of List<X>
. We need to first introduce the new type parameter and then
we can use it in the signature and body of the method. We can introduce one or more type parameters for methods.
Our emptyList()
method declares a new type parameter Y
and claims that it will return as a result a List<Y>
; that is a list parameterized by that type parameter that we introduced.
Notice that emptyList()
takes no arguments and uses the same type parameter Y
to create an instance of Empty
. The instance of Empty
will be specialized to Y
, that is it is not just
any Empty
it is an Empty
for a specific type that will be inferred at the point in our code that we call emptyList()
List<Integer> intergerList = List.emptyList(); // Y is Integer
List<List<String>> intergerList = List.emptyList(); // Y is List<String>
Bounded Type Parameters
Having a type parameter gives us a lot of flexibility. Sometimes however we would like to control the set of types that we would like to allow for our type parameter. This is useful in situations were
-
We rely on a method to be called on variables typed at our type parameter, e.g., calling
equals()
works an any type, but callingsize()
requires that the type parameter is some subtype ofList
-
Our code only works for certain types and not all types
For example if we would like to have a generic list that only handles Shapes
(from previous lectures) so that we can add methods like totalArea
to our list.
Then we can bound are type parameters to only accept subtypes of Shape
public interface List<X extends Shape> {
public static <Y extends Shape> List<Y> emptyList() {
return new Empty<Y>();
}
// ... same code as before ...
}
We can add bounds to type parameters on classes, interfaces and methods.
Note that the use of the keyword extends
when defining bounds refers to
-
either extends
Shape
, ifShape
is a class -
or implements
Shape
, ifShape
is an interface.
We can also specify multiple bounds by adding &
followed by the bound type’s name
public interface List<X extends Shape & Scaleable & Moveable> {
public static <Y extends Shape> List<Y> emptyList() {
return new Empty<Y>();
}
// ... same code as before ...
}
If we add multiple bounds and one of them is a class, that bound needs to go first. So if Shape is a class and Scaleable and Moveable are interfaces the above code compiles.
public interface List<X extends Scaleable & Shape & Moveable … does not compile.
|
Inheritance and Generics
Looking at our previous List
implementation we know from OO (and this is supported by Java) that we can have the static type of a variable be a superclass of the dynamic type, e.g.,
AList l1 = new Empty();
Assume that
Unfortunately, even though it feels "natural" to say
List<Number> l1 = new Empty<Integer>(); // Number is a super class of Integer
the preceding line signals a compiler error stating
Type mismatch cannot convert from type Empty<Integer> to List<Number>
|
Even if we try
Empty<Number> l1 = new Empty<Integer>(); // Number is a super class of Integer
We get a similar compile time error
Type mismatch cannot convert from type Empty<Integer> to Empty<Number>
|
The reason:
Subtyping does work with generic classes when the type parameter is the same. Notice how Empty<Integer>
is a subtype of AList<Integer>
.
It also works when we have more than one type parameter, as long as the inherited type parameter stays the same, the subtype relationship holds
The Wildcard ?
There are cases where we do not know the type to use for the type parameter or we do not care to provide a name for the type parameter. In these cases we can use the special
character ?
instead
Upper Bound Wildcards
public static Number add(List<? extends Number> input) {
//...
}
The static method add
takes a list of elements that are required to be subtypes of Number
. In the body of the methods we can use elements
of the list input
as type Number
and call any methods available at type Number
.
Unbound Wildcards
We generalize over all types. Compare
public static String asStringListObj(List<Object> list) {
String result = "";
for (Object elem: list) {
result += elem + " ";
}
return result;
}
asStringListObj
works for arguments of type List<Object>
,
List<Integer> li = List.createEmpty();
List<Object> lo = List.createEmpty();
asStringListObj(lo); // works!
asStringListObj(li); // COMPILE TIME ERROR!
If we however use ?
,
public static String asStringList(List<?> list) {
String result = "";
for (Object elem: list) {
result += elem + " ";
}
return result;
}
The above method will work for any parameterized list.
List<Integer> li = List.createEmpty();
List<Object> lo = List.createEmpty();
asStringList(lo); // works!
asStringList(li); // works as well!
List<Object> is not the same as List<?> .
|
Lower bounds
We can also set lower bounds on a type parameter but only when using ?
.
public static void addNumbers(List<? super Integer> list) {
// ...
}
The method addNumbers
is allowed to add any value of type Integer
or its supertypes, e.g., Number
and Object
.
We can use ?
and lower bounds to create relationships between generic types.
Wildcard Guidelines
-
In type parameters refer to type parameters that are used by the code as "inputs"
-
For a method
copy(src,dest)
src
is an in parameter
-
-
Out type parameters refer to type parameters that are used by the code as "outputs"
-
For a method
copy(src,dest)
dest
is an out parameter
-
-
An "in" variable is defined with an upper bound wildcard,
extends
-
An "out" variable is defined with a lower bound wildcard,
super
-
If your code uses methods defined in
Object
on "in" variable, use unbound wildcard, i.e.,?
-
If your code uses a variable as both "in" and "out" do not use a wildcard.
We will see more examples in the upcoming lectures.
Type erasure
Generics do not exist at runtime!
The Java language and JVM implement generics using type erasure. This means that the generics that we write exist statically; they are erased at runtime.
-
Bounds are replaces with their lower or upper bound type
-
Fixed type parameters are replaced with
Object
-
The compiler inserts type casts in order to preserve type safety
-
Generates code to preserve polymorphism in extended generic types
To see examples of some of these re-writes, check the Java Tutorial on Generics.
You have been warned!
Java util package
Let’s have a look at the java.util
package and use some of the commonly used classes.