Relevant Reading Material
Generics
Generic Types
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.
and here is the code for each class/interface
|
|
|
|
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>
.
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 lecture 2) 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();
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
public static String asStringList(List<?> list) {
String result = "";
for (Object elem: list) {
result.append(elem + " ");
}
return result;
}
The above method will work for any parameterized list.
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.
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 replaces 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!
Mutation
The ability to alter, in-place, the state of an object.
Setters
In the same way that we have getter methods that return the value of an objects field, we also have setter methods that allow us to update, in-place, the value of an objects field.
public class Name {
private String first;
private String last;
public Name(String first, String last){
this.first = first;
this.last = last;
}
public String getFrist() {
return this.first;
}
public String getLast() {
return last;
}
public void setFirst(String first) {
this.first = first;
}
public void setLast(String last) {
this.last = last;
}
}
The first thing to notice here is the keyword void
. void
is used as a return value to a method to indicate
that this method does not return any value, it updates the object on which this method is called. So the caller
of a setter method should not expect a new value back. The caller of a setter methods should expect the internal
state of the object to change based on what the setter method documentation.
Walk though an example and its evaluation to show the effect of a setter. |
Circular data and the need for null
We would like to implement a circular, singly linked list.
Here is our first attempt
Lets try and write examples. Can we create an example for a circular list with 1 element, e.g.,
What is the second argument to the constructor?
Cell singleElementList = new Cell("A", ???);
What to we write in the place of ???
? We would like to put singleElementList
but we cannot since we are in the process of creating it and we are not done creating singleElementList
.
We need to somehow
-
partly create
singleElementList
-
set the
next
field of our partly created object to itself
Itemization!
We need an itemization such that we capture the empty case and the non-empty case.
Then we can
Cell singleElementList = new NMT("A", new MT());
singleElementList.setNext(singleElementList);
This means we have to create a corresponding MT
class with all the methods of the super class (Cell
) implemented (some will throw exceptions, i.e., getNext()
) for all such situations.
Java provides a special value called null
that can be used to denote such empty (uninitialized or absent-value) cases.
What do you think is the type of null
?
Using null
We can go back to our original design and use null
in the location of ???
Cell singleElementList = new Cell("A", null);
singleElementList.setNext(singleElementList);
What happens if we run the following code?
Cell singleElementList = new Cell("A", null);
singleElementList.setNext(singleElementList);
singleElementList.toString(); // ?
Walk through the execution of toString()
|
What happens when we run this code
Cell singleElementList = new Cell("A", null);
singleElementList.setNext(singleElementList);
singleElementList.toString(); // ?