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 8m, 30d 12m, 30d 19m, 30d 45m, 30d 55m, 39d 14m]
I said that I wanted to finish up our discussion of the ArrayIntList class. We
ended with a pretty good version of the class to serve as a guide for the first
homework, but I mentioned that there is at least one important method that we
were missing.I asked people to consider the situation where a client wants to replace a value at a particular location. The only option we have given the client is to remove the value and then to add a new value back in its place. This requires shifting values twice, which can be very inefficient if the list is long and if the change occurs towards the front of the list. So I said that we would include a method called set that can be used to replace the value at a given inde:
public void set(int index, int value) {
elementData[index] = value;
}
Of course, we have to indicate the precondition on the index and we have to
check the index to make sure it is legal. We introduced a private method
called checkIndex that performs the check for us:
// pre : 0 <= index < size()
// post: replaces the integer at the given index with the given value
public void set(int index, int value) {
checkIndex(index);
elementData[index] = value;
}
I mentioned that the new version also has a method called clear that returns
the list to being empty by resetting the size to 0:
// post: list is empty
public void clear() {
size = 0;
}
Then I said I wanted to discuss how to implement an iterator.
Recall that an iterator as having three basic operations:
Iterator<Integer> i = list.iterator();
int product = 1;
while (i.hasNext()) {
int next = i.next();
product = product * next;
}
System.out.println("product = " + product);
this variation of the code prints each value and removes any occurrences of
values that are multiples of 3:
Iterator<Integer> i = list.iterator();
int product = 1;
while (i.hasNext()) {
int next = i.next();
product = product * next;
if (next % 3 == 0) {
i.remove();
}
}
System.out.println("product = " + product);
This code examines each value in the list and removes all the multiples of
3.Then we spent some time discussing how the ArrayIntListIterator is implemented. The main function the iterator performs is to keep track of a particular position in a list, so the primary field will be an integer variable for storing this position:
public class ArrayIntListIterator {
private int position;
public ArrayIntListIterator(?) {
position = 0;
}
public Integer next() {
position++;
}
...
}
I asked people how we would implement hasNext and someone said we'd have to
compare the position against the size of the list. I then said, "What list?"
Obviously the iterator also needs to keep track of which list it is iterating
over. We can provide this information in the constructor for the iterator. So
the basic outline became:
public class ArrayIntListIterator {
private ArrayIntList list;
private int position;
public ArrayIntListIterator(ArrayIntList list) {
position = 0;
this.list = list;
}
public Integer next() {
use get method of list & position
position++;
}
public boolean hasNext() {
check position against size
}
...
}
We briefly discussed how to implement remove. We have to keep track of when
it's legal to remove a value. Recall that you can't remove before you have
called next and you can't call remove twice in a row. We decided that this
could be implemented with a boolean flag inside the iterator that would keep
track of whether or not it is legal to remove at any given point in time.
Using this flag, we can throw an exception in remove if it is not legal to
remove at that point in time:
public class ArrayIntListIterator implements Iterator<Integer> {
private ArrayIntList list;
private int position;
private boolean removeOK;
public ArrayIntListIterator(ArrayIntList list) {
position = 0;
this.list = list;
removeOK = false;
}
public Integer next() {
use get method of list & position
position++
removeOK = true;
}
public boolean hasNext() {
check position against size
}
public void remove() {
if (!removeOK) {
throw new IllegalStateException()
}
call remove method on list
removeOK = false;
}
}
This is a fairly complete sketch of the ArrayIntListIterator code. The
calendar includes a complete version. You will notice some odd details that
will make more sense after we have learned more about the collections framework
(e.g., the class implements the Iterator<Integer> interface and the next method
returns a value of type Integer instead of int).Then I discussed the fact that the new version of the list "grows" the list as needed if it runs out of capacity. It isn't, in general, easy to make an array bigger. We can't simply grab the memory next to it because that memory is probably being used by some other object. Instead, we have to allocate a brand new array and copy values from the old array to the new array. This is pretty close to how shops and other businesses work in the real world. If you need some extra space for your store, you can't generally break down the wall and grab some of the space from the store next door. More often a store has to relocate to a larger space.
The new version of ArrayIntList has this functionality built into it. In the previous version we manually checked the capacity and threw an exception if the array wasn't big enough. In the new version that has been replaced by an ensureCapacity method that constructs a new array if necessary.
Obviously you don't want to construct a new array too often. For example, suppose you had space for 1000 values and found you needed space for one more. You could allocate a new array of length 1001 and copy the 1000 values over. Then if you find you need space for one more, you could make an array that is 1002 in length and copy the 1001 old values over. This kind of growth policy would be very expensive.
Instead, we do something like doubling the size of the array when we run out of space. So if we have filled up an array of length 1000, we double its size to 2000 when the client adds something more. That makes that particular add expensive in that it has to copy 1000 values from the old array to the new array. But it means we won't need to copy again for a while. How long? We can add another 999 times before we'd need extra space. As a result, we think of the expense as being spread out or "amortized" over all 1000 adds. Spread out over 1000 adds, the cost is fairly low (a constant).
You will find that the built-in ArrayList class does something similar. The documentation is a little coy about this saying, "The details of the growth policy are not specified beyond the fact that adding an element has constant amortized time cost." If you look at the actual code, you'll find that increase by 50% each time (a multiplier of 1.5).
The latest version of the ArrayIntList class along with the ArrayIntListIterator class are included in the calendar for this lecture.