CS 10: Winter 2016

Lecture 9, January 22

Code discussed in lecture

Dragging out rectangles

The class DrawRects.java allows us to drag out and draw rectangles on the screen. Here, we use an expanded Rect class in Rect.java. Each Rect object knows its upper left corner, width, height, and color. It also knows how to draw itself.

The DrawRects class has the user drag out rectangles, and it adds each of them to the ArrayList named boxes. It does so using three methods that handle mouse events and mouse-motion events. The method mousePressed remembers the place where the mouse was pressed in pressedPoint, and it constructs a new currentRect with 0 width and height at the place where the mouse is pressed. It colors this new rectangle black.

The method mouseDragged updates currentRect so that it has pressedPoint and the current mouse position as two opposite corners. Note the use of Math.min to get the upper left corner and of Math.abs to compute the height and width of this rectangle. Because this method responds to mouse dragged events, the rectangle is updated (almost) continuously. The screen is repainted after each update.

The method mouseReleased completes the operation. It changes the color of the current rectangle to one of four that are saved in the array color. (Note the use of an initializer list when this array is defined.) It then adds the current rectangle to the set boxes. Finally it sets currentRect to null to indicate that the Rect object that currentRect had a reference to is now in the data structure boxes and should not be used any more for getting new rectangles from the user. Both this method and mouseDragged check to see that the currentRect is not null before doing anything to it.

The drawing is done on a Canvas inner class similar to the ones that we have seen before. Its paintComponent first draws a black rectangle around the canvas. (The -1 in the width and height keeps the pen from being off of the canvas on the right side and bottom). It then draws all the saved rectangles. Finally it draws the rectangle currently being dragged (if any).

There is also a contructor for Canvas, which is something new. This constructor just sets the preferred size of the Canvas component. It does this by calling setPreferredSize and handing it a Dimension object. This is what you call on a JPanel instead of setSize.

There is another method in Canvas: actionPerformed. The Canvas class implements ActionListener. The actionPerformed method will handle clicks on the Clear button. It just clears boxes and calls repaint.

Finally we can look at the init method of DrawRects. It adds this as both a mouseListener and a mouseMotionListener. It sets the applet size. It gets the content pane and says that it should use FlowLayout. It adds the canvas to the content pane.

It then creates a JButton with the label "Clear". It sets the button's background color and adds the canvas as an actionListener. Note that these calls are made on clearButton rather than this. Finally it adds the button to the content pane. Because there is not enough room next to the canvas, it is placed centered below the canvas.

Abstract classes and a hierarchy of graphical objects

In a previous lecture we used an interface to allow ourselves to define a variable declared to be a reference to a GeomShape. In fact, GeomShape was an interface, and the way we implemented the program, the variable was actually a reference to either a Circle or a Rectangle object. We can accomplish the same thing via inheritance and avoid duplicating code in the process.

Consider geometric shapes: circles, rectangles, segments, squares, diamonds, etc. What do they have in common? We might want all of them to have a color, to know how to draw themselves, to know how to move themselves, and to know how to compute their area. The class GeomShape.java defines a class with these properties. Note that all shapes will have a color, and so myColor is an instance variable that is initialized in a constructor. There are methods to set and get the color. The draw method doesn't know how to draw the object, but we will look later at how it can allow the shape to be drawn in the correct color without permanently changing the color of the Graphics object.

The methods areaOf and move depend on specific information about the shape. Therefore they are given as abstract methods. The header ends in a semicolon and the word abstract is included in the header. The entire class is also declared as abstract. No object of an abstract class can ever be created. Hence, no GeomShape object can be created. Abstract classes exist only to serve as superclasses for other classes.

If a class declares one or more abstract methods, then the class itself must be declared abstract. However, you can still declare a class to be abstract even if it declares no abstract methods. As before, declaring a class to be abstract means that no object of the class can be created (even if the class contains no abstract methods).

A second abstract class, PointRelativeShape.java, is a subclass of GeomShape. This class is for shapes that have some "center" or reference point, where the shape is defined relative to that reference point.

PointRelativeShape takes care of defining the instance variables myX and myY, providing methods to return these values, and providing the move method. It extends GeomShape. Even though it declares no methods as abstract, it fails to implement areaOf, and so it must be abstract as well.

