Lecture 8

  1. Announcements
  2. Defining classes
    1. The form of a class definition
    2. Method headers
    3. Constructors
    4. A Tshirt class
  3. Some simple exercises
  4. More on classes and instances


Announcements


Defining classes

So far in writing programs we have just been modifying methods in the WindowController class. That is, we have been defining new classes which extend WindowController and which substitute new behavior for methods begin, onMouseClick, onMousePress, etc.

You've also used objects from other classes -- such as Lines, Text, and FilledRects. In general, Java programs involve the interaction of objects from several classes. Today we'll talk about how you can write your own class.

Consider the laundry sorter lab you're working on this week. It's very primitive, given its intended application, because it can only deal with rectangles of different colors. It would be great if it worked with pictures of different clothing items. For now, we'll be satisfied to have it work with t-shirts of different colors.

Demo. T-shirt Sorter

How was this done? In order to help us focus on the t-shirt, rather than the sorting, let's consider a simpler program that simply displays a t-shirt and lets us drag it around the window.

Demo. Drag a T-shirt

In this program, we construct a t-shirt in the begin method. Before we can begin to drag the t-shirt with the mouse, we have to determine whether the mouse has been pressed on the t-shirt. This involves figuring out whether the mouse has been pressed in any of the pieces of the t-shirt. Finally, to do the actually dragging, we have to move every component of the t-shirt.

This certainly works. But wouldn't it be wonderful if we could simplify our program to look more like the original box dragging version. That is, wouldn't it be great if it could look like this:


import objectdraw.*;
import java.awt.*;

public class WhatADrag extends WindowController{

    // Starting position for t-shirt
    private static final int START_LEFT = 200,
                             START_TOP = 100,

    private Tshirt theShirt;     // shirt to be dragged

    private Location lastPoint;   // point where mouse was last seen

    // whether the shirt has been grabbed by the mouse
    private boolean shirtGrabbed = false;

    // make the t-shirt
    public void begin()
    {
        theShirt = new Tshirt(START_LEFT, START_TOP, canvas);
    }

    // Save starting point and whether point was in the shirt
    public void onMousePress(Location point)
    {
        lastPoint = point;
        shirtGrabbed = shirt.contains(point);
    }

    // if mouse is in shirt, then drag the shirt
    public void onMouseDrag(Location point)
    {
        if ( shirtGrabbed )
        {
            shirt.move( point.getX() - lastPoint.getX(),
                        point.getY() - lastPoint.getY()
                     );
            lastPoint = point;
        }
    }
}

The bad news is that Java doesn't provide for us the ability to construct and move t-shirts. Nor does our library. But the good news is that with a little bit of work, we can do this! All we need to do is learn how to define a new class of objects.


The form of a class definition

You've already been defining classes - the WindowController extension that has been the framework for your whole program. From this you are familiar with the basics of the form of a class definition:

    public class Name
    {
        constant and variable declarations

        methods
    }

Our earlier classes always extended WindowController, which provides us the opportunity to handle mouse actions. With the new classes we will be defining, that will generally not be relevant.

In the case of t-shirts, this might look something like:

    public class Tshirt
    {
        constant and variable declarations

        public void move(double xOffset, double yOffset  )
        {
            ...
        }

        public boolean contains( Location point)
        {
            ...
        }
    }

When we define a class:


Method headers

While we have been using the methods provided with the graphic objects of the ObjectDraw library, we have not bothered showing you the declarations of these methods. The following is the declaration of the move method:

    public void move( double xOffset, double yOffset)

