CSE143 Notes for Friday, 5/23/08

I began by discussing the next programming assignment. The idea is to construct a binary tree that has information about a number of different kinds of things. We use yes/no questions to distinguish between them.

Initially it constructs a tree with just one leaf node containing "computer":

                +---+      +------------+
    overallRoot | +-+-->   | "computer" |
                +---+      +------------+
In this particular program, leaf nodes contain the names of objects and branch nodes contain questions. Whenever we get to a leaf node, we have no choice left but to guess that particular thing. So if we use the tree above, we'd always start by asking whether the object happens to be computer. If the user says yes, then we've correctly guessed the object and we give the message:

        Great, I got it right!
When we exit the program, the current tree is written to a file called question.txt. We saw that with just this simple tree composed of one leaf node, the file looks like this:

        A:
        computer
Then we explored what happens when the user thinks of something other than a computer as their object. In that case, we expand our tree to incorporate the new kind of object as well. I asked the class for suggestions of what to think of and someone said "puppy". To incorporate this into our tree, we need to replace the leaf node with a branch node that has a question and we want that node to have two leaves: our old object ("computer") and this new object. We start by asking the user what their object is. I said "puppy." Then we ask the user for a question that distinguishes between their object and our object. We said, "Is it alive?" The plan is to replace our old leaf node with a branch node containing this question and with the old and new objects as leaves. But we don't know which one to put to the left and which one to put to the right. To figure that out, we have to ask the user what the answer is for their object. We said that the answer for "puppy" is yes. I mention in the assignment writeup that we'll follow the convention that yes answers go to the left and no answers go to the right. So we replace the old root with this new tree:

                +---+    +----------------+
    overallRoot | +-+--> | "Is it alive?" |
                +---+    +----------------+
                               /   \
                              /     \
                   +---------+      +------------+
                   | "puppy" |      | "computer" |
                   +---------+      +------------+
When we exited the program, it wrote this information to question.txt:

        Q:
        Is it alive?
        A:
        puppy
        A:
        computer
The information is stored using a preorder traversal of the tree. When I ran the program again, I told it to read back in this file. I asked people to think of another object and someone said "elephant". So the program began by asking if our object is alive. We said yes. Then it reached the leaf node with "puppy" in it. Whenever the program reaches a leaf node, it has no choice but to make that guess. So it asked us whether our object is puppy and we said no. So it asked for a question to distinguish the two. Someone said, "Is it gray?". Then it asked what the answer is for "elephant" and we said yes. So now the tree becomes:

                +---+    +-----------------+
    overallRoot | +-+--> | "Is it alive? " |
                +---+    +--------------+--+
                               /   \
                              /     \
             +---------------+      +------------+
             | "Is it gray?" |      | "computer" |
             +---------------+      +------------+
                   /   \
                  /     \
     +------------+     +---------+
     | "elephant" |     | "puppy" |
     +------------+     +---------+
This process continues as long as the user wants to keep guessing. When the program finishes executing, you write out the contents to question.txt using a preorder traversal. That way, if the user wants to, they can start the next time with this as the initial tree. That would allow you to grow this tree bigger and bigger each time the game is played.

We saw that after adding this second object, the program wrote the following to question.txt:

        Q:
        Is it alive?
        Q:
        Is it gray?
        A:
        elephant
        A:
        puppy
        A:
        computer
I also pointed out that the zip file for the assignment includes a file called bigquestion.txt that has almost 10 thousand entries for animals. You have to rename the file to question.txt, but then your program should be able to read it in and play the game.

Then I started a new topic. I discussed inheritance and what it can be used for. Before the midterm we talked about the mechanics of inheritance, but we haven't yet talked about where inheritance is helpful.

I started with a simple example. We've seen how to use an ArrayList which has operations like add, get and size, as in:

        ArrayList<String> list = new ArrayList<String>();
        list.add("hello");
        list.add("there");
        System.out.println(list);
which produces the following output:

        [hello, there]
I posed the following question. Suppose that you want a list that behaves slightly differently? In particular, you'd like it to be the case that every time that add is called, it actually adds two of the value to the list. So it should have a kind of stuttering effect. In all other respects, it should behave like a normal list.

How would you make this change? The ArrayList class is part of the Java class libraries, so it would not be easy to change it. But that isn't a problem when you work in an object oriented language. There is a mantra that you'll hear in the object-oriented programming community that we want "additive--not invasive--change." Programmers are very used to the idea of doing surgery on existing code. We call that "invasive change" because it involves cutting open an existing class and rearranging it. This is like doing surgery on a person. Sometimes you have no choice but to perform surgery, but it can be very traumatic to do so. As a result, we want to avoid it if we can.

