CSE143 Notes for Wednesday, 5/5/21

I began by discussing the idea of binary search. This is an important algorithm to understand. 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.

The number of steps involved will depend on how many times you have to cut the set of possible answers in half. If you start with n answers, eventually you will get down to just one. The number of steps, then, can be computed as:

        n / 2 / 2 / 2 ... / 2 = 1
        n / 2^? = 1
        n = 2^?
        ? = log2(n)
I contrasted this with the way that we wrote the indexOf method for both the ArrayIntList and the LinkedIntList. In both cases we iterated through the list from beginning to end until we either found the value or ran out of values. This is an approach known as linear search.

I said that clearly the binary search is faster than the linear search, but how much faster? The linear algorithm exams all n values potentially. If a value is not found, it has to examine all of them. Even if it is found, it will tend to look at half of them on average if the value appears once in the list. So we are comparing an algorithm that takes n steps versus and algorithm that takes log2(n) steps. That is a huge difference. I mentioned that an appoximation I often use to reason about this is that 2^10 is about equal to 10^3 (the actual values are 1024 versus 1000).

Using this approximation, we can say that for one thousand values to search, we are talking about an algorithm that takes 10 steps versus an algorithm that takes one thousand steps. For a million values, it's 20 steps versus a million. For a billion values, it's 30 steps versus a billion. For a trillion it is 40 steps versus a trillion. And so on.

I then mentioned that I wanted to write the code for binary search. The Arrays class has several versions of the binary search method some of which take just an array and some of which take an array and a range of indexes to examine. I said that we would write the simple one that takes the entire array as a parameter:

        public static int binarySearch(int[] list, int value) {
            ...
        }
To write the binary search code it is important to recognize that we keep track of a current range of values to examine using two index variables. Initially these would be set to include all values in the array:

        int low = 0;
        int high = list.length - 1;
We want these values to get closer to each other. Eventually they will become equal to each other. You might think you would be done then, but actually that would mean that there is still one value left to consider. We want to loop until the two values cross, which would mean that there are no values left to consider. So our loop will look like this:

        while (low <= high) {
            ...
        }
Inside the loop we want to find the midpoint of low and high. If the value we are looking for is stored there, then we return that index. Otherwise we throw away half of the possibilities by resetting one of low or high to be the midpoint. Initially we wrote it this way:

            if (list[mid] == value) {
                return mid;
            } else if (list[mid] < value) {
                low = mid;
            } else {
                high = mid;
            }
Unfortunately this leads to an infinite loop. Eventually you reach a point where low and high are equal, which means that mid will also equal that value. So resetting low or high to mid changes nothing. That leads to the infinite loop. We can fix this by noticing that we have examined list[mid], so we can do better than resetting to mid itself. We can set to a value just before or just after mid:

            if (list[mid] == value) {
                return mid;
            } else if (list[mid] < value) {
                low = mid + 1;
            } else {
                high = mid - 1;
            }
We're almost done writing the code. We need to include a statement after the loop to return a value indicating that it didn't find the value. A simple convention is to return -1, but you can do something more useful. Having completed the binary search, you know where the given value should be inserted in the list so as to preserve sorted order. This would be useful information to provide to a client. But you have to be careful to not confuse it with an actual location. The convention is to return the negation of the index where a value should be inserted. But that causes a problem when the insertion point is 0, so the convention is to return one less than that.

Putting these pieces together, we ended up with this complete method:

    // pre : list is sorted
    // post: returns the index of where value appears in the given array;
    //       returns -(insertion point) - 1 if not found
    public static int binarySearch(int[] list, int value) {
        int low = 0;
        int high = list.length - 1;
        while (low <= high) {
            int mid = (low + high) / 2;
            if (list[mid] == value) {
                return mid;
            } else if (list[mid] < value) {
                low = mid + 1;
            } else {
                high = mid - 1;
            }
        }
        return -low - 1;
    }
Then I discussed inheritance and what it can be used for. I started with a simple example. We've seen how to use an ArrayList which has operations like add, get and size, as in:

        List<String> list = new ArrayList<>();
        list.add("four");
        list.add("score");
        list.add("and");
        list.add("seven");
        list.add("years");
        list.add("ago");
        System.out.println("list = " + list);
which produces the following output:

        list = [four, score, and, seven, years, ago]
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 StutteredList that inherits from ArrayList:

        public class StutteredList<E> extends ArrayList<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 StutteredList<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:

        List<String> list = new StutteredList<>();
        list.add("four");
        list.add("score");
        list.add("and");
        list.add("seven");
        list.add("years");
        list.add("ago");
        System.out.println("list = " + list);
we get the following output:

        list = [four, four, score, score, and, and, seven, seven, years, years, ago, ago]
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 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(600, 600);
                f.setTitle("CSE143 is fun");
                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("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 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.

We next added a private counter to keep track of how many times paint is called and we added some extra drawing commands to paint:

        public class CustomFrame extends Frame {
            private int count;

            public void paint(Graphics g) {
                count++;
                System.out.println("paint count = " + count);
                g.drawString("hello world!", 50, 50);
                g.setColor(Color.YELLOW);
                g.fillRect(50, 100, 30, 30);
            }
        }
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.
        public boolean mouseDown(Event e, int x, int y) {
            Graphics g = getGraphics();
            g.setColor(Color.BLUE);
            g.fillOval(x - 5, y - 5, 10, 10);
            return true;
        }
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 in an attempt to have the program do an etch-a-sketch type operation if the user drags the mouse around. We had to modify mouseDown to remember the old values of x and y.

        public boolean mouseDown(Event e, int x, int y) {
            Graphics g = getGraphics();
            g.setColor(Color.BLUE);
            g.fillOval(x - 5, y - 5, 10, 10);
            oldX = x;
            oldY = y;
            return true;
        }
        
        public boolean mouseDrag(Event e, int x, int y) {
            Graphics g = getGraphics();
            g.setColor(Color.RED);
            g.drawLine(oldX, oldY, x, y);
            return true;
        }
}
This didn't quite work. It drew a series of red lines all originating from the point where we first clicked the mouse. That's because we didn't update the fields oldX and oldY in the mouseDrag method.
        public boolean mouseDrag(Event e, int x, int y) {
            Graphics g = getGraphics();
            g.setColor(Color.RED);
            g.drawLine(oldX, oldY, x, y);
            oldX = x;
            oldY = y;
            return true;
        }
Once we made this change, it had the Etch-a-Sketch like behavior we were looking for, drawing many tiny lines that looked like a curve following our mouse movements.

I 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: Fri Jun 4 09:36:38 PDT 2021