The first keyword occuring in a method declaration is one of public or private. (The keyword protected is also allowed, but we won't be using it in this course.) These keywords determine the visibility of a method. If a method is declared to be public then it can be called from methods in other classes. For example, the onMouseDrag method of the Laundry class sent the move method to the FramedRect representing the laundry item. This would not have been possible if that first keyword had been private.

All of our instance variables are private, because they should only be used inside of the class where they are declared. Occasionally we will have constants that need to be visible outside of the class that contains them. In that case we will declare them as public static final ....

The next keyword in the declaration of move above is void. This (not very intuitively) indicates that it is a mutator method. That is, it simply performs an action rather than returning a value which can be used in an expression.

The parameters of move are of type double. These give the distance that the object should move in the x and y directions, respectively. Recall that the way we use the move method is as follows:

    anObject.move(10, 20);

Another example of a method is

    public boolean contains( Location point )

This differs from the move method by having a return type of boolean. That is, the results of sending this method to an object will be a value of either true or false. As a result, it is used in a context expecting a boolean result, such as:

    if (anObject.contains(lastPoint)) ...

In method headers/signatures, parameters serve as declarations of variables. Thus the occurrence of "Location point" between parentheses, serves to declare the variable point as a parameter of type Location. Parameters are used to pass along information to a method. Thus the body of method contains can use the variable point to represent the current location of the mouse. If we were to write the method move, the parameters xOffset and yOffset represent the amount we wish to move the object.

Of course we have already been using parameters when overriding mouse event methods such as onMouseClick when extending WindowController. The parameter point, which was declared to have type Location in the method header, was used as the current location of the mouse in writing the bodies of these methods.

We have also been declaring and using instance variables for some time - they are simply the variables declared at the beginning of classes. Thus we need only discuss the form of constructors to finish.


Constructors

Constructors are used to perform the actions which must be undertaken when the object is created. As a result, they often perform the same kind of actions as the begin method in the classes extending WindowController. In particular, they typically provide initial values of instance variables and create graphic objects needed in a class.

The form of the constructor will be very similar to that of methods:

    public ClassName( params here ... )

The name of the constructor will always be the same as the name of the class being defined. Thus if we were defining a class named Tshirt, the constructor would have the same name. Constructors are generally public, and may have as many parameters as are necessary to provide them with the information necessary to initialize instance variables. However, constructors differ from methods by omitting a return type before the constructor's name.

As an example, the constructor for class Tshirt might have parameters corresponding to the starting location of the upper left hand corner of the T-shirt, as well as information on what canvas the T-shirt should be drawn. Thus its declaration might look like:

    public Tshirt(double x, double y, DrawingCanvas aCanvas)

Again, notice there is no type between the keyword public and the name of the class, Tshirt. If this were a method declaration we would need a void to occur between these two if it were a mutator method, and a type name if it was an accessor method that returned a value of that type.

The type of the third parameter of the Tshirt constructor is new. DrawingCanvas is the type of the variable canvas that we have been using when creating graphic objects. It is the type of a surface that can be used for drawing graphic objects.


A Tshirt class

Let's put all of these pieces together and look at the code for the Tshirt class. You'll notice that this t-shirt looks like the one in the laundry demo, rather than like the simple one in our t-shirt dragging example.

import objectdraw.*;
import java.awt.*;

// A class that defines a graphical type that looks a bit
// like a t-shirt
public class Tshirt {

        // overall size of shirt
    private static final double SIZE = 120;
    
        // size of sleeves
    private static final double SLEEVE_WIDTH = SIZE;        
    private static final double SLEEVE_HEIGHT = 0.2*SIZE;

        // size of body
    private static final double BODY_WIDTH = 0.6*SIZE;
    private static final double BODY_HEIGHT = (0.65)*SIZE;
    
        // horizontal inset of body from sleeve
    private static final double BODY_INSET = 0.2*SIZE;
    
        // size of neck
    private static final double NECK_WIDTH = 0.3*SIZE;
    private static final double NECK_HEIGHT = 0.06*SIZE;
    
        // horizontal inset of neck from sleeve
    private static final double NECK_INSET = 0.35*SIZE;


    private FramedRect sleeveTrim, bodyTrim;    // rectangles that form a 
    private FramedOval neckTrim;                // border around the shirt

    private FilledRect body, sleeves;           // rectangles that form the 
    private FilledOval neck;                    // interior color of the shirt


    // create a new T-shirt with its upper left corner at (x,y)
    public Tshirt( double x, double y, DrawingCanvas canvas )
    {
            // create boundary rectangles
        sleeveTrim = new FramedRect( x, y + NECK_HEIGHT/2, SLEEVE_WIDTH, 
                                         SLEEVE_HEIGHT, canvas);
        bodyTrim = new FramedRect( x + BODY_INSET, y + NECK_HEIGHT/2, 
                                         BODY_WIDTH, BODY_HEIGHT, canvas);
        
            // create interior rectangles
        sleeves = new FilledRect( x+1, y+NECK_HEIGHT/2+1, SLEEVE_WIDTH-1, 
                                      SLEEVE_HEIGHT-1, canvas);
        body = new FilledRect( x+BODY_INSET+1, y+NECK_HEIGHT/2+1, 
                                   BODY_WIDTH-1, BODY_HEIGHT-1, canvas);

            // give it a neck hole
        neck = new FilledOval( x + NECK_INSET, y, NECK_WIDTH, 
                                   NECK_HEIGHT, canvas);
        neckTrim = new FramedOval( x + NECK_INSET, y, NECK_WIDTH, 
                                       NECK_HEIGHT, canvas);
        
            // set the interior to white
        body.setColor(Color.white);
        neck.setColor(Color.white);
        sleeves.setColor(Color.white);
    }

    // move the t-shirt by specified offsets.
    public void move( double xOffset, double yOffset )
    {
        body.move(xOffset,yOffset);
        neck.move(xOffset,yOffset);
        sleeves.move(xOffset,yOffset);
        bodyTrim.move(xOffset,yOffset);
        sleeveTrim.move(xOffset,yOffset);
        neckTrim.move(xOffset,yOffset);
    }

    // move the t-shirt to a new upper-left coordinate position
    public void moveTo( double x, double y)
    {
        move( x - sleeves.getX(), y - neck.getY());
    }

    // returns true if the t-shirt contains the point; false otherwise
    public boolean contains( Location pt)
    {
        return body.contains(pt) || sleeves.contains(pt) || 
               neck.contains(pt);
    }
}

The constructor takes the parameters x, y, and canvas, and creates the rectangles and ovals which form the T-shirt. The methods move, moveTo, and contains have exactly the same declarations as the methods of the same name of our other graphic objects.

The method move is straightforward. It simply moves all 6 pieces of the T-shirt the same amount. The method moveTo uses a clever trick. Rather than computing where to move each of the six pieces (notice that all 6 of them are created at different positions in the constructor), the method calculates how far (in each of the x and y directions) the new position is from the left and top of the entire T-shirt.

Notice the left edge of the T-shirt is that of the sleeves, while the top is the same as that of the neck. Hence the distance to move the T-shirt in the x direction is x - sleeves.getX(), while the distance to move the T-shirt in the y-direction is given by y - neck.getY(). Now the move method is invoked with those computed values as the distance to move in the x and y directions.

In summary, we took advantage of the fact that we had already written the move method, and used it to perform the actions necessary for moveTo. We had to compute how far to move it from the absolute coordinates given by the parameters of moveTo, but this was significantly easier than calculating where to move each individual piece.

There is one strange thing about the body of the moveTo method:

    move( x - sleeves.getX(), y - neck.getY());

The move message is not sent to an object! We are used to writing body.move(...), neck.move(...), etc., but not just writing move by itself without an object that it is being sent to.

When we write a method name by itself inside the body of another method, it means execute that method in the same class. Technically, Java considers the line of code above as an abbreviation for:

    this.move( x - sleeves.getX(), y - neck.getY());

The new version looks a bit more familiar, in that at least now the move message is being sent to an object, this. But what does this stand for? It stands for the object executing the current method.

Let's think about this more concretely. Suppose we have created a Tshirt called item. What happens if we evaluate

    item.moveTo(100, 200);

When this is evaluated, the formal parameters x and y in the declaration of method moveTo(double x, double y in Tshirt get assigned the values 100 and 200, respectively.

Now we execute the body of the method, this.move(...). Because the moveTo method was originally sent to item (the Tshirt), the keyword this stands for item. Thus the move message is sent to item with parameters that are the values obtained by computing x - sleeves.getX() and y - neck.getY().


Some simple exercises

Now it's time for a few simple exercises. Make sure you can do all of these:

  1. Write a setColor method for class Tshirt that takes a color as a parameter. The declaration of setColor for our graphic objects is:
  2.         public void setColor(Color newColor)

    Execution of the method should result in all of the pieces of the T-shirt being of the same newColor.

  3. Our Tshirt dragging program implemented onMouseEnter by moving the Tshirt back to its original starting position. Add a new method reset() to the Tshirt class which moves the Tshirt to the point at which it was originally created. In order to do this, you will need to add new instance variables which remember the starting position passed in to the constructor. When this is done, change the original Tshirt dragging program so that the body of onMouseEnter simply reset's the Tshirt.


More on classes and instances

If C is the name of a class, then when we execute a constructor for the class (by evaluating new C(...)), we create a new instance of the class. We can think of a class as a template for the construction of objects. Every time we call the constructor for the class, we get another new object.

For instance, in the following demo program, we start off with more than one Tshirt. This time we drag whichever Tshirt we pressed the mouse on.

Demo. Two T-shirt dragging

We determine which shirt to drag similarly to the way we determined which laundry bin was correct. That is, when we press the mouse button, we determine which Tshirt was pressed and remember that Tshirt with the variable selectedShirt.

Notice that each of the two objects (Tshirts) has its own collection of instance variables (one collection for each instance of class Tshirt). In particular, each has its own variables named sleeveTrim, bodyTrim, neckTrim, body, sleeves, and neck. One can tell from the picture that the variable body for redShirt holds a different FilledRect than the corresponding variable for blueShirt. Thus when we send the move message to redShirt it sends the move method to its own instance variables, but it has no impact on the instance variables of blueShirt.