CSE143 Notes for Friday, 2/29/08

I reminded people of something we discussed earlier when we talked about sorting. The String class has a method called compareTo that allows you to compare two String objects. So what does compareTo return? The rule in Java is that:

I demonstrated this in DrJava with a couple of String variables:

        > String s1 = "hillary"
        > String s2 = "barack"
        > s1.compareTo(s2)
        6
        > s1.compareTo(s1)
        0
        > s2.compareTo(s1)
        -6
Exactly what negative or positive integer is returned is left up to the person who implements the class. In the case of String, the method returns the difference between the character values of the first two characters that differ in the Strings (in the example, s1.compareTo(s2) returns ('h' - 'b'), which equals 6, indicating that "hillary" is alphabetically greater than "barack").

String is not the only class that can be compared in this way. There is an interface in Java known as the Comparable interface that captures this concept. Not every class implements Comparable because not all data can be put into sorted order in an intuitive way. For example, the Point class does not implement the Comparable interface because it's not clear what would make one two-dimensional point "less than" another two-dimensional point. But many of the standard classes we have seen like String and Integer implement the Comparable interface. Classes that implement the Comparable interface can be used for many built-in operations like sorting and searching. Some people pronounce it as "come-pair-a-bull" and some people pronounce it as "comp-ra-bull". Either pronunciation is okay. The interface contains a single method called compareTo.

        public interface Comparable<T> {
            public int compareTo(T other);
        }
Notice that this is a generic declaration using "T" to stand for "Type". And notice that we have just a method header for compareTo. Instead of a method body that would be enclosed in curly braces we have just a semicolon because, as always, an interface indicates that the method should exist, but doesn't say how it is implemented.

Given this interface, we can define the subset of Java classes for which sorting makes sense. Anything that implements the Comparable interface can be sorted. Implementing the Comparable interface is important enough that I will ask you to show me on the final exam that you can do it. For example, for a class Foo, you'd have to say this in the header:

        public class Foo implements Comparable<Foo> {
           ...
        }
Such a class would need a compareTo method:

        public class Foo implements Comparable<Foo> {
            public int compareTo(Foo other) {
                ...
            }

           ...
        }
I pointed out that in Wednesday's lecture we wrote an add method for the IntTree class that would allow you to build a binary search tree of integers. You might want to build other kinds of binary search trees with different kinds of data and you wouldn't want to make many copies of essentially the same code (one for integers, one for Strings, etc). Instead, you want to write the code in a more generic way.

I showed a client program for a new version of the class that I call SearchTree that will work for any class that implements what is known as the Comparable interface. The new class is a generic class, so it would be better to describe it as SearchTree<E> (for some element type E). The client code constructs a SearchTree<String> that puts words into alphabetical order and a SearchTree<Integer> that puts numbers into numerical order.

I then went over some of the details that come up in converting the IntTree binary search tree code into the generic SearchTree code. I mentioned that programming generic classes can be rather tricky. I'm showing this example so that you can see how it's done, but I wouldn't expect you to implement a generic class on your own. You should, however, know how to make use of a generic class or interface. For example, we might ask you to use a LinkedList<String> or we might ask you to implement the Comparable<T> interface, but you won't have to write a generic class from scratch.

We started by writing a node class for the tree. We found it was very tedious because we had to include the <E> notation in so many different places:

        public class SearchTreeNode<E> {
            public E data;
            public SearchTreeNode<E> left;
            public SearchTreeNode<E> right;
        
            public SearchTreeNode(E data) {
                this(data, null, null);
            }
        
            public SearchTreeNode(E data, SearchTreeNode<E> left,
                                  SearchTreeNode<E> right) {
                this.data = data;
                this.left = left;
                this.right = right;
            }
        }
Then I asked about the SearchTree class. Like our IntTree, it should have a single field to store a reference to the overall root, so we wrote the following:

        public class SearchTree<E> {
            private SearchTreeNode<E> overallRoot;

            ...
        }
We then looked at how to convert the IntTree add method into a corresponding generic method. The syntax makes it look fairly complicated, but in fact, it's not that different from the original code. Remember that our IntTree insert looks like this:

        public void add(int next) {
            overallRoot = add(next, overallRoot);
        }
    
        private IntTreeNode add(int next, IntTreeNode root) {
            if (root == null)
                root = new IntTreeNode(next);
            else if (next <= root.data)
                root.left = add(next, root.left);
            else
                root.right = add(next, root.right);
            return root;
        }
If we just replace "int" with "E" and replace "IntTreeNode" with "SearchTreeNode<E>", we almost get the right answer:

        public void insert(E value) {
            overallRoot = insert(value, overallRoot);
        }
    
        private SearchTreeNode<E> insert(E value, SearchTreeNode<E> root) {
            if (root == null)
                root = new SearchTreeNode<E>(value);
            else if (value <= root.data)
                root.left = insert(value, root.left);
            else
                root.right = insert(value, root.right);
            return root;
        }
The problem is that we can no longer perform the test in this line of code:

        else if (value <= root.data)
Instead, we have to use a call on the compareTo method:

        else if (value.compareTo(root.data) <= 0)
We have to make one more change as well. All that Java would know about these data values is that they are of some generic type E. That means that as far as Java is concerned, the only role it knows they can fill is the Object role. Unfortunately, the Object role does not include a compareTo method because not all classes implement the Comparable interface. We could fix this with a cast and that is what you'll find in most of the code written by Sun:

        else if (((Comparable<E>) value).compareTo(root.data) <= 0)
Another approach is to modify the class header to include this information. We want to add the constraint that the class E implements the Comparable interface. We specify that by saying:

        public class SearchTree<E extends Comparable<E>> {
            ...
        }
It's odd that Java has us use the keyword "extends" because we want it to implement the interface, but that's how generics work in Java. If we are defining a class, we make a distinction between when it extends another class versus when it implements an interface. But in generic declarations, we have just the word "extends", so we use it for both kinds of extension.

We then spent some time discussing how to write a contains method for the class. Someone pointed out that it could be written in a very similar manner to the add method. We started with a public method that calls a private method passing the overall root as a parameter:

        public boolean contains(E next) {
            return contains(next, overallRoot);
        }

        private boolean contains(E next, SearchTreeNode<E> root) {
            ...
        }
So we just have to write the body of the private method. An empty tree again serves as the easy case. If we're asked whether the empty tree contains a value, the answer is no (false):

        private boolean contains(E next, SearchTreeNode<E> root) {
            if (root == null)
                return false;
            ...
        }
For the add method, we had two other cases. We either added to the left subtree or added to the subtree. For this problem, there are three cases. We will have cases for searching the left and right subtree, but another possibility is that the root contains the value we are looking for. In fact, these three cases match exactly the three kinds of values returned by a call on compareTo. It's best not to call compareTo multiple times, so we introduced a local variable to store that result:

        private boolean contains(E next, SearchTreeNode<E> root) {
            if (root == null)
                return false;
            else {
                int compare = next.compareTo(root.data);
                ...
            }
        }
Then we just had to fill in the three different cases:

        private boolean contains(E next, SearchTreeNode<E> root) {
            if (root == null)
                return false;
            else {
                int compare = next.compareTo(root.data);
                if (compare == 0)
                    return true;
                else if (compare < 0)
                    return contains(next, root.left);
                else // compare > 0
                    return contains(next, root.right);
            }
        }

Stuart Reges
Last modified: Wed Mar 5 09:18:26 PST 2008