CSE143 Notes for Friday, 4/9/10

We discussed several different aspects of the ArrayIntList class so that I could distribute a new version of the code.

As we discussed on Wednesday, we want to throw an exception when a client violates a precondition. For example, the ArrayIntList constructor that takes a capacity as an argument has a precondition that the capacity should not be negative:

        // pre : capacity >= 0
        // post: constructs an empty list with the given capacity
        public ArrayIntList(int capacity) {
            elementData = new int[capacity];
            size = 0;
        }
Recall that you can throw an IllegalArgumentException when the value of a parameter violates a precondition:

        // pre : capacity >= 0 (throws IllegalArgumentException if not)
        // post: constructs an empty list with the given capacity
        public ArrayIntList(int capacity) {
            if (capacity < 0) {
                throw new IllegalArgumentException("capacity: " capacity);
            }
            elementData = new int[capacity];
            size = 0;
        }
Notice that the exception is included in the comment for the method.

It may seem a little odd to construct an exception object each time you want to throw an exception, but there is a good reason for this. The exception object stores information about what was going on at the moment the exception was thrown. For example, it can provide a backtrace showing the sequence of calls that led to the exception. This is the information that Java displays when an exception like this is thrown. As a result, it is best to follow the pattern above of constructing the object as you are throwing it.

The throw statement is like a return statement in that it stops the method from executing. So we don't need an else branch for the other lines of code. The general pattern that Java programmers follow is to include one or more if statements that contain a throw statement at the beginning of a method to check obvious error conditions and then to include the actual code after the if statements.

We then looked at how to write a similar throw statement for the get method. The precondition for get says:

        // pre : 0 <= index < size()
We turned this into code by saying:

        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("illegal index");
        }
In this case, we throw a different exception. Instead of IllegalArgumentException, we throw IndexOutOfBoundsException. The rule in Java is to throw the most specific exception that you can. It would be okay to use IllegalArgumentException, but the IndexOutOfBounds exception is more specific about what is wrong.

Then we discussed the fact that the new version of ArrayIntList has a method called contains that can be used to ask the list if it contains a specific value. In some sense we don't need this method because we already have indexOf to find the location of a value in the list. But this is a standard method that many of the Java collections classes have, including the ArrayList that we are trying to understand.

I asked people what the return type should be and someone said boolean. So we want the method to look like this:

        public boolean contains(int value) {
            ...
        }
So how do we write this? We don't want to duplicate the code we included in indexOf, so instead we'll call indexOf. Remember that it returns a value of -1 if the value is not found, so we can test whether or not indexOf returned an index that is greater than or equal to 0:

        // very bad code
        public boolean contains(int value) {
            if (indexOf(value) >= 0) {
               return true;
            } else {
               return false;
            }
        }
I mentioned that we expect 143 students to use boolean variables and boolean expressions in their simplest form. This is something we refer to as "boolean zen" and it is described in chapter 5 of the textbook. Students will lose style points if they write code like what I have above.

Think about the core expression we're working with:

        indexOf(value) >= 0
What does this evaluate to? It evaluates either to true or to false. If it evaluates to true, we want to return true. If it evaluates to false, we want to return false. There's no need to use an if/else for this. We can simply return the value of the expression rather than including it in an if/else:

        return (indexOf(value) >= 0);
I put the expression inside parentheses to make it clear that it computes the value of that particular test, but even the parentheses are not necessary.

I then mentioned that another new method is called isEmpty. It returns a boolean value indicating whether or not the list is empty. I asked people how it would be implemented and people quickly saw that you can simply test the size to see if it is 0:

        public boolean isEmpty() {
            return (size == 0);
        }
Again, we don't need an if/else, we can simply return the value of the boolean expression.

We also discussed a "bulk add" method called addAll. It may seem a little odd to have one ArrayIntList dealing with another ArrayIntList, but this actually happens fairly often and you will have to practice it in your next homework assignment. The idea is that "this" ArrayIntList is supposed to add all of the values from the "other" ArrayIntList. So its header looks like this:

        public void addAll(ArrayIntList other) {
            ...
        }
We can call the appending add method to add values to the list. We just have to figure out how to write a loop that indicates what values to add. I wrote the method this way:

        public void addAll(ArrayIntList other) {
            for (int i = 0; i < other.size; i++)
                add(other.elementData[i]);
        }
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 int 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 int 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 get 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 {
            private ArrayIntList list;
            private int position;
            private boolean removeOK;
        
            public ArrayIntListIterator(ArrayIntList list) {
                position = 0;
                this.list = list;
                removeOK = false;
            }
        
            public int 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.

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 had a checkCapacity method that throws an exception if the array isn'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).

I also pointed out that in the new version of ArrayIntList, the get, set and remove methods all have a similar check of the index value. These two lines of code would be repeated in all three places:

        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("index: " + index);
        }
You'll notice that in the new version of ArrayIntList, these three lines of code appear in a private method that is called three times to avoid this redundancy.


Stuart Reges
Last modified: Sun Apr 11 13:47:35 PDT 2010