CSE143 Notes for Monday, 10/4/10

We began with the idea of encapsulation, which we discussed briefly on Wednesday. When you buy a radio or other appliance at Best Buy that you'll find that all of the electronics are inside of a plastic or metal case. We would say that the electronics are encapsulated inside this case. You can't see them or touch them from the outside. In fact, if you flip the device over, you're likely to find a metal plate with screws that can be removed, but it often comes with a warning along the lines of, "Do not remove. You will void your warranty if you remove this."

Why the warning? Someone said that they don't want you to damage the electronics and that is exactly right. So is there something analogous in the ArrayIntList class we have been writing? What might the client might do to damage the object? Someone said that they could set the size to a huge number or a negative number:

        list1.size = 10000;
        list2.size = -384;
We can prevent this kind of interference by changing the fields of the class to be private. Currently they are declared this way:

    int[] elementData;
    int size;
We added the word private to each:

    private int[] elementData;
    private int size;
Then when we tried to compile the client code that changed size, we got an error message indicating that the size field is private and cannot be changed. Private fields cannot be modified outside of the class. This allows us to guarantee that our object is never in a corrupt state.

I mentioned that Joshua Bloch, who was the chief architect of the Java Collections Classes, emphasizes this in his book Effective Java (I have a link to it under "useful links"). He says, "The single most important factor that distinguishes a well-designed module from a poorly designed one is the degree to which the module hides its internal data and other implementation details from other modules."

Encapsulation is a good thing, but it seems like we want to allow clients to check the size of a list. To do so, we introduce a size method that the client can call to ask for the current list size:

        public int size() {
            return size;
        }
We then rewrite these two lines of client code:

        System.out.println(list1.size);
        System.out.println(list2.size);
to call the size method instead:

        System.out.println(list1.size());
        System.out.println(list2.size());
I asked people whether there were other methods we probably want to supply that would allow a client to access the list. Someone mentioned that there should be a method to look at the individual values in the list. This is a method called "get" and it takes an index as a parameter, returning the integer at that index:

        public int get(int index) {
            return elementData[index];
        }
Someone asked what happens when a client calls the get method with an illegal value for index. So I spent a few minutes talking about the concept of preconditions and postconditions. This is a way of describing the contract that a method has with the client. Preconditions are assumptions the method makes. They are a way of describing any dependencies that the method has ("this has to be true for me to do my work"). Postconditions describe what the method accomplishes assuming the preconditions are met ("I'll do this as long as the preconditions are met.").

I have included pre/post comments on all of my ArrayIntList methods. I encourage people to use this style of commenting. It is not required, but if you use a different style, be sure that you have addressed the preconditions and postconditions of each method in the comments for the method.

As an example, I pointed out that methods like "get" that are passed an index assume that the index is legal. The method wouldn't know how to get something that is at a negative index or at an index that is beyond the size of the list. Whenever you find a method that has this kind of dependence, you should document it as a precondition of the method.

So we added the following comments to the get method:

        // pre : 0 <= index < size()
        // post: returns the value at the given index
        public int get(int index) {
            return elementData[index];
        }
Later we'll see a way to guarantee that preconditions are satisfied. For now, we'll settle for simply documenting them so that a client of the code will be properly warned.

Then we turned to the question of constructors. I said that every time you call new, Java is actually calling a special method known as a constructor. There is a special syntax for constructors. They must have the same name as the class and they have no return type. For example, to define a constructor that has no parameters, you'd say something like this:

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

            public ArrayIntList() {
                // constructor code here
                ...
            }

            ...
        }
It seems odd that we could construct objects of type ArrayIntList without defining a constructor for the class. Java has a special rule that if you define no constructors whatsoever, it will provide a constructor for you that has no parameters. So even though we didn't define a constructor, Java defined one for us.

To explore this, I defined a constructor and moved the initialization code into it:

        public class ArrayIntList {
            private int[] elementData;
            private int size;

            public ArrayIntList(){
                elementData = new int[100];
                size = 0;
            }

            ...
        }
I pointed out that in our grading, we are going to expect that code to initialize fields should appear in a constructor. This is considered better style in Java and we'd like you to do it that way.

The initialization of size is not really necessary. Java automatically initializes fields to the zero-equivalent for that type. All int fields are initialized to 0. All double fields are initialized to 0.0. All boolean fields are initialized to false. And fields that store references to objects like elementData are initialized to null (a special value that means "no object"). As a result, the size field will be initialized to 0 with or without the line of code in the constructor. There is disagreement among Java programmers about which style is better. Some people argue that it is best to be explicit about initialization. Others argue that you should know how Java works and should be thinking about auto-initialization.

I asked people whether they had any criticism of this code and someone pointed out that it's not very flexible to have the value 100 as the array size. What if you wanted an array 200 long or 500 long? So we modified the constructor to take an integer capacity:

        public ArrayIntList(int capacity) {
            elementData = new int[capacity];
            size = 0;
        }
Then we modified the client code to indicate a capacity:

        ArrayIntList list1 = new ArrayIntList(200);
        ArrayIntList list2 = new ArrayIntList(500);
This worked fine. But then I changed one of them so that it didn't list a capacity:

        ArrayIntList list1 = new ArrayIntList(200);
        ArrayIntList list2 = new ArrayIntList();
We got an error message. Java said that it could not find a zero-argument constructor. It turns out that by adding the constructor that takes a capacity, we lose the old constructor that takes no parameters. The rule is that if you don't define any constructors at all, Java will define a zero-argument constructor, but if you define even one constructor, then Java assumes you know what you're doing and doesn't define any for you. This means we have to add a new constructor for the zero-argument case:

        public ArrayIntList() {
            elementData = new int[100];
            size = 0;
        }
While you can define the constructor this way, it is better style to define this constructor in terms of the other constructor. Most Java classes have one "real" constructor that all the others call. It is most often the constructor that has more parameters than any of the others. You can have one constructor call another by including the keyword this and a set of parameter values inside parentheses, as in:

        public ArrayIntList() {
            this(100);
        }
The call on the other constructor must appear as the first statement. Java can tell that you are calling the other constructor because it sees an int inside of parentheses. I pointed out that if you accidentally wrote it this way:

        public ArrayIntList() {
            this();  // bad!!
        }
you'd have a constructor that calls itself infinitely.

We also discussed the idea that the number 100 is arbitrary, so we can introduce a class constant for it:

        public static final int DEFAULT_CAPACITY = 100;
We then rewrote our zero-argument constructor to be:

        public ArrayIntList() {
            this(DEFAULT_CAPACITY);
        }
I asked why it's okay to have a public constant but it's not okay to have public fields. Someone said that it's because it's a constant. The keyword "final" in the definition guarantees that nobody can alter the value of the constant, so there is no danger of someone damaging the constant. The same is not true of fields, which is why they should always be declared private.

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: Mon Oct 4 15:50:52 PDT 2010