Inheritance provides a great mechanism for avoiding surgery. We can construct a new class called StutterList that inherits from ArrayList:

        public class StutterList<E> extends StutterList<E> {
            ...
        }
Just with this simple inheritance declaration, we get a class that behaves just like an ArrayList. Now we can say what is different about our version. We want this version of the list to add everything twice. We can easily define that in terms of the existing "add" in the super class:

        public class StutterList<E> extends ArrayList<E> {
            public boolean add(E value) {
                super.add(value);
                super.add(value);
                return true;
            }
        }
Note: the add method in ArrayList returns a boolean value to indicate whether the add was successful, so we have to replicate that behavior in our version. This is all you have to do to make a variation that adds twice. Remember that when you inherit, you have the option to override a method. In this case, we are overriding the add method to give it a different behavior. But you still have access to the method you are overriding by using the "super" keyword (in this case, calling super.add).

Now we have a class that we can use in place of an ArrayList that will have the stuttering behavior we wanted. For example, if we say:

        StutterList<String> list = new ArrayList<String>();
        list.add("hello");
        list.add("there");
        System.out.println(list);
we get the following output:

        [hello, hello, there, there]
I mentioned that this is such an important application of inheritance that I'll be asking a final exam question like this. The idea is that inheritance is very good at capturing variations. Using inheritance we can make a minor change to an existing class without changing the original class (additive--not invasive--change).

As a second example, I said to suppose that you wanted to have a Point object that remembers how many times it has been translated. How can you do that?

The answer will involve inheritance again. But this time it is not as simple as defining a new method. We'll also need a new data field. To remember the number of times it has been translated, the point will need an integer counter. So it will look like this:

        import java.awt.*;
        
        public class MyPoint extends Point {
            private int count;
        
            ...
        }
I don't have a good short name for this class. I don't want to call it PointThatRemembersHowManyTimesItHasBeenTranslated. So instead I just called it "MyPoint." We'll want to override the translate method. It can call the original translate method because we'll still want to do that original behavior, but we can also increment our counter:

        public void translate(int dx, int dy) {
            count++;
            super.translate(dx, dy);
        }
So now we have a data field to remember this and we've modified the translate method to increment the counter each time it is called. The problem is that this is a private data field that we can't see from the outside. So if we want to allow someone to actually see the value of the counter, we'd have to add a "getter" method:

        public int getTranslateCount() {
            return count;
        }
This provides a pretty good version of the class, although there are a few details that we still have to work out. Here's what we have so far:

        import java.awt.*;
        
        public class MyPoint extends Point {
            private int count;
        
            public void translate(int dx, int dy) {
                count++;
                super.translate(dx, dy);
            }
        
            public int getTranslateCount() {
                return count;
            }
        }
Someone suggested that we need to think about the initialization of the counter. I said that we certainly need to think about it, but in this case, we're okay. Remember that Java initializes all data fields to the 0-equivalent. So the counter will be initialized to 0. But the details I'm thinking about do have to do with the constructing of a Point.

So I mentioned to people a detail of inheritance that we haven't yet discussed. What do you get when you inherit from another class? You inherit the state and behavior of the other class. But not entirely. You inherit most methods from the class that you extend, but not all of them. Someone said "constructors" and I said, "That's right." You don't inherit constructors from a class that you extend.

I decided to use this as an opportunity to review the details of constructors and to discuss how inheritance factors into this. So I asked people what Java does when you don't define any constructors. The answer is that Java defines one for you. We refer to it as the "default" or "paren paren" or "zero argument" constructor. For our class called MyPoint, it would be as if we had included the following method in the class:

        public MyPoint() {
            // nothing to do
        }
But this would be the only constructor for the class. In particular, we wouldn't be able to say something like:

        Point p1 = new MyPoint(8, 17);
I then mentioned that every constructor makes a call on a superclass constructor, whether you do so explicitly or not. The syntax for calling a superclass constructor is very similar to the syntax for one constructor calling another, except for the fact that you use the keyword "super" instead of the keyword "this". For example, if you want to call the default constructor of the superclass, you say:

        super();
We have been writing classes for a long time without including such calls. The reason that works is that if you don't call a superclass constructor, then Java does it for you. And by default it calls the default constructor. So if you don't include a call on the superclass constructor, Java will insert the call above for you.

That means that our MyPoint constructor isn't quite as empty as we had thought. It really does the following:

        public MyPoint() {
            super();
        }
Remember that Java inserted this constructor for us because we didn't define one of our own. What would happen if the Point class did not have a default constructor? In that case, we would have gotten a weird error message:

        MyPoint.java:3: cannot find symbol
        symbol  : constructor Point()
