We are only going to keep track of degrees and minutes, so we began with a class that has fields for each and a constructor:
public class Angle { private int degrees; private int minutes; public Angle(int degrees, int minutes) { this.degrees = degrees; this.minutes = minutes; } }We started with some client code that constructed an array of Angle objects and that printed it out:
import java.util.*; public class AngleTest { public static void main(String[] args) { Angle a1 = new Angle(23, 26); Angle a2 = new Angle(15, 48); List<Angle> list = new ArrayList<Angle>(); list.add(a1); list.add(a2); System.out.println(list); } }When we ran this program, it produced output like the following:
[Angle@42719c, Angle@30c221]The text being generated in each case includes the name of the class (Angle) followed by an at sign followed by a hexadecimal (base 16) address. This is what the built-in toString method produces. To get better output, we decided to add a toString method to the class.
Ideally we would show something like 23 degrees and 26 minutes using the standard symbols used for degrees and minutes:
23° 26′But those characters aren't easy to work with in Java, so instead we'll settle for "d" for degrees and "m" for minutes:
23d 26mWe wrote the following toString method to produce that output:
public String toString() { return degrees + "d " + minutes + "m"; }When we ran the client code again, it produced the following output:
[23d 26m, 15d 48m]Then I discussed how to include a method that would add two of these angles together. In a sense, what I'd like to be able to say in the client code is something like this:
Angle a1 = new Angle(23, 26); Angle a2 = new Angle(15, 48); Angle a3 = a1 + a2;We can't write it that way because the "+" operator is used only for numbers and String concatenation. Some programming languages like C++ allow you to do something called "operator overloading," but Java doesn't allow that. So we have to settle for having a method called "add" that we can use to ask one of these angles to add another one to it:
Angle a1 = new Angle(23, 26); Angle a2 = new Angle(15, 48); Angle a3 = a1.add(a2);I pointed out that this is a common convention in the Java class libraries. For example, there is a class called BigInteger for storing very large integers that has a method called add that is similar.
We wrote this as the first version of our add method:
public Angle add(Angle other) { int d = degrees + other.degrees; int m = minutes + other.minutes; return new Angle(d, m); }I pointed out that we're referring to other.degrees and other.minutes, which are private fields. This works because the understanding of the word private is that it is "private to the class." This is not at all the way that we as humans understand the meaning of private (if something is private to me, then it shouldn't be available to other humans). But in Java, one Angle object can access private elements of another Angle object because they are both of the same class.
I modified the client code to add this angle to the list as well:
Angle a1 = new Angle(23, 26); Angle a2 = new Angle(15, 48); Angle a3 = a1.add(a2); List<Angle> list = new ArrayList<Angle>(); list.add(a1); list.add(a2); list.add(a3); System.out.println(list);When we ran it, we got the following output:
[23d 26m, 15d 48m, 38d 74m]Clearly the program has added the two angles to get a third, but the third angle is listed as having 74 minutes, which isn't right. I mentioned that this is a good place to apply the idea of a class invariant, which is described in chapter 8 of the textbook. We want to guarantee that the values for minutes and seconds are always legal. We can begin by adding a precondition to the constructor:
// pre: minutes <= 59 and minutes >= 0 and degrees >= 0 public Angle(int degrees, int minutes) { this.degrees = degrees; this.minutes = minutes; }Then we added code to throw an exception when the precondition was not satisfied:
// pre: minutes <= 59 and minutes >= 0 and degrees >= 0 // (throws IllegalArgumentException if not true) public Angle(int degrees, int minutes) { if (minutes < 0 || minutes > 59 || degrees < 0) { throw new IllegalArgumentException(); } this.degrees = degrees; this.minutes = minutes; }But we still needed to handle the case where the minutes become larger than 59. We were able to fix this with a simple if statement:
public Angle add(Angle other) { int d = degrees + other.degrees; int m = minutes + other.minutes; if (m >= 60) { m -= 60; d++; } return new Angle(d, m); }Because of the class invariant, we know that we don't need more than a simple if to solve this problem because adding two legal angles together can't require more than one operation of converting 60 minutes to a degree.
It also would have been possible to use integer division and the mod operator to figure this out.
When we ran the code again, we got this output, indicating that it had correctly added the two angles together:
[23d 26m, 15d 48m, 39d 14m]Then I said that I wanted to explore how to modify the class so that we can put a collection of Angle objects into sorted order. I added the following client code to add a specific list of Angle values to our list and then called Collections.sort to put them into sorted order:
int[][]data = {{30, 19}, {30, 12}, {30, 45}, {30, 8}, {30, 55}}; for (int[] coords : data) { list.add(new Angle(coords[0], coords[1])); } System.out.println(list); Collections.sort(list); System.out.println(list);Unfortunately, this code did not compile. That's because we haven't told Java how to compare Angle objects to put them into order. For example, we know that an angle of 45 degrees and 15 minutes is more than an angle of 30 degrees and 55 minutes, but how is Java supposed to figure that out?
If you want to use utilities like Arrays.sort and Collections.sort, you have to indicate to Java how to compare values to figure out their ordering. 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); }So what does compareTo return? The rule in Java is that:
public class Angle implements Comparable<Angle> { ... }We completed the compareTo method fairly quickly. I pointed out that in general the degrees field tells you which Angle is larger, so we can almost get by with writing the method this way:
public int compareTo(Angle other) { return degrees - other.degrees; }Consider the case where an Angle object has a value for degrees that is larger than the other object's value of degrees. Then the expression above returns a positive integer, which indicates that the object is greater. If an Angle object has a lower value for degrees than the other one, then this expression returns a negative value, which indicates that it is less than the other Angle object.
The only case where this fails is when the degree values are equal. In that case, we should use the minutes fields in a similar manner to break the tie. So the right way to code this is as follows:
public int compareTo(Angle other) { if (degrees == other.degrees) { return minutes - other.minutes; } else { return degrees - other.degrees; } }When we ran the client code with this new version of the code, we got output like the following:
[23d 26m, 15d 48m, 39d 14m] [23d 26m, 15d 48m, 39d 14m, 30d 19m, 30d 12m, 30d 45m, 30d 8m, 30d 55m] [15d 48m, 23d 26m, 30d 19m, 30d 12m, 30d 45m, 30d 8m, 30d 55m, 39d 14m]Then I spent some time showing how to write a generic version of the binary search tree class. I pointed out that in a previous 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 add looks like this:
public void add(int value) { overallRoot = add(value, overallRoot); } private IntTreeNode add(IntTreeNode root, int value) { if (root == null) { root = new IntTreeNode(value); } else if (value <= root.data) { root.left = add(root.left, value); } else { root.right = add(root.right, value); } return root; }If we just replace "int" with "E" and replace "IntTreeNode" with "SearchTreeNode<E>", we almost get the right answer:
public void add(E value) { overallRoot = add(overallRoot, value); } private SearchTreeNode<E> add(SearchTreeNode<E> root, E value) { if (root == null) { root = new SearchTreeNode<E>(value); } else if (value <= root.data) { root.left = add(root.left, value); } else { root.right = add(root.right, value); } 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.
Then I spent a few minutes 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!When we exit the program, the current tree is written to a file called question.txt. We saw that with just this simple tree composed of one leaf node, the file looks like this:
A: computerThen we explored what happens when the user thinks of something other than a computer as their object. In that case, we expand our tree to incorporate the new kind of object as well. I asked the class for suggestions of what to think of and someone said "tree". 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 "tree." Then we ask the user for a question that distinguishes between their object and our object. We said, "Does it have life?"
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 "tree" is yes. 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 we replace the old root with this new tree:
+---+ +-----------------------+ overallRoot | +-+--> | "Does it have life? " | +---+ +-----------------------+ / \ / \ +---------+ +------------+ | "tree" | | "computer" | +---------+ +------------+When we exited the program, it wrote this information to question.txt:
Q: Does it have life? A: tree A: computerThe information is stored using a preorder traversal of the tree. When I ran the program again, I told it to read back in this file. I asked people to think of another object and someone said "apple." So the program began by asking if our object has life. I said yes. So then it reached the leaf node with "tree" 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 tree and we said no. So it asked for a question to distinguish the two. Someone said, "Is it red?". Then it asked what the answer is for "apple" and we said yes. So now the tree becomes:
+---+ +-----------------------+ overallRoot | +-+--> | "Does it have life? " | +---+ +-----------------------+ / \ / \ +--------------+ +------------+ | "Is it red?" | | "computer" | +--------------+ +------------+ / \ / \ +---------+ +--------+ | "apple" | | "tree" | +---------+ +--------+This process continues as long as the user wants to keep guessing. When the program finishes executing, you write out the contents to question.txt 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.
We saw that after adding this second object, the program wrote the following to question.txt:
Q: Does it have life? Q: Is it red? A: apple A: tree A: computerI also pointed out that the zip file for the assignment includes a file called bigquestion.txt that has almost 10 thousand entries for animals. You have to rename the file to question.txt, but then your program should be able to read it in and play the game.