Finally, Circle.java and Rectangle.java inherit from PointRelativeShape. Note the use of super in the constructors. The drawShape methods (we'll see these methods soon) call getX and getY to figure out where the object is. They do not have to supply a move method, because they inherit it.

We can also have classes that inherit directly from GeomShape. Segment.java does so, and it implements everything itself.

Here's a picture of our class hierarchy:

Now, let's look at the driver GeomShapeDriver.java, which demonstrates all of these classes, showing that everything works. Again, we see polymorphism in action. The variable shape is declared as a reference to GeomShape, but in reality it will refer to a Circle, Rectangle, or Segment object. The calls at the bottom of the loop body show dynamic binding in action. The first time through the loop, we call methods in the Circle class; the second time through, we call methods in the Rectangle class; and the third time through, we call methods in the Segment class.

How to leave the current color alone when drawing an object

So far, when we went to draw a shape, we set the color of the Graphics object and drew our shape. Therefore, the next time we draw something, it will be in the color of the last shape drawn. If we next call drawString, then the rendered text will be in that color. Suppose we wanted the text to remain whatever the original foreground color was?

The Graphics class has a getter method that returns the current color for drawing: getColor. Thus, we can solve the problem by making the draw methods in the subclasses save the current color by calling getColor, changing the Graphics object's color to the color of the object being drawn, drawing the object, and finally restoring the saved color. For the Circle class, we could write the draw method as follows:

  public void draw(Graphics page) {
    // Save the current color.
    Color savedColor = page.getColor();

    // Set the color.
    page.setColor(getColor());;
  
    // Draw the circle.
    page.drawOval(getX() - myRadius, getY() - myRadius, 2*myRadius, 2*myRadius);

    // Restore the color we had on the way in.
    page.setColor(savedColor);
  }

Unfortunately, that's a lot of work that we'd have to repeat for each instantiable subclass of GeomShape. There's a better way to accomplish this goal, with less code. We move all of the code except the actual drawing of the shape up to the GeomShape class in GeomShape.java in its draw method, and we add the following abstract method:

  protected abstract void drawShape(Graphics page);

Note that this is an example of the template pattern. The draw method does all of the color saving, changing, and restoring and leaves the detail of how to draw the shape to an abstract method, which each shape class fills in as appropriate.

Each instantiable class will have to define the drawShape method, and it will consist of the code that is specialized to that shape. For example, here's the drawShape method in the Rectangle class, within Rectangle.java:

  protected void drawShape(Graphics page) {
    page.drawRect(getX(), getY(), myWidth, myHeight);
  }

You can see also how we wrote the drawShape methods in Circle.java and Segment.java.

Now we can understand the draw method in GeomShape.java. It saves the current color, sets the current color to the shape's actual color, does the drawing specialized to the shape, and finally restores the original color:

  public void draw(Graphics page) {
    Color savedColor = page.getColor(); // save the current color
    page.setColor(myColor);             // set the color
    drawShape(page);                    // draw the shape in the shape's color
    page.setColor(savedColor);          // restore the color we had on the way in
  }

Once again, dynamic binding helps us. When the draw method calls drawShape, which class has its drawShape method called? As usual, it's the class of the object that draw was called on.

Why did I make drawShape protected? I intend drawShape to be called only by the draw method in GeomShape. By making it protected, it cannot be called by methods outside of GeomShape or its subclasses. (Well, except for the detail that any other class in the same package can call it, also!)

Because drawShape is protected in GeomShape, I had to make drawShape protected in the subclasses Circle, Rectangle, and Segment—even though I don't intend for there to be any subclasses of Circle, Rectangle, and Segment. That's because when we declare a method as abstract in a superclass, the method has to have at least the same degree of "visibility" in the subclasses. Why? When we say that a superclass allows a method to be called, that method has to be callable in all subclasses. So the method must be at least as visible in the subclasses as it is in the superclass. Indeed, I could have declared drawShape as public in Circle, Rectangle, and Segment, so that it would be even more visible than in GeomShape, but there's no point in doing so.

Access control

We have talked about public and private access. Those are the kinds of access that we will usually use. However, you should know that there are four access levels:

  1. public access,
  2. private access,
  3. protected access, and
  4. package access (which is the default when no other access is specified).

We have seen that public access means that any method of any class can access the variable or method. Private access is limited to the given class and its inner classes.

Protected access is new for us. With protected access, the variable or method should be accessible to only the class and any subclass that extends the class (or extends a class that extends the class, etc.). Protected access seems to be a good compromise between public and private, and it is certainly an improvement on public.

There is a problem with protected access, however. Because anyone can extend a class, there is no real control over who can access protected instance variables. Again, there is no abstraction barrier between the superclass and its subclasses. A better way to restrict access is to make all instance variables (except constants) private and then to make the methods that can access those instance variables protected. That should prevent classes other than subclasses from accessing the instance variables, while still keeping encapsulation.

The default access, package access, is what you get if you don't put anything before the instance variable or method. In other words, it's what you'd get for the instance variable x in the following class:

public class Bozo {
  int x;           // package access
  ...
}

I consider it a design flaw of the language that there is no package visibility level specifier, as there is for public, private, and protected. It is too easy to forget to declare an instance variable (or method) private. I would have preferred that leaving off the visibility level specifier would be a syntax error. That way the programmer would always have to make an explicit choice.

Package access makes the variable or method visible to everything in the same package. As we mentioned when we talked about import statements, package is a way of grouping methods together that are intended to work together as a group, such as java.util or java.awt. To put a file into a package, you start the file with a package statement:

package package_name;

where you replace package_name by the name of the package.

If there is not a package statement, then all classes in the file go into the default package.

You seldom want package access. Anyone can add a class to a package, so you have very little control of who has access to things that have package access. It sometimes makes sense to give package access for methods that you want as utility routines within a package, but it seldom makes sense to have package access for instance variables.

You might have noticed that when I described what protected means, I said it "should" only be available to the class and its descendents (subclasses or subclasses of those subclasses, etc.). That is what protected means in C++ and some other OO languages. Unfortunately, that is not what it means in Java. As we have described it, public is the least restrictive, private the most, and protected and package are both somewhere in between, but you can't say that one is more restrictive than the other. They control access in very different ways.

The designers of Java decided that not having a linear heirarchy of protection levels was a bad idea. Therefore they somewhat arbitrarily decided that protected access should include package access, so that package access is more restrictive than protected. So in Java protected really means visible in descendents and to classes in the same package. If you are using the default package, that means it is not much different than public! However, I will still declare things protected to indicate who should have access.

Adapter classes

When we created listeners, we had to implement an interface. For the MouseListener interface, there were five methods that had to be implemented, and we usually used only one or two. We had to create empty-bodied implementations for the others. Having to do so was a minor irritant.

This situation comes up often enough that Java has provided an alternate approach. For each Listener interface, there is a corresponding Adapter class that supplies empty-body implementations for each of the methods in the interface. We then can extend these classes, overriding just the methods that we want to actually do something. We no longer have to supply the empty-body implementations, because the Adapter class has already done that, and we inherit them.

For example, consider DragAMac3.java. It has two inner classes that implement the MouseListener and the MouseMotionListener interfaces. But instead of saying that we will implement these interfaces, we instead extend the MouseAdapter and MouseMotionAdapter classes. The empty-body implementations that we had to supply before are now gone.

Note that if we use the applet itself as the listener (using this as the parameter to the appropriate addListener method calls), we cannot use this approach. That is because in creating the class DragAMac3, we have already extended Applet, and Java does not allow multiple inheritance. In other words, Java does not allow a class to extend more than one other class. DragAMac3 has to extend Applet, and we can either implement the two interfaces directly or extend the Adapter classes in inner classes as we have done here. In fact, we had to use two different inner classes, since a single inner class cannot extend both Adapter classes.

We saw one other interface that we use to handle events: the ActionListener interface. Unlike MouseListener and MouseMotionListener, ActionListener has no corresponding adapter class. There's a pretty good reason for that. What do you think it is?

Inheritance vs. interfaces

The above point is worth repeating.

In Java, a subclass can extend just one superclass.

(Extending more than one superclass would be multiple inheritance.) For example, the following class declaration would not be legal:

class Bozo extends Clown, TVStar {
  ...
}

On the other hand, a class is allowed to implement any number of interfaces. Thus, the following class declaration would be legal:

class Bozo implements Clown, TVStar {
  ...
}

Why doesn't Java allow multiple inheritance? Because it can cause problems that are difficult to resolve. What if more than one superclass defines the same instance variable that is visible to the subclass (i.e., either public or protected)? What if more than one superclass defines a method with the same name and signature? What if you have a "diamond" shaped inheritance structure, where A is the superclass, B and C both inherit from A, and D inherits from both B and C. D should have access to all instance variables that B and C have. But each has a copy of A's instance variables! Should D have two copies of each?

C++ allows multiple inheritance, and it pays the price in its complexity. The designers of Java decided to keep things simple, and so Java does not provide for multiple inheritance. Interfaces provide a way to get most of the benefits of multiple inheritance without most of the problems. The downside of interfaces, of course, is that they don't allow you to define instance variables in them, nor do they allow you to provide method bodies that are inherited.