CSE143 Notes for Friday, 11/17/06

I began by discussing the next programming assignment. The idea is to construct a binary tree that has information about a number of different kinds of things. We use yes/no questions to distinguish between them.

Initially it constructs a tree with just one leaf node containing "computer":

                +---+      +------------+
    overallRoot | +-+-->   | "computer" |
                +---+      +------------+
In this particular program, leaf nodes contain the names of objects and branch nodes contain questions. Whenever we get to a leaf node, we have no choice left but to guess that particular thing. So if we use the tree above, we'd always start by asking whether the object happens to be computer. If the user says yes, then we've correctly guessed the object and we give the message:

        Great, I got it right!
If this isn't their object, then we expand our tree to incorporate this new kind of object as well. I asked the class for suggestions of what to think of and someone said "duck". To incorporate this into our tree, we need to replace the leaf node with a branch node that has a question and we want that node to have two leaves: our old object ("computer") and this new object. We start by asking the user what their object is. I said "duck." Then we ask the user for a question that distinguishes between their object and our object. We said, "Can you plug it in?" The plan is to replace our old leaf node with a branch node containing this question and with the old and new objects as leaves. But we don't know which one to put to the left and which one to put to the right. To figure that out, we have to ask the user what the answer is for their object. We said that the answer for "duck" is no. I mention in the assignment writeup that we'll follow the convention that yes answers go to the left and no answers go to the right. So I'd replace the old root with this new tree:

                +---+      +-----------------------+
    overallRoot | +-+-->   | "Can you plug it in?" |
                +---+      +-----------------------+
                                   /   \
                                  /     \
                     +------------+     +------------+
                     | "computer" |     |   "duck"   |
                     +------------+     +------------+
Then the process starts again. I asked people to think of another object and someone said "tree". So the program began by asking if our object can be plugged in. We said no. So then it reached the leaf node with "duck" in it. Whenever the program reaches a leaf node, it has no choice but to make that guess. So it asked us whether our object is duck and we said no. So it asked for a question to distinguish the two. Someone said, "Does it have a root?". Then it asked what the answer is for "tree" and we said yes. So now the tree becomes:

                +---+      +-----------------------+
    overallRoot | +-+-->   | "Can you plug it in?" |
                +---+      +-----------------------+
                                   /   \
                                  /     \
                     +------------+     +------------------------+
                     | "computer" |     | "Does it have a root?" |
                     +------------+     +------------------------+
                                                  /   \
                                                 /     \
                                      +----------+     +------------+
                                      |  "tree"  |     |   "duck"   |
                                      +----------+     +------------+
This process continues as long as the user wants to keep guessing. When the program finishes executing, you write out the contents of the tree to a file using a preorder traversal. That way, if the user wants to, they can start the next time with this as the initial tree. That would allow you to grow this tree bigger and bigger each time the game is played.

Then I switched to a new topic. I pointed out that in Wednesday's lecture we wrote an insert 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 sample log of execution 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). I also showed the client code that constructs a SearchTree<String> that puts words into alphabetical order and a SearchTree<Integer> that puts numbers into numerical order.

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. So what does compareTo return? The rule in Java is that:

So suppose that we have two String variables defined:

        String s1 = "hello";
        String s2 = "there";
We can't compare them the way we would compare primitive types, as in:

        // doesn't compile
        if (str1 <= str2) {
            ...
        }
We instead call the compareTo method and test the value that is returned relative to 0:

        if (str1.compareTo(str2) <= 0) {
            ...
        }
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, str1.compareTo(str2) returns ('h' - 't'), which equals -12, indicating that "hello" is less than "there").

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) {
                ...
            }

           ...
        }
We will go over some examples in next Tuesday's section.

In the short time that I had left, I went over some of the details that came 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 insert 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 insert(int next) {
            overallRoot = insert(next, overallRoot);
        }
    
        private IntTreeNode insert(int next, IntTreeNode root) {
            if (root == null)
                root = new IntTreeNode(next);
            else if (next <= root.data)
                root.left = insert(next, root.left);
            else
                root.right = insert(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 next) {
            overallRoot = insert(next, overallRoot);
        }
    
        private SearchTreeNode<E> insert(E next, SearchTreeNode<E> root) {
            if (root == null)
                root = new SearchTreeNode<E>(next);
            else if (next <= root.data)
                root.left = insert(next, root.left);
            else
                root.right = insert(next, root.right);
            return root;
        }
The problem is that we can no longer perform the test in this line of code:

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

        else if (next.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>) next).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.


Stuart Reges
Last modified: Mon Nov 20 12:47:41 PST 2006