That would seem like an odd error message because we never call Point() anywhere in our code. But it makes sense when you understand that Java is doing it for us. Without a constructor, Java will give us a default constructor. And since we didn't say how to call a superclass constructor, it makes the call super().

The Point class actually does have a default constructor, so we don't get this message. But it's important to remember that this will occur. Many classes do not have a default constructor, and then you must call a superclass constructor.

In our case, we want to have a constructor that takes two integers that specify x and y. Our class doesn't keep track of x and y, that's what the Point class does. So all we want to do is to pass the values up the chain to the Point class' constructor that takes two arguments. We can also initialize the counter just to be clear:

        public MyPoint(int x, int y) {
            super(x, y);
            count = 0;
        }
Now we can construct MyPoint objects with two ints. Unfortunately, now we can no longer construct the origin by asking for a "new MyPoint()". Java defines a default constructor if you don't define any of your own. But if you have even one constructor in your class, then Java assumes that you know what you're doing and that you would have defined a default constructor if you wanted one. So if we really want that for the MyPoint class, we have to specifically include it. In this case, we can call the two-argument constructor in the MyPoint class:

        public MyPoint() {
            this(0, 0);
        }
Putting it all together, here is the definition of MyPoint:

        import java.awt.*;
        
        public class MyPoint extends Point {
            private int count;
        
            public MyPoint() {
                this(0, 0);
            }
        
            public MyPoint(int x, int y) {
                super(x, y);
            }
        
            public void translate(int dx, int dy) {
                count++;
                super.translate(dx, dy);
            }
        
            public int getTranslateCount() {
                return count;
            }
        }
I reminded people that there would be a final exam question like this.

I then mentioned that the "killer app" for inheritance that convinced many people to use it was user interface. Once people started writing GUIs (Graphical User Interfaces), they found that they had to write a lot of detailed code for buttons, text boxes, windows, scroll bars and so on. Inheritance provided an easy mechanism to write the common code once and then to define lots of variations through inheritance.

I then spent some time showing how this works in Java. I warned people that I was showing "old" Java as it originally worked. Java has since evolved and the preferred techniques are more complex than what I showed. I pointed out that there is a Frame class in Java that creates a top-level user interface frame. Even without inheritance, we were able to create one and set various properties by writing code like this:

        import java.awt.*;

        public class DrawFrame {
            public static void main(String[] args) {
                Frame f = new Frame();
                f.setSize(400, 400);
                f.setTitle("Oooooh!");
                f.setBackground(Color.CYAN);
                f.setVisible(true);
            }
        }
Then we came upon the issue of how to make it draw something inside the frame. The metaphor in Java is painting, that you paint things on the screen, and the Frame class has a method called "paint". How do we change how it works? That's where inheritance comes in. We define our own CustomFrame class that extends the Frame class and overrides the paint method. At first we just put a simple println in this method:

        import java.awt.*;

        public class CustomFrame extends Frame {
            public void paint(Graphics g) {
                System.out.println("in paint");
            }
        }
Then we changed the main program so that it constructs a CustomFrame object instead of a Frame object:

        Frame f = new CustomFrame();
It didn't seem like this would work because I never called the paint method. But it actually did work. When I did things like minimizing the window and then restoring it, we saw the println happening. That's because there is a vast amount of code written for us that takes care of details like when to repaint a frame. That's the whole point of this exercise. We know that someone at Sun has written tons of code that we want to take advantage of. Through inheritance we can make tiny additive changes that define the specialized behavior we want.

Obviously we don't want the frame to just call println, so we wrote some code to draw something inside the frame:

        import java.awt.*;

        public class CustomFrame extends Frame {
           public void paint(Graphics g) {
                g.drawString("Hello world!", 50, 50);
                g.setColor(Color.YELLOW);
                g.fillRect(50, 100, 25, 25);
            }
        }
I then extended this example by overriding a method called mouseDown so that it would draw a blue circle whenever the user clicks the mouse. An interesting property of that code is that the blue circles go away if the frame is redrawn because we didn't make it part of our paint method. I joked that this could be thought of as a bug or a feature depending upon your point of view. We also overrode the method mouseDrag to have the program do an etch-a-sketch type operation if the user drags the mouse around. The complete code for these is in handout #29.

I also pointed out that it is unfortunate that we don't have more time to explore using inheritance with frameworks like Java's AWT and Swing libraries. In spring of 2007 I taught a course CSE190L that explored this in great detail. The course web page has lecture notes and assignments in case you want to pursue this on your own. I also highly recommend the textbook we used, Core Java.


Stuart Reges
Last modified: Wed May 28 07:13:40 PDT 2008