CSE143 Notes for Wednesday, 3/31/10

I reminded people that I will be using Java's ArrayList class as a kind of software cadaver that we will dissect as the quarter progresses. ArrayList is one of the most commonly used classes in the Java Class Libraries. It will take us a while to understand it because of its complexity. To simplify things, we will study something that I call ArrayIntList, which is a variation of ArrayList for storing int values. Even this simplification won't be enough to allow us to understand the class right away, so my plan is to develop the class through several stages, adding functionality at each stage.

I said that we'd need some client code. I suggested that we have something very simple that creates two lists, adds some values to each and prints them. Suppose you're going to write a class like this (where I'm using pseudocode in comments to indicate what I plan to do):

        public class ArrayIntListClient {
            public static void main(String[] args) {
                // construct ArrayIntList list1
                // construct ArrayIntList list2
                // add 1, 82, 97 to list1
                // add 7, -8 to list2
                // print list1
                // print list2
            }
        }
We are going to implement the ArrayIntList as an unfilled array, so we need two variables. We need an array and we need a size variable. Remember that there is a difference between the capacity of the list (the length of the array) and the current size. Our plan is to start out with an empty list initially and to fill it up as the client requests us to add values.

If we're going to have two ArrayIntLists, as in the pseudocode above, then we'd need four variables (two arrays and two sizes). Obviously this isn't a good solution. This is where the ArrayIntList class comes in. Instead of declaring two arrays and two sizes, we can include those variables inside the ArrayIntList class:

        public class ArrayIntList {
            int[] elementData = new int[100];
            int size = 0;
        }
The two variables (the array called elementData and the variable called size) become the data fields or instance variables of the class. These variables become part of what we call the "state" of the object. Once the object is constructed, these variables have a permanence to them that local variables don't have. They stay around indefinitely. They are the "innards" that allow this object to do what it has to do.

So we avoid having four different variables (two arrays and two sizes) by having two different objects (each with its own array and its own size). They will need to be constructed, which means the first two lines in our client code will become calls on "new":

        public class ArrayIntListClient {
            public static void main(String[] args) {
                ArrayIntList list1 = new ArrayIntList();
                ArrayIntList list2 = new ArrayIntList();
                // add 1, 82, 97 to list1
                // add 7, -8 to list2
                // print list1
                // print list2
            }
        }
Then we turned to the idea of adding values to the list. We could directly manipulate the two fields inside of each list object. For example, to add the values 1, 82, 97, to list1, we could set each array element and set the size:

        public class ArrayIntListClient {
            public static void main(String[] args) {
                ArrayIntList list1 = new ArrayIntList();
                ArrayIntList list2 = new ArrayIntList();
                // add 1, 82, 97 to list1
                list1.elementData[0] = 1;
                list1.elementData[1] = 82;
                list1.elementData[2] = 97;
                list1.size = 3;
                // add 7, -8 to list2
                // print list1
                // print list2
            }
        }
But this isn't a very object-oriented approach. We don't want to make the client have to deal with low-level details of the class. Instead, it makes sense to introduce an add method that we can call from the client code:

        public class ArrayIntListClient {
            public static void main(String[] args) {
                ArrayIntList list1 = new ArrayIntList();
                ArrayIntList list2 = new ArrayIntList();
                list1.add(1);
                list1.add(82);
                list1.add(97);
                list2.add(7);
                list2.add(-8);
                // print list1
                // print list2
            }
        }
Notice that each time we call add, we indicate which list we want to add values to (list1 for the first three calls on add, list2 for the next two calls). So we want to include an add method in our ArrayIntList class.

The section handout indicated that the following lines of code could be executed to add a value to the end of an unfilled array:

        elementData[size] = value;
        size++;
We can put this inside a method called "add". What would it's parameters be? Obviously it would need to know the value to be appended to the list. But how does the method get access to the array called elementData and the variable called size? The answer is that we are going to write an instance method which has something known as an implicit parameter. In cse142 we mostly wrote static methods, as in:

        public static void add(int value) {
            // defines a static method add
            ...
        }
By removing the word static from the header, we turn this into an instance method:

        public void add(int value) {
            // defines an instance method add
            ...
        }
Instance methods require the dot notation when you call them. With a static method you could say:

        add(17);  // call on static method
With an instance method, we have to mention which list we want to add a value to, as in:

        list1.add(1);  // calling instance method on list1
        list2.add(7);  // calling instance method on list2
The variable that appears before the dot is known as the implicit parameter. In the first call above, list1 is the implicit parameter. It's as if someone were to shout, "Hey, list1! I'm talking to you. I want you to execute your add method with a value of 1." In the second call, list2 is the implicit parameter ("Hey, list2! I'm talking to you. Execute your add method with a value of 7").

After adding this method to the ArrayIntList class it looked like this:

        public class ArrayIntList {
            int[] elementData = new int[100];
            int size = 0;

            public void add(int value) {
                elementData[size] = value;
                size++;
            }
        }
There are two key ideas here. First, each instance of the ArrayIntList class has its own fields called elementData and size. Second, when we call an instance method like add, there is an implicit parameter (a particular object that we are talking to). When we refer to variables like elementData and size inside an instance method, we are saying, "Modify the fields of the object you are talking to."

We then talked about the idea of printing the two lists. As a starting point, I suggested that we print the size of each list after the calls on add:

        public class ArrayIntListClient {
            public static void main(String[] args) {
                ArrayIntList list1 = new ArrayIntList();
                ArrayIntList list2 = new ArrayIntList();
                list1.add(1);
                list1.add(82);
                list1.add(97);
                list2.add(7);
                list2.add(-8);

                System.out.println(list1.size);
                System.out.println(list2.size);
            }
        }
As you would expect, the program printed the values 3 and 2, which is a good sign. So how do we print the contents of the lists? I suggested that we use the print method like the one discussed in section:

        public static void print(int[] list) {
            if (list.length == 0) {
                System.out.println("[]");
            } else {
                System.out.print("[" + list[0]);
                for (int i = 1; i < list.length; i++) {
                    System.out.print(", " + list[i]);
                }
                System.out.println("]");
            }
        }
We had to make several modifications to turn this into an instance method. We removed the word "static" because this is going to be an instance method. We also removed the parameter "list". Instead, we changed "list" to "elementData" so that we will be looking at the array stored inside of whatever object we are printing. We also had to change "list.length" to "size" because we only want to print values up to the current size (not up to the capacity).

        public void print() {
            if (size == 0) {
                System.out.println("[]");
            } else {
                System.out.print("[" + elementData[0]);
                for (int i = 1; i < size; i++)
                    System.out.print(", " + elementData[i]);
                System.out.println("]");
            }
        }
We then modified our client code to include calls on this print method for each list:

        public class ArrayIntListClient {
            public static void main(String[] args) {
                ArrayIntList list1 = new ArrayIntList();
                ArrayIntList list2 = new ArrayIntList();
                list1.add(1);
                list1.add(82);
                list1.add(97);
                list2.add(7);
                list2.add(-8);
                list1.print();
                list2.print();
                System.out.println(list1.size);
                System.out.println(list2.size);
            }
        }
This worked fairly well. The print commands produced these lines of output:

        [1, 82, 97]
        [7, -8]
I then said let's just see what happens when we include a simple println statement for each list. So at the end of the client code, we added these two lines of code:

        System.out.println("list1 = " + list1);
        System.out.println("list2 = " + list2);
This did not produce good output. It produced a weird output that included the name of the class and an "@" and a hexadecimal number. Someone then suggested the idea of a toString method. We discussed the fact that this print method isn't very flexible. It always sends its output to System.out. What if you are writing a GUI (Graphical User Interface) and want the output to go to some particular part of the screen? What if you are writing to a file? What if you want to print several lists on a single line of output? We couldn't do any of these things with this print method.

So we then rewrote the print method to have it return a String. Java has a standard name for a method that converts an object into text form: toString, so we also changed its name:

        public String toString() {
            if (size == 0) {
                return "[]";
            } else {
                String result = "[" + elementData[0];
                for (int i = 1; i < size; i++)
                    result += ", " + elementData[i];
                result += "]";
                return result;
            }
        }
We then had to change our client code to print the String returned by this method. At this point, our client code looked like this:

        public class ArrayIntListClient {
            public static void main(String[] args) {
                ArrayIntList list1 = new ArrayIntList();
                ArrayIntList list2 = new ArrayIntList();
                list1.add(1);
                list1.add(82);
                list1.add(97);
                list2.add(7);
                list2.add(-8);
                System.out.println(list1.toString());
                System.out.println(list2.toString());
                System.out.println(list1.size);
                System.out.println(list2.size);
                System.out.println("list1 = " + list1);
                System.out.println("list2 = " + list2);
            }
        }
The first two printlns were calling our toString method and showed the list contents, as we would expect. The next two lines reported the sizes of the lists. The final two lines didn't work before, but suddenly the two println calls at the end of the program started producing good output. That's because the toString method in Java has special properties. You can call it explicitly, as in the first two calls, but you can also leave off the call, as in the last two printlns, in which case Java calls toString for you (an implicit call).

I then spent some time discussing the idea of binary search. This is an important algorithm to understand and we're going to use it for the next programming assignment. We'll also be discussing it in section.

The idea behind binary search is that if you have a sorted list of numbers, then you can quickly locate any given value in the list. The approach is similar to the one used in the guessing game program from CSE142. In that game the user is supposed to guess a number between 1 and 100 given clues about whether the answer is higher or lower than their guess. So you begin by guessing the middle value, 50. That way you've eliminated half of the possible values once you know whether the answer is higher or lower than that guess. You can continue that process, always cutting your range of possible answers in half by guessing the middle value.

Binary search uses a similar approach. The Java class libraries include a version of binary search that you can call. The method looks like this:

        public static int binarySearch(int[] list, int fromIndex, int toIndex,
                                       int key) {
            ...
        }
The convention in Java is to specify a range like this using a from-index that is inclusive and a to-index that is inclusive. For example, if you want to search indexes 0 through 12, you use a from-index of 0 and a to-index of 13. The to-index is always 1 higher than the actual value you want to search for. This can be confusing, but that's the convention used consistently throughout the Java class libraries.

At any given time, we have a particular range of values that have yet to be searched. Initially this is the full range, so we'd begin our method by initializing a pair of variables to keep track of this range:

        int low = fromIndex;
        int high = toIndex - 1;
Then we have a while loop that will continue searching until it finds the values or runs out of values to search:

        int low = fromIndex;
        int high = toIndex - 1;
        while (...) {
            ...
        }
We always focus our attention on the middle value, so we can compute the index of that value and store the value in a local variable:

        int low = fromIndex;
        int high = toIndex - 1;
        while (...) {
            int mid = (low + high) / 2;
            int midVal = list[mid];
            ...
        }
Once we've found the middle value, we compare it against the key we are looking for. There are three possibilities:

So we can expand our code:

        int low = fromIndex;
        int high = toIndex - 1;
        while (...) {
            int mid = (low + high) / 2;
            int midVal = list[mid];
            if (midVal == key) {
                // we found it
            } else if (midVal < key) {
                // throw away first half of range
            } else {  // midVal > key
                // throw away second half of range
            }
        }
Now we just need to fill in the commented lines of code. If we have found the key, then we can simply return the index where we found it. To throw away the first half of the range, we can reset the low value to be the midpoint. To throw away the second half of the range, we can rest the high value to be the midpoint. This leads to the following code:

        int low = fromIndex;
        int high = toIndex - 1;
        while (...) {
            int mid = (low + high) / 2;
            int midVal = list[mid];
            if (midVal == key) {
                return mid;
            } else if (midVal < key) {
                low = mid;
            } else {  // midVal > key
                high = mid;
            }
        }
This approach doesn't quite work. The problem is that eventually the values of low and high will become equal to each other, in which case the value of mid will also be that value. So each time through this loop we'd be resetting low or high to the value they already have, which will lead to an infinite loop. We can fix that by noticing that if the value isn't in the middle, then we can throw that away as a possible index as well. So we just have to do slightly better in resetting low and high. Instead of doing this:

            } else if (midVal < key) {
                low = mid;
            } else {  // midVal > key
                high = mid;
We instead say:

            } else if (midVal < key) {
                low = mid + 1;
            } else {  // midVal > key
                high = mid - 1;
This almost completes the code. We still need to fill in a loop test and we have to think about what happens when the search fails. If the value is not found, then eventually low and high will cross (low will become larger than high). This becomes the basis for our loop test. If we search the entire range and don't find it, then we can return -1 as a way to indicate that it was not found. This leads to the following complete method:

        public static int binarySearch(int[] list, int fromIndex, int toIndex,
                                       int key) {
            int low = fromIndex;
            int high = toIndex - 1;
            while (low <= high) {
                int mid = (low + high) / 2;
                int midVal = list[mid];
                if (midVal == key) {
                    return mid;
                } else if (midVal < key) {
                    low = mid + 1;
                } else {  // midVal > key
                    high = mid - 1;
                }
            }
            return -1;
        }
We will discuss this further in section. The section version of the code is slightly different. In particular, it doesn't return -1 when the value is not found. Instead, it returns a value that indicates where the value should be inserted so as to preserve sorted order.


Stuart Reges
Last modified: Wed Mar 31 20:14:48 PDT 2010