I first gave a recap of what we've seen. We've been looking at an array-based class called ArrayIntList that has a number of operations for manipulating a list of integers. We discussed a variation of ArrayIntList called LinkedIntList that uses a linked list instead of an array to store the data. We saw that we can capture the "int list" abstraction by defining an IntList interface that both classes implement. So we end up with a generic IntList interface and we have two specific implementations: ArrayIntList and LinkedIntList.
I began by showing a new version of the IntList interface with some extra operations:
public interface IntList {
public int size();
public int get(int index);
public int indexOf(int value);
public boolean isEmpty();
public boolean contains(int value);
public void add(int value);
public void add(int index, int value);
public void addAll(IntList other);
public void remove(int index);
public void removeAll(IntList other);
public void set(int index, int value);
public void clear();
}
We expect that each of our implementations will provide all of this
functionality. As we go to implement these different operations, we'll find
that some of them are quite different. For example, when we write the "get"
method to return a value at a particular index, for the array we'll be able to
just ask for elementData[index], but for the linked list, we'll have to start
at the beginning of the list and keep doing some kind of "current =
current.next" operation to position ourselves to the right spot in the list.
So the implementations of "get" will be very different in the two classes.But what about a method like addAll? It is supposed to add all of the values from one list to another list. We'll probably build it on top of low-level operations like add. So perhaps we'll write the same code for each class.
How do we eliminate redundancy? Should we change the interface to an abstract class instead? Someone said we want both and I said that's a good idea. Because IntList is an interface, anyone can implement their own version of IntList in any way they want. If we were to change it to an abstract class, then we'd be forcing people to extend our class. You only get one inheritance relationship, so it would be very annoying for someone to tell you that it has to be used to extend a particular abstract class.
Instead, we have both. We keep IntList as an interface, but we also introduce an abstract class that we can use to factor out common code between our two implementations:
AbstractIntList----(implements)----> IntList
/ \
/ \
ArrayIntList LinkedIntList
This is a very flexible approach. The abstract class allows us to eliminate
any redundant code for our two implementations. Anyone who wants to can extend
our abstract class as well to take advantage of that common code. But they can
also do something completely different with no connection to our abstract class
as long as they implement the IntList interface. This pattern is so useful
that you'll find it used throughout the collections framework. For example,
there is a Map interface that has two implementations called TreeMap and
HashMap and each of the implementations extend a class called AbstractMap.
There is a similar structure for sets and lists.Then I turned to the question of how we would implement an operation like addAll to include in the AbstractIntList class. How do we do that? The idea is to get values from the second list to add them to "this" list. How do we get those values? Someone said we'd call get. We could do so, writing code along these lines:
for (int i = 0; i < other.size(); i++)
add(other.get(i));
This will work, but there is a problem with it. The idea is that we're writing
one version of the code that works for both implementations. This will work
fine for the array-based implementation, but it will end up being very slow for
the linked list implementation.Think about how get will be implemented for the linked list. We'll start a variable current at the front of the list and we'll move along until we get to the right spot. This is fairly quick when you want to get to something towards the front of the list. But what happens when you have a really long list? You might find yourself asking the list for the element at index 1000. That takes a lot of work (moving current over 1000 times). Then you'd ask it for the value at index 1001, which requires again a lot of work (moving current over 1001 times).
In fact, writing the code this way will turn addAll into an O(n2) operation for the linked list operation. We obviously don't want it to be that slow if we can avoid it. So how do we access the values more efficiently? Someone mentioned iterators. But what kind of iterator? I showed people Java's generic iterator interface:
public interface Iterator<E> {
public boolean hasNext();
public E next();
public void remove();
}
Someone said that we want an Iterator<Integer>. But that means we would need
to add something to the IntList interface that indicates that we have a method
that we can call to produce an iterator. The convention in Java is to call the
method "iterator":
public interface IntList {
...
public Iterator<Integer> iterator();
}
If we assume this is part of the interface, then we can write the following
code:
public void addAll(IntList other) {
Iterator<Integer> i = other.iterator();
while (i.hasNext())
add(i.next());
}
Then I reminded people about the foreach loop. Sun added this with Java 5.
It has a simpler syntax than the while loop above. There is an interface in
Java that is known as the Iterable interface. Saying that you implement the
Iterable interface means that you can produce an iterator:
public interface Iterable<E> {
public Iterator<E> iterator();
}
So we modified the header for the IntList interface to specify that it also
implements the Iterable interface (remember that you use the "extends" keyword
when one interface is being related to another interface):
public interface IntList extends Iterable<E> {
...
}
With this change, we were ablew to use a foreach loop for a method like
addAll:
public void addAll(IntList other) {
for (int i: other)
add(i);
}
This version works exactly the same as the previous version, because the
foreach loop is implemented using iterators.We then spent a few minutes looking at the implementation of the removeAll method. This method takes an IntList as a parameter and removes all values from this list that appear in the other list. Here is the code:
public void removeAll(IntList other) {
Iterator<Integer> i = iterator();
while (i.hasNext())
if (other.contains(i.next()))
i.remove();
}
This method works by calling a method the iterator class called remove. The
idea is that while you are iterating over a list, you might want to remove the
last value you saw. It's like saying, "I didn't like that last value...get rid
of it."The iterator's remove method is supposed to remove the value that was most recently returned by a call on next. As a result, it's not legal to call remove two times in a row or to call remove before next has been called. We then spent a few minutes looking at the new version of the ArrayIntList class. We saw that the iterator method constructs an object of type ArrayIterator. ArrayIterator is a private inner class. Because it is defined inside the ArrayIntList class, each instance of the class has access to the ArrayIntList object that constructs it. This allows it to call methods like size to decide what value hasNext should return.
I then spent some time showing an example of inheritance to write code for a GUI. I had previously mentioned that GUIs were the "killer app" that made object oriented programming more popular in the 1980s. 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("CSE143x 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.