So far in class, we have been implementing simplified data structures in Java that only hold int
s (e.g. ArrayIntList
, IntTree
). This reading will explore the idea of generics that allows you to write classes that can hold any type the user wants! The final goal will be turning our IntTree
into a TreeSet<E>
such that it can hold any type that can be sorted. Before we do that, we start with a simplified generics example.
Pair¶
Suppose I wanted to write a Pair
class that represents a pair of values. With what we have seen so far, we might define the class to only hold int
s, like in the example below.
public class IntPair {
private int first;
private int second;
public IntPair(int first, int second) {
this.first = first;
this.second = second;
}
public int getFirst() {
return first;
}
public int getSecond() {
return second;
}
public String toString() {
return "(" + first + ", " + second + ")";
}
}
To construct this IntPair
I would write something like:
IntPair p = new IntPair(1, 2);
This is all well and good, but what if I want a pair of String
s instead? I could write a new class called StringPair
, which we have shown below.
public class StringPair {
private String first;
private String second;
public StringPair(String first, String second) {
this.first = first;
this.second = second;
}
public String getFirst() {
return first;
}
public String getSecond() {
return second;
}
public String toString() {
return "(" + first + ", " + second + ")";
}
}
To construct this StringPair
I would write something like:
StringPair p = new StringPair("a", "b");
This is a pretty ridiculous approach to solving this problem since it would require duplicating this class for every possible type of pair values in Java. These classes are exactly the same besides what the type is for the first
and second
values. It would be nicer if I could write a single Pair
class that is more generic in the sense it can hold any type of data!
Enter Type Parameters¶
Java 5 introduced this really nifty feature that allows you to have a class accept a type parameter in order to make it more generic. The type parameter lets you write one version of your class that can work with any type of object that it will store. To specify a type parameter, you use this <E>
syntax in the class name. We start by showing a generic Pair<E>
class below and then we explain what this does.
public class Pair<E> {
private E first;
private E second;
public Pair(E first, E second) {
this.first = first;
this.second = second;
}
public E getFirst() {
return first;
}
public E getSecond() {
return second;
}
public String toString() {
return "(" + first + ", " + second + ")";
}
}
How would you now construct one of these Pair<E>
? You have to specify the type when you construct it!
Pair<Integer> p1 = new Pair<Integer>(1, 2); // Notice Integer, not int
Pair<String> p2 = new Pair<String>("a", "b");
What have we done here? We have made a Pair
class that is generic: it takes a type parameter E
that indicates what type of value it will hold and replaces all instances of a specific value type with this E
. In some sense, E
is just a place-holder type for any type someone might make a pair of (in the example above: for p1
, E
is Integer
and for p2
, E
is String
). You can think of it as, when you make a Pair<Scanner>
it goes ahead and makes a new class based on Pair<E>
, but all the E
s are replaced by Scanner
!
This is helpful because you only need to write a single Pair
object and it can be used in many different contexts!
Interlude: What about Object
?¶
You might be thinking there is another way to solve this problem:
Forget type parameters, I’ll just use this
Object
class that is a super-class of all classes!
In this section, we will ignore everything we have just seen about these type parameters and try to solve this problem without them. For example, you could write the class below without using type parameters.
public class ObjectPair {
private Object first;
private Object second;
public ObjectPair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
public String toString() {
return "(" + first + ", " + second + ")";
}
}
And then when you wanted to construct it, you could write something like the following.
ObjectPair p3 = new ObjectPair(1, 2);
ObjectPair p4 = new ObjectPair("a", "b");
This is actually not a bad idea since this is how we had to write generic classes before the advent of Java 5. However, we’ll see this causes some problems later on, when we want to use the values inside the instance. For example, what if you wanted to call the getFirst
method on p4
and print out the upper case version of that String
? You might try to write something like:
System.out.println(p4.getFirst().toUpperCase()); // causes a compiler error
This doesn’t work because Java doesn’t know the return type of p4.getFirst()
is a String
! All it knows is it’s an Object
which means any type could actually be in there (i.e. the PromiseType
is Object
). With what we saw from the inheritance lesson, you could get around this by using casting to tell Java to chill since you know the value is actually a String
, but this is error prone since many casting errors can’t be checked at compile time.
This awkwardness with casting is essentially the whole reason type parameters exist! With generics, you can guarantee at compile-time that if you call p1.getFirst()
(recall p1
is type Pair<Integer>
), you know it’s return type will be Integer
so you never have to cast.
Multiple Type Parameters¶
In some applications, it makes sense to make your class be generic with multiple type parameters. You can add more than one type parameter by putting a comma separated list of type parameter names. For example, below we show a Pair2<E1, E2>
class that can have different types for the first and second value.
public class Pair2<E1, E2> {
private E1 first;
private E2 second;
public Pair2(E1 first, E2 second) {
this.first = first;
this.second = second;
}
public E1 getFirst() {
return first;
}
public E2 getSecond() {
return second;
}
public String toString() {
return "(" + first + ", " + second + ")";
}
}
This can be useful in some cases for more complicated classes. The intuition is the same, but now you can have differing types. If you made a Pair2<String, Integer>
, you would know that getFirst
returns a String
while getSecond
returns an Integer
.
Make TreeSet<E>
¶
With everything we have seen so far, we are ready to tackle turning IntTree
into TreeSet<E>
. Below we show the whole class which is the same as the IntTree
we ended with two weeks ago except: * We have renamed it to TreeSet
* We have made it generic so it now takes a type parameter E
. This means every place we used int
, we know use this generic E
value. This means we have renamed IntTreeNode
to TreeNode<E>
as well!
This looks complicated at first, but notice it’s exactly the same class with just these find/replace operations described above!
// This class represents a binary search tree of any type
// A binary search tree is one that every value to the left of a value is less
// than it, and every value to the right is greater than it. Duplicates are not allowed.
public class TreeSet<E> {
private TreeNode<E> overallRoot;
// Constructs a tree with no numbers
public TreeSet() {
overallRoot = null;
}
// Returns true if the value is in this tree, false otherwise
public boolean contains(E value) {
return contains(overallRoot, value);
}
// Returns true if the given value is contained in the tree
// rooted at the given root, false otherwise
private boolean contains(TreeNode<E> root, E value) {
if (root == null) {
return false;
} else if (root.data == value) {
return true;
} else if (value < root.data) {
return contains(root.left, value);
} else { // value > root.data
return contains(root.right, value);
}
}
// pre: This is a binary search tree
// post: Adds the value to the tree such that
// it maintains the binary search tree property
public void add(E value) {
overallRoot = add(overallRoot, value);
}
// pre: the subtree rooted at `root` is a binary search tree
// post: adds the given value to the subtree rooted at
// `root` such that it preserves the binary search tree
// property
private TreeNode<E> add(TreeNode<E> root, E value) {
if (root == null) {
root = new TreeNode<E>(value);
} else if (value < root.data) {
root.left = add(root.left, value);
} else if (value > root.data) {
root.right = add(root.right, value);
}
return root;
}
// Returns the number of numbers in this tree.
public int size() {
return size(overallRoot);
}
// Returns the number of nodes in the tree starting at root.
private int size(TreeNode<E> root) {
if (root == null) {
return 0;
} else {
int leftSize = size(root.left);
int rightSize = size(root.right);
return 1 + leftSize + rightSize;
}
}
// Class that represents a single node in the tree
private static class TreeNode<E> {
public E data; // data stored at this node
public TreeNode<E> left; // reference to left subtree
public TreeNode<E> right; // reference to right subtree
// Constructs a leaf node with the given data.
public TreeNode(E data) {
this(data, null, null);
}
// Constructs a leaf or branch node with the given data and links.
public TreeNode(E data, TreeNode<E> left, TreeNode<E> right) {
this.data = data;
this.left = left;
this.right = right;
}
}
}
Unfortunately, this class doesn’t work as written! It’s pretty close, but there is one pretty big flaw that has to do with how we do the “less than” or “greater than” checks! Remember, <
or >
only work for numeric types like integers or doubles. Inside our class, we have no idea what type E
is! This means, it can be any Object
type so there is no guarantee you can use <
or >
to compare them!
How do we compare objects that aren’t numeric types in Java? We have to make sure they implement the Comparable<E>
interface so that we are sure they have a compareTo(E)
method!
The first thing we need to do to accomplish this is enforce that the type E
implements the Comparable<E>
interface. The syntax for this looks a bit weird, but you can put restrictions on what types are valid for a type parameter using the syntax shown below.
public class TreeSet<E extends Comparable<E>> {
...
}
What this is saying is that the only types that are allowed inside the TreeSet
are types that implement the Comparable<E>
of interface. This means TreeSet<String>
is allowed, but TreeSet<List<String>>
is not since List<String>
is not comparable. Confusingly, you use the extends
keyword in type parameters to put these restrictions, even though Comparable<E>
is an interface.
The second thing we need to accomplish is replacing all of the <
, >
, and ==
in our code with calls on compareTo
. Below we just show the contains
method and then show the full class at the bottom. All this method is doing is just using compareTo
as we learned about so that we are able to figure out if value
comes before or after root.data
in the sorted ordering; the logic is exactly the same but the mechanics of how you do the check differ.
// Returns true if the value is in this tree, false otherwise
public boolean contains(E value) {
return contains(overallRoot, value);
}
// Returns true if the given value is contained in the tree
// rooted at the given root, false otherwise
private boolean contains(TreeNode<E> root, E value) {
if (root == null) {
return false;
} else if (root.data.compareTo(value) == 0) {
return true;
} else if (value.compareTo(root.data) < 0) { // value < root.data
return contains(root.left, value);
} else { // value > root.data
return contains(root.right, value);
}
}
Below, we show the fully generic TreeSet<E>
with correct comparison tests
// This class represents a binary search tree of any type
// A binary search tree is one that every value to the left of a value is less
// than it, and every value to the right is greater than it. Duplicates are not allowed.
public class TreeSet<E extends Comparable<E>> {
private TreeNode<E> overallRoot;
// Constructs a tree with no numbers
public TreeSet() {
overallRoot = null;
}
// Returns true if the value is in this tree, false otherwise
public boolean contains(E value) {
return contains(overallRoot, value);
}
// Returns true if the given value is contained in the tree
// rooted at the given root, false otherwise
private boolean contains(TreeNode<E> root, E value) {
if (root == null) {
return false;
} else if (root.data.compareTo(value) == 0) {
return true;
} else if (value.compareTo(root.data) < 0) { // value < root.data
return contains(root.left, value);
} else { // value > root.data
return contains(root.right, value);
}
}
// pre: This is a binary search tree
// post: Adds the value to the tree such that
// it maintains the binary search tree property
public void add(E value) {
overallRoot = add(overallRoot, value);
}
// pre: the subtree rooted at `root` is a binary search tree
// post: adds the given value to the subtree rooted at
// `root` such that it preserves the binary search tree
// property
private TreeNode<E> add(TreeNode<E> root, E value) {
if (root == null) {
root = new TreeNode<E>(value);
} else if (value.compareTo(root.data) < 0) { // value < root.data
root.left = add(root.left, value);
} else if (value.compareTo(root.data) > 0) { // value > root.data
root.right = add(root.right, value);
}
return root;
}
// Returns the number of numbers in this tree.
public int size() {
return size(overallRoot);
}
// Returns the number of nodes in the tree starting at root.
private int size(TreeNode<E> root) {
if (root == null) {
return 0;
} else {
int leftSize = size(root.left);
int rightSize = size(root.right);
return 1 + leftSize + rightSize;
}
}
// Class that represents a single node in the tree
private static class TreeNode<E> {
public E data; // data stored at this node
public TreeNode<E> left; // reference to left subtree
public TreeNode<E> right; // reference to right subtree
// Constructs a leaf node with the given data.
public TreeNode(E data) {
this(data, null, null);
}
// Constructs a leaf or branch node with the given data and links.
public TreeNode(E data, TreeNode<E> left, TreeNode<E> right) {
this.data = data;
this.left = left;
this.right = right;
}
}
}
Generic Collections¶
Below, we link all of the files we worked on class today but with their generic versions.