CSE143 Notes for Friday, 4/2/10

We continued our discussion of the ArrayIntList class that we were developing in Wednesday's lecture. I then turned to 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;
Allowing a client to reset the size of the list is not a good idea. We want to make the fields of the class private so that they cannot be modified outside of the class. That 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.


Stuart Reges
Last modified: Sat Apr 3 11:55:24 PDT 2010