Relevant Reading Material
Chapters from How to Design Classes (HtDC)
-
Chapters 30, 31, 32, 33
Parametric polymoprhism and Java Generics
Parametric polymorphism is the ability to write data or methods/functions generically such that we can handle values in exactly the same way without relying on their type(s).
Another way to think about parametric polymorphism is the ability to abstract over types instead of values. For example, when we design a method the body of the method is parameterized over the inputs (parameters).
public Integer myAdd(Integer a, Integer b) {
return a + b;
}
The myAdd
method can add any two Integer
s.
public boolean equals(Object other) { ... }
The equals
method can check for equality between the currently executing object this
and some other Object called other
.
We would also like to be able to parameterize over our types as well. We have seen something similar in CS5001 with abstract functions
and abstract data definitions, recall LoF<X>
and map
which had the signature map: [X → Y] Lof<X> → LoF<Y>
.
The Java language feature that allows us to abstract over reference types is called generics.
Pair
Consider our Posn
class. The Posn
class is essentially a pair—a data definition that holds two values—that we use to capture
cartesian coordinates.
It is typically, and in fact sometimes very usefull, to be able to capture a pair in our code and specialize that to our specific case. For example
-
use our pair as a cartesian point
-
use our pair as our internal pair that maps a key to a value in our priority queue implementation
So we would like to create a more general class that allows us to represent a pair of values that we will call first
and second
.
The generality that we want to achieve here is that we can store any value for first
and second
and we want to keep our nice
type properties that Java provides.
Let’s start with an example that although allows us to store any value in our pair, it does not maintain the type properties for our values.
/**
* Represents a pair of values.
*/
public class Pair {
private Object first;
private Object second;
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public void setFirst(Object first) {
this.first = first;
}
public Object getSecond() {
return second;
}
public void setSecond(Object second) {
this.second = second;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Pair pair = (Pair) o;
if (getFirst() != null ? !getFirst().equals(pair.getFirst()) : pair.getFirst() != null)
return false;
return getSecond() != null ? getSecond().equals(pair.getSecond()) : pair.getSecond() == null;
}
@Override
public int hashCode() {
int result = getFirst() != null ? getFirst().hashCode() : 0;
result = 31 * result + (getSecond() != null ? getSecond().hashCode() : 0);
return result;
}
@Override
public String toString() {
return "Pair{"
+ "first=" + first
+ ", second=" + second
+ '}';
}
public static void main(String[] args) {
Pair p1 = new Pair(1, "a"); (1)
Pair p2 = new Pair(new Posn(1,1), true);
Object w = p1.getFirst(); (2)
Object x = p1.getSecond();
Object y = p2.getFirst();
Object z = p2.getSecond();
Integer i = (Integer) w; (3)
String a = (String) x;
Posn p = (Posn) y;
Boolean b = (Boolean) z;
}
}
1 | We create instances of Pair and provide values that have specific types. |
2 | Getting any value out of a Pair we are now stack with a compile-time type of Object |
3 | To get back to the more specific compile-time type we need top cast. YUCK! This also means we need to
remember what type of a value did we add to which instance of Pair or add extra code to check the runtime-type
before we cast and then cast. |
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.
Method formal parameters | Class/Interface type parameters |
---|---|
|
|
/**
* Represents a pair that holds two values.
*
* @param <X> type for the first value.
* @param <Y> type for the second value.
*/
public class Pair<X,Y> { (1)
private X first; (2)
private Y second;
/**
* Creates a pair from the two given values.
*
* @param first value to be stored as the first element of the pair.
* @param second value to be stored as the second element of the pair.
*/
public Pair(X first, Y second) { (3)
this.first = first;
this.second = second;
}
/**
* Get the first element of the pair.
* @return the first element
*/
public X getFirst() { (4)
return first;
}
/**
* Set the first element of the pair.
*
* @param first the new value for the first element.
*/
public void setFirst(X first) {
this.first = first;
}
/**
* Get the second element of the pair.
*
* @return the second element
*/
public Y getSecond() {
return second;
}
/**
* Set the second element of the pair.
*
* @param second the new value for the second element
*/
public void setSecond(Y second) {
this.second = second;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Pair<?, ?> pair = (Pair<?, ?>) o;
if (getFirst() != null ? !getFirst().equals(pair.getFirst()) : pair.getFirst() != null)
return false;
return getSecond() != null ? getSecond().equals(pair.getSecond()) : pair.getSecond() == null;
}
@Override
public int hashCode() {
int result = getFirst() != null ? getFirst().hashCode() : 0;
result = 31 * result + (getSecond() != null ? getSecond().hashCode() : 0);
return result;
}
@Override
public String toString() {
return "Pair{"
+ "first=" + first
+ ", second=" + second
+ '}';
}
}
1 | X and Y are type parameters. Type parameters are enclosed in <> and come immediately after the class/interface. The scope of X and Y is the whole class. They are similar to normal parameters in that we are going to have to provide a type for X and Y . Read <> as of so Pair<X,Y> can be read as Pair of X and Y. |
2 | We can use a type parameter in the same way that we would use a reference type, as the type of a class field. |
3 | We can use a type parameter in the same way that we would use a reference type, as the type of an argument. |
4 | We can use a type parameter in the same way that we would use a reference type, as the return type of a method. |
Type parameters range over reference types only.
Parameter type names are typically 1 character, and it is a capital character.
Using our Generic Pair class
Using (create instances and calling methods on these instances) a generic class is similar to a non-generic class with some differences.
Creating an instance of a generic class requires us to provide specific types for each of the type parameters.
Pair<Integer,Integer> coord1 = new Pair<Integer, Integer>(1,2); (1)
Pair<Integer,Integer> coord2 = new Pair<>(1,2); // PREFERED (2)
1 | The compile time time for coord1 needs to provide actual reference types for each type parameter. The call to Pair 's constructor
also needs to provide the reference types for each type parameter. |
2 | Since Java 7, we can now use the diamond <> on the right hand side of an assignment and the Java compiler will auto populate the
reference type for each type parameter from the compile-time type on the left hand side of the assignment. |
With Generics the Java compiler now remembers what type(s) of value(s) we have set at the time of object creation and will then return
the specific type for each value inside of a Pair
public static void main(String[] args) {
Pair<Integer, String> p1 = new Pair<>(1, "a");
Pair<Posn,Boolean> p2 = new Pair<>(new Posn(1,1), true);
Integer w = p1.getFirst();
String x = p1.getSecond();
Posn y = p2.getFirst();
Boolean z = p2.getSecond();
}
No casts!
Once we have created an instance of Pair and provided reference types for each type parameter, the Java compiler will check our usage of our instance to ensure that our code does not violate any of the restrictions imposed by our types.
Integer xCoord = coord1.getFirst();
coord1.setFirts(10);
coord1.setFirst("A"); // COMPILE TIME ERROR
The last line in our preciding code snippet will signal a compile-time error. The compile-time error indicates that we cannot
use a String
value in the place of an Integer
value.
The flexibility of Java generics allows us to now create Pair
s that can contain any reference type values for
first
and second
Pair<Integer,String> roman1 = new Pair<>(1, "I");
Pair<Integer,String> roman2 = new Pair<>(2, "II");
Pair<Integer,String> roman3 = new Pair<>(3, "III");
Pair<Integer, List> keyList =
new Pair<>(42, List.createEmpty("A").add("Bar").add("Beer"));
And something even more interesting is that our Pair
can have Pair
s as its first
and second
as well.
Pair<Integer, Pair<Integer, Integer>> node =
new Pair<>(10, new Pair<>(5, 15));
Designing with Generic Classes
Generic classes can be very useful. Sometimes however we would like to use a generic class to capture information from our problem domain. The information we would like to capture, can be modelled with an existing generic class but we would like to further add specific behaviour that is not generic in nature.
Consider our Pair
example. It is pretty easy to see that we can replace our Posn
class with uses of our Pair
class
where the type parameters are both Integer
or Double
. But if we would also like to have specific behaviour that relates to
Posn
, i.e., distance between Posn
s, we do not want to add that behaviour to Pair
. That will take away from our nice generic Pair
class. Instead we would like to wrap or extend our Pair
class, and specialize it for Posn
.
For this example we will use Integer
for the values of our Posn
class.
public class Posn extends Pair<Integer, Integer> { (1)
public Posn(Integer x, Integer y) {
super(x, y);
}
/**
* Getter for property 'x'.
*
* @return Value for property 'x'.
*/
public Integer getX() { (2)
return getFirst();
}
/**
* Setter for property 'x'.
*
* @param x Value to set for property 'x'.
*/
public void setX(Integer x) {
setFirst(x);
}
/**
* Getter for property 'y'.
*
* @return Value for property 'y'.
*/
public Integer getY() {
return getSecond();
}
/**
* Setter for property 'y'.
*
* @param y Value to set for property 'y'.
*/
public void setY(Integer y) {
setSecond(y);
}
public Double distanceTo(Posn other) { (3)
return Math.sqrt(Math.pow(this.getX() - other.getX(), 2) +
Math.pow(this.getY() - other.getY(), 2));
}
@Override
public String toString() {
return "Posn{" + this.getX() + ", " + this.getY() + "}" ;
}
}
1 | Our Posn class extends Pair<Integer, Integer> because we want to always have Integer values for x and y |
2 | Wrapper methods like these will allow us to reuse our Posn<Integer,Integer> with a small code change with existing clients.
It is also typically considered good style to provide methods names that match the problem domain like getX and getY instead of
getFirst and getSecond |
3 | Now our Posn class has a distance method that takes another Posn . The method is specific to Posn s and takes
as an argument a specific value, another Posn . distance has not connection or relation to Pair<> . |
List of
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
Recall from CS5001
|
|
|
|
In UML our generic list interface
The Java source for our generic list interface.
public interface List<X> { (1)
/**
* 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); (2)
/**
* 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;
}
1 | X is a type parameter. |
2 | We are using X just like any other type name, as the type of the argument element and as part of
the return type List<X> . These uses of X refer to the same X introduced on the first line, public interface List<X> { … } |
Our new generic list is now parameterized over any reference type, much like our List<X>
from Racket. We can not create
any list that contains values of a specific reference type from one interface.
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
A generic method (static or non-static), like a generic class or interface, allows a method to abstract over types.
Consider our Pair<X,Y>
class and for simplicity here, lets try and write a PairUtil
class that will contain utility
methods for Pair<X,Y>
. For instance a method that takes 2 generic pairs and tells us if they are the same.
public class PairUtil {
public static <X,Y> boolean pairEquals(Pair<X,Y> p1, Pair<X,Y> p2) {
return p1.getFirst().equals(p2.getFirst())
&& p1.getSecond().equals(p2.getSecond());
}
}
In the case of generic methods, the new type parameters are defined before the return type, in our case right before boolean
.
These type parameters are available
-
in the method’s signature
-
in the method’s body
Let’s use our pairEquals
method.
Pair<String, Integer> p1 = new Pair<>("I", 1);
Pair<String, Integer> p2 = new Pair<>("II", 2);
PairUtil.<String, Integer> pairEquals(p1, p2);
Observer that when we call our pairEquals
method immediately before the method name we provide the concrete types for the type arguments
<X,Y>
.
The Java compiler tries to help out (like int he case of Pair<>
) and will allow us to omit <String, Integer>
in PairUtil.<String, Integer> pairEquals(p1,p2)
, e.g.,
Pair<String, Integer> p1 = new Pair<>("I", 1); // Java infers the type for us
Pair<String, Integer> p2 = new Pair<>("II", 2);
PairUtil.pairEquals(p1, p2); // Java infers the types for us.
Adding back our static factory method
Notice that our implementation of generic list List<X>
is missing the public static
method that we used to create instances on List
.
Let’s consider our static factory methods for List
as we had it before we added generics
public interface IntegerList {
static IntegerList createEmpty() {
return new Empty();
}
// elided code
}
Our static factory method was responsible for creating an empty IntegerList
, that is an empty list of a specific type.
Now that we added generics, we would like to have a similar method that will allow us to create a list of any type, Integer
, String
, Posn
, etc.
Similar to how we abstracted over types for List
we can abstract over types for a method. This applies to both static and non-static
methods.
In order to abstract over a method and allow the methods to apply to any type, we need to introduce a new type parameter much like
we did for List
. The Java language allows us to introduce new type parameters for a method as part of the method’s signature,
immediately before the method’s return type.
For example, here is the generic factory method
public static <Y> List<Y> emptyList() {
return new Empty<Y>();
}
The first <Y>
introduces a new type parameter; similar to the <X>
on the definition of List<X>
. This type parameter <Y>
is available for emptyList
method only.
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 and their scope is limited to
-
the method’s signature
-
the method’s body
Why do I need <Y>
why can’t I use <X>
from List<X>
You noticed that for our static
method we introduced a new type parameter <Y>
and we named it Y
to
show that it is different that the existing type parameter <X>
introduced by our interface List<X>
.
The reason for having a new type parameter <Y>
is because we want our method to be generic. We want our
code to be able to call this method and provide whatever type is appropriate. Further more, we are calling
emptyList
before we even have an instance of List<X>
; that is the point of emptyList
to create
an empty list for whatever type we want.
This is similar to our pairEquals
method from earlier.
Recall
List<Integer> lint = new Empty<Integer>();
In this example, lint
uses Integer
for X
. We typically say
-
type parameter when we talk about
X
inList<X>
-
type argument when we talk about
Integer
inList<Integer> lint = new Empty<Integer>();
In the case of our static factory method, our intention is to use this method to create new lists of any type.
Our static method is generic, it abstracts over types of lists that we can create.
Our static factory method is called before a concrete constructor for a List
is called, e.g., Empty
's
or Cons
's constructors.
We therefore need our method to be able to accept any type argument. This type argument will be passed on
to our call of the appropriate constructor, in this case Empty
. Therefore our method needs a type parameter to capture
the appropriate type argument and create the specific list requested by the client code.
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
,
it is not just any Empty
it is an Empty
for a specific type
that will be provide (or inferred) at the point in our code that we call emptyList()
.
The specific reference type that will replace Y
will be given at the time
that we call createEmpty
, e.g.,
List<Integer> intergerList = List.<Integer>emptyList(); // Y is Integer
List<Integer> intergerList = List.emptyList(); // equivalent if Java can infer the type
List<List<String>> intergerList = List.<List<String>> emptyList(); // Y is List<String>
List<List<String>> intergerList = List.emptyList(); // equivalent if Java can infer the type
Java’s type inferencer for type parameters
Java has a type inferencer. The type inferencer tries to detect and infer the appropriate type arguments for type parameters.
Recall
List<Integer> lint = new Empty<>(); // infers Integer for Empty<Integer>
List<String> lstr = List.createEmpty(); // infers <String> for List.<String> createEmpty();
Sometimes, however, Java’s type inferencer cannot infer the most appropriate type argument and it will cause errors.
List<String> lstr = List.createEmpty().add("a"); // Compile time error!
The preceding line has a compile-time error that states:
Incpomaptible types: Required: java.lang.String Found: java.lang.Object
The details for why Java’s inference cannot infer the appropariate types has to do with how generics are implemented in Java which is not the main focus of the course. We will mention some information on the implementation of Generics in Java later on.
The work around for these situtaions are:
-
Provide the type arguments in your code
-
Break you statement into multiple statements to help the inferencer
For example, we can provide the type parameter that we want
List<String> lstr = List.createEmpty().add("a"); // Compile time error!
List<String> lstr = List.<String> createEmpty().add("a"); // Fixed!
Or break our statement into multiple statements making it easier for Java’s type inferencer to correctly detect the appropriate type arguments
List<String> lstr = List.createEmpty().add("a"); // Compile time error!
List<String> lstr = List.<String> createEmpty();
lstr.add("a"); // Fixed!
You typically want to follow the first pattern, it is clear what you want the type arguments to be despite the fact that the syntax might look a little odd.
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();
For example Java has the following hierarchy
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
Bounded Type Parameters
The notion of subtyping in the presence of generics affects our design and our use of polymorphism.
Lets consider the following situation that uses Shape
s and a List<Shape>
.
We would like to write a utility class that given a list of any shapes, returns the total area occupied by all the shapes in the list.
Our first attempt to writing this method
public interface ShapeListUtil {
static Double totalArea(List<Shape> shapes) {
Double acc = new Double(0);
for (int i = 1; i <= shapes.size(); i++) {
acc = acc + shapes.elementAt(i).area();
}
return acc;
}
}
Let’s write some tests
public void totalArea() throws Exception {
List<Shape> slist = List.EmptyList();
Posn p = new Posn(1,1);
slist =
slist
.add(new Rectangle(p, 10, 10))
.add(new Rectangle(p, 5,5));
System.out.println(ShapeList1.totalArea(slist));
}
Well this one works!
How about this test
public void totalArea() throws Exception {
Posn p = new Posn(1,1);
List<Rectangle> rlist = List.EmptyList();
rlist =
rlist
.add(new Rectangle(p, 10, 10))
.add(new Rectangle(p, 5,5));
System.out.println(ShapeList1.totalArea(rlist)); // COMPILE TIME ERROR!
}
Our second test does not even compile. We get the compile-time error message:
totalArea (genericlist.notes.List<genericlist.shapes.Shape>) cannot be applied to (genericlist.notes.List<genericlist.shapes.Rectangle>)
The subtype relationship between generic classes does not define List<Rectagle>
as a subtype of List<Shape>
.
This is odd since we actually have a test, our first test, that is in fact a list of Rectangle
s and it works! How can we
write out totalArea
method such that it works for all List
s that contain Shape
types?
The answer is to use bounded type parameters. A bounded type parameter in Java allows us to specify an upper bound—a type that we can safely use regardless of the actual parameter type.
An upper bound instructs the Java language that we will accept any type in our type hierarchy that is our upper bound or any of its subtypes.
Lets re-write our example using an upper bound. The syntax for an upper bound is
-
<X extends Shape>
This means that we want to generalize our type parameter X
such that X
must be a subtype of Shape
.
If we try and use a type parameter that is not a subtype of Shape
the compiler will issue a compile-time error.
An upper bound on our type parameter allows us to rely on any operation defined on the upper bound type, in this case Shape
,
within our generic class/method body.
static <X extends Shape> Double totalArea(List<X> shapes) {
Double acc = new Double(0);
for (int i = 1; i <= shapes.size(); i++) {
acc = acc + shapes.elementAt(i).area(); // We can rely on area() since our upper bound is Shape
}
return acc;
}
Our generic method totalArea
is now abstracted over all List<X>
where X
can range over all subtypes of Shape
, so what we have is a method that accepts any of the following types
-
List<Shape>
-
List<Rectagle>
-
List<Circle>
-
and any other subtype of
Shape
that we might add later on
Think of the upper bound as way to restrict the types that your code will accept for X
.
Note that the use of the keyword extends
when defining bounds works for both
-
extends
Shape
, ifShape
is a class -
implements
Shape
, ifShape
is an interface.
Read it as suptype even though we type the word extends
.
We can also specify multiple bounds by adding &
followed by the bound type’s name if our code is going to depend
on behaviour defined in any of these upper bounds.
public interface List<X extends Shape & Scaleable & Moveable> {
public static <Y extends Shape & Scaleable & Moveable> List<Y> emptyList() {
return new Empty<Y>();
}
public Double totalArea();
}
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.
|
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
Observe that in our last version of totalArea
even though we have a type parameter X
the code does not
use X
at all. We could use a wildcard instead, e.g.,
static Double totalArea(List<? extends Shape> shapes) {
Double acc = new Double(0);
for (int i = 1; i <= shapes.size(); i++) {
acc = acc + shapes.elementAt(i).area(); // We can rely on area() since our upper bound is Shape
}
return acc;
}
and the implementations are equivalent.
Unbound Wildcards
There is a much stronger argument for the existence of ?
.
Consider
// print each list element separated by a space
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<?> . List<?> is the mother of generic list types. Like Object is to non-generic
reference types.
|
Let’s take a step back and recap
Before Generics subtyping in Java allowed us to use a instances of a subtype in the place of a super type, e.g.,
We could then write code that would use instances of B
as A
A a = new B();
B b = new B();
A a2 = b;
We relied on this behaviour extensively!
Now with generics however
List<A> la = List.<B> createEmpty(); // COMPILE TIME ERROR
List<B> lb = List.<B> createEmpty();
List<A> la = lb; // COMPILE TIME ERROR
The notion of subtypes in the presence of generics follows different rules. We know that
for any generic class, e.g., List<X>
the most common supertype is List<?>
.
We also know that subtyping for generic types requires
-
the type parameter to be the same
-
the parameterized class, e.g.
List
,Empty
andCons
, to be in a subtype relationship
Recall
Also recall
Lower bounds
In the same manner that we can use our upper bounds on ?
to restrict the unknown type to be a specific type or a subtype of that type
we can also use lower bounds to restrict the unknown type to be a specific type or a super type of that type.
Think for example of a method that you would like to add to a Rectangle
to a generic list, but you want this method to work for
List<Rectangle>
, List<Shape>
and List<Object>
. Another way of putting it is you would like this method to work on any list that can hold Rectangles
.
The syntax for a lower bound is
-
<? super Rectangle>
public static void addRectangles(java.util.List<? super Rectangle> recs, Integer maxWidth, Integer maxHeight){
Posn p = new Posn(0,0);
for (int i = 1; i < maxWidth; i++) {
for (int j = 1; j < maxHeight; j++){
recs.add(new Rectangle(p, i, j));
}
}
}
Here are some uses of our method
List<Object> lobj = new ArrayList<Object>();
ShapeList1.addRectangles(lobj, 3, 3);
List<Shape> lshape2 = new ArrayList<Shape>();
ShapeList1.addRectangles(lshape2, 3, 3);
List<Rectangle> lrec = new ArrayList<Rectangle>();
ShapeList1.addRectangles(lrec, 3, 3);
Wildcard Guidelines
See Collections.copy
-
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 and Collection
The java.util
package contains utility classes and interfaces. The package has been evolving with each Java version to add commonly used
classes/interfaces. One of the most commonly used framework (collection of types) is the Collection
framework.
Collections Framework
A collection here refers to a group of objects. Objects that are part of a collection are referred to as elements. Under the collections framework we have
-
Interfaces that define the behaviour of collections, e.g.,
Set<E>
,List<E>
,Queue<E>
etc. -
Concrete classes that provide a general-purpose implementation of the interface(s) that you can use directly, e.g.,
ArrayList
,LinkedList
,HashSet
-
Abstract classes that implement the interface(s) of the collection framework that you can extend in order to create specialized data structures applicable for your problem.
The goals of the Collections Framework is to
-
Reduce programmer effort by providing the most common data structures
-
Provide a set of types that are easy to use and extend
-
Provide flexibility through defining a standard set of interfaces for collections to implement
-
Improve program quality through the use and reuse of tested implementations of common data structures
Overview of Collections Framework Hierarchy
UML class diagram is intentionally incomplete. The class diagram below contains some of the Collections Framework interfaces and classes
and for each type only some of its signatures. For a complete list of types and methods see the javadoc for java.util
.
public static void main(String[] args) {
System.out.println("**** List Examples ****");
List<Integer> lint1 = new ArrayList<>();
List<Integer> lint2 = new LinkedList<>();
for (int i = 0; i < 10 ; i++) {
lint1.add(i);
lint2.add(i);
}
System.out.println(lint1);
System.out.println(lint2);
System.out.println(lint1.isEmpty());
System.out.println(lint2.isEmpty());
System.out.println(lint1.size());
System.out.println(lint2.size());
System.out.println(lint1.contains(0));
System.out.println(lint2.contains(1));
System.out.println("**** Set Examples ****");
Set<String> sset = new HashSet<>();
sset.add("a");
sset.add("b");
System.out.println(sset);
sset.add("a");
System.out.println(sset);
System.out.println(sset.contains("a"));
System.out.println(sset.contains("x"));
Set<String> sset2 = new HashSet<>();
sset2.add("b");
sset2.add("a");
System.out.println(sset2.equals(sset));
sset.add("x");
sset.add("y");
sset2.add("y");
sset2.add("x");
System.out.println(sset.equals(sset2));
System.out.println("**** Queue Examples ****");
Queue<String> q1 = new ArrayDeque<>();
System.out.println(q1.size());
System.out.println(q1);
q1.add("a");
q1.add("b");
q1.add("c");
System.out.println(q1.size());
System.out.println(q1);
System.out.println(q1.peek()); // same as calling element()
System.out.println(q1);
q1.poll(); // same as calling remove()
System.out.println(q1);
Deque<String> dq1 = new ArrayDeque<>();
dq1.addFirst("x");
dq1.addFirst("y");
dq1.addFirst("z");
dq1.addLast("a");
dq1.addLast("b");
dq1.addLast("c");
System.out.println(dq1);
System.out.println(dq1.peekFirst());
System.out.println(dq1.peekLast());
dq1.pollFirst();
System.out.println(dq1);
dq1.pollLast();
System.out.println(dq1);
}
Let’s take a closer look at some of the classes that implement List<E>
-
ArrayList
. More compact, less memory. -
LinkedList
. More efficient if high number of insertions/deletions (no shifting) -
Vector
. LikeArrayList
but better for concurrent code. -
Stack
. ExtendsVector
. Prefer theDequeue
interface (ArrayDequeue
) instead.
Another common interface from the java.util
collection is Map
which allow us to maintain a mapping between keys and values.
The Map
interface provides three collection views
-
a set of the map’s keys
-
a collection of the map’s values
-
a set of key-value mappings as a
Map.Entry
type. We will talk about this weird looking type name in our next class.
public static void main(String[] args) {
Map<Integer, String> arabicToRoman = new HashMap<>();
arabicToRoman.put(1, "I");
arabicToRoman.put(2, "II");
arabicToRoman.put(3, "III");
arabicToRoman.put(4, "IV");
arabicToRoman.put(5, "V");
System.out.println(arabicToRoman);
Set<Integer> keys = arabicToRoman.keySet();
System.out.println(keys);
Collection<String> values = arabicToRoman.values();
System.out.println(values);
for (Map.Entry<Integer,String> entry : arabicToRoman.entrySet()) {
System.out.println(entry);
}
}