Chapter 14. Defining classes

Our programs so far have used classes, like Turtle and GOval, which were written by other people. In writing larger programs, we often find that another class would be handy for our purposes, even though nobody has written such a class yet. Also, as we write larger programs, we will need some way to break a program into independent parts, so that the task of programming becomes more manageable. For both situations, the solution is to develop our own class.

Large Java programs are broken into many interrelated classes. So it is very important that we understand how to write our own classes and get them to interact. We study the fundamentals of this in this chapter.

14.1. A simple example

Just as we had a set of design questions to answer before defining a method, we also have several questions to answer before defining a class.

One class that would be handy for our programs would be the notion of a button — basically, a rectangular region with a word nested within it, which the user could click. We'll call it the GButton class. An example where this might be handy would be a program with a moving ball and two buttons to control whether the ball is moving, as illustrated in Figure 14.1.

Figure 14.1: Running StopGoBall.

Figure 14.2 contains the StopGoBall program, which illustrates such a program using a hypothetical class GButton.

Figure 14.2: The StopGoBall program.

  1  import acm.graphics.*;
  2  import acm.program.*;
  3  import java.awt.*;
  4  import java.awt.event.*;
  5  
  6  public class StopGoBall extends GraphicsProgram {
  7      private boolean going;
  8      private GButton stop;
  9      private GButton go;
 10  
 11      public void run() {
 12          going = true;
 13          stop = new GButton("Stop", 10, 10);
 14          go = new GButton("Go", 10 + stop.getWidth() + 10, 10);
 15  
 16          add(stop);
 17          add(go);
 18          addMouseListeners();
 19  
 20          double dx = 2;
 21          GOval ball = new GOval(75, 75, 50, 50);
 22          ball.setFilled(true);
 23          ball.setFillColor(Color.RED);
 24          add(ball);
 25  
 26          while(true) {
 27              pause(20);
 28              if(going) {
 29                  if(ball.getX() < 0 || ball.getX() + 50 > getWidth()) {
 30                      dx = -dx;
 31                  }
 32                  ball.move(dx, 0);
 33              }
 34          }
 35      }
 36  
 37      public void mouseClicked(MouseEvent e) {
 38          if(stop.contains(e.getX(), e.getY())) {
 39              going = false;
 40          } else if(go.contains(e.getX(), e.getY())) {
 41              going = true;
 42          }
 43      }
 44  }

Imagining the StopGoBall program allows us to envision what methods we will need from a GButton class.

GButton(String textdouble xdouble y)

(Constructor) Creates a button with its upper left corner at (xy) and its label being text.

boolean contains(double xdouble y)

Returns true if the point (xy) lies within this button.

double getWidth()

Returns the width of this button.

That addresses the question of what methods the class needs. But what class should it extend? In thinking about this, we notice that a GButton is a combination of two GObjects — a rectangle and a label. We could thus think of a GButton as just a special type of GCompound, and so it is natural to make GButton be a subclass of GCompound. This will have the useful side effect that we won't have to define contains and getWidth: GCompound defines these methods already, doing exactly what we want, and GButton will automatically inherit these methods if we extend it. Our implementation of GButton in Figure 14.3 takes advantage of this by defining the constructor only.

Figure 14.3: The GButton class.

  1  import acm.graphics.*;
  2  import java.awt.*;
  3  
  4  public class GButton extends GCompound {
  5      public GButton(String textdouble xdouble y) {
  6          GLabel label = new GLabel(text, 0, 0);
  7          label.setLocation(20, 10 + label.getAscent());
  8  
  9          GRect background = new GRect(0, 0, label.getWidth() + 40,
 10              label.getAscent() + 20);
 11          background.setFilled(true);
 12          background.setFillColor(Color.GRAY);
 13  
 14          add(background);   // add background first, so it is below label
 15          add(label);
 16          setLocation(xy); // reposition entire GCompound to (x,y)
 17      }
 18  }
Aside

Many professional Java programmers would argue that using GCompound as a superclass for GButton would be a flawed design: After all, we think of buttons as unit objects, not as compounds. Moreover, GButton will inherit several methods, such as add, that have no obvious relationship with buttons. Such programmers would agree that GButton should definitely be a subclass of GObject, since it is a graphical object, and perhaps of GRect, since it's basically a rectangle that happens to have a word on top of it. We haven't done it that way, though, because doing that would involve some more complex techniques that don't really pertain to our discussion.

The primary point here is that Java professionals have definite opinions about when properly to use subclasses. Most would agree that one thing you should definitely not do is to select a superclass based purely on whether you could inherit methods that happen to work as desired.

Because the two instance methods that we need for the StopGoBall program — contains and getWidth — are automatically inherited, our implementation has only to define the constructor. The definition of a constructor closely resembles the definition of an instance method; the difference is that you write the class name before the parameters, whereas with an instance method you would list the return type and method name.

public <className>(<parmType> <parmName>) {
    <bodyOfMethod>
}

The purpose of a constructor is to set up an object that is in the process of being created. In this case, a newly created GButton should be initialized to have a combination of a GLabel and a GRect, and it adds them both into the GCompound using the inherited add method. The ordering is a bit peculiar, because the program must first create the label so that the rectangle for the background can be sized to be just a bit larger than the label; but the label must be added afterwards so that it is drawn on top of the background.

A detail to remember

Beginners, accustomed to always having a return type just after public, often mistakenly insert the word void. For example, a beginner might write:

public void GButton(String textdouble xdouble y) { // bad!!

Rather than reading this as a constructor, the compiler will understand it as the definition of an instance method, which happens to be named GButton, and which doesn't return anything. Thus, if we could create a GButton variable stop, we would be able to write stop.GButton("str", 0, 0) to invoke the instance method. This is not what the programmer means, but the compiler will not complain about it.

The compiler will still refuse to compile the program, but its explanation it gives will not describe the problem well: The compiler will point to the line in StopGoBall where the GButton is constructed, and it will indicate that GButton doesn't have a constructor. The compiler has of course misdiagnosed the problem completely.

The solution is to delete the word void, so that the compiler interprets it as a constructor definition.

14.2. Instance variables

Section 13.3 discussed instance variables, but it did not discuss one of their more important characteristics: Each instance of the class has its own version of the instance variable. Thus, if GButton had an instance variable named label, then every GButton will have its own variable named label. (In fact, GButton doesn't have any instance variables as defined in Figure 14.3. There is a label variable, but it is a local variable, not an instance variable, since its declaration appears inside the constructor.) When an instance method refers to an instance variable, then the computer will access the variable associated with the particular object that is performing the method (i.e., this's version of the instance variable).

Let's consider an example. Suppose that we want to modify our GButton class so that it provides an instance method for changing the text in the button's label. We'll call this method setLabel. Such a method could be useful for our bouncing ball program: We may want to combine the two buttons into a single button, which says Stop when the ball is moving and Go when the ball is stopped. If we had a setLabel method, then we could accommodate this by modifying StopGoBall's mouseClicked method to the following instead. We're imagining that the single button is now referenced by an instance variable named stopGo.

public void mouseClicked(MouseEvent e) {
    if(stopGo.contains(e.getX(), e.getY())) {
        if(going) {
            going = false;
            stopGo.setLabel("Go");
        } else {
            going = true;
            stopGo.setLabel("Stop");
        }
    }
}

For this to work, we will need to define a setLabel instance method in GButton. In order to write setLabel, we will need some way of referencing the label that the button contains. In our earlier version of GButton (Figure 14.3), label was a local variable available to the constructor only. We now need to reference that variable in setLabel also, so we will have to lift this variable declaration out of the constructor to become an instance variable instead. We'll do the same with background, since when the label is changed, the rectangle will need to be resized to match the size of the label. Figure 14.4 contains this modified class.

Figure 14.4: The modified GButton class.

  1  import acm.graphics.*;
  2  import java.awt.*;
  3  
  4  public class GButton extends GCompound {
  5      private GLabel label;
  6      private GRect background;
  7  
  8      public GButton(String textdouble xdouble y) {
  9          label = new GLabel(text, 0, 0);
 10          label.setLocation(20, 10 + label.getAscent());
 11  
 12          background = new GRect(0, 0, label.getWidth() + 40,
 13              label.getAscent() + 20);
 14          background.setFilled(true);
 15          background.setFillColor(Color.GRAY);
 16  
 17          add(background);   // add background first, so it is below label
 18          add(label);
 19          setLocation(xy); // reposition entire GCompound to (x,y)
 20      }
 21  
 22      public void setLabel(String text) {
 23          label.setLabel(text);
 24          background.setSize(label.getWidth() + 40, background.getHeight());
 25      }
 26  }

If we have a program containing several GButton objects, then each will have its own label and background variables. If StopGoProgram also had two more GButtons referenced by instance variables named faster and slower, then the computer will remember all these instance variables as diagrammed in Figure 14.5.

Figure 14.5: Multiple buttons as represented in computer's memory.

When a program invokes setLabel on a button, that setLabel method will end up modifying the GLabel and GRect variables contained in that button. Invoking setLabel on a different button will modify different GLabel and GRect variables.

14.3. Protection levels

Up until now, we've consistently labeled instance variables as private and instance methods as public. Actually, instance variables can be labeled public also. If we do this, then other classes can access those variables. For example, if label were a public instance variable, then StopGoButton could perform the following to change the label to read Stop.

stopGo.label.setLabel("Stop");

This line accesses the label instance variable within the GButton referenced by stopGo, and it tells that GLabel to change its text. (Of course, the background rectangle's size would not change, since GLabel's setLabel method is a different method from GButton's setLabel method, and the GLabel has no way of knowing about the background rectangle.)

A large majority of professional programmers agree that instance variables should never be public. The reason is that in the context of a large team project, having an instance variable be public loses control of the instance variable, since then other classes might work around shortcomings in the methods by manipulating the variables in unexpected ways. The possibility above, where the label changes but not the background, is an example. Thus, instance variables should always be declared private.

Methods can be declared public or private. A method would be declared private if it is only intended to be used within that class, as a helper for other methods. Our createBalloon method from Section 12.1 is an example of such a method: We wanted the method so that other methods in the same class could use it, but we have no reason to allow methods in other classes to access createMethod. It would make more sense to define it as a private instance method.

A detail to remember

Though programs with dozens of classes may need other choices, beginners' programs should abide by the following rules: Always declare instance variables as private; and always declare methods as public or private, as appropriate.

Aside

You should always specify whether a class member, such as an instance variable, constructor, or instance method, is public or private. This is for stylistic reasons. In fact, the Java compiler will allow you to declare a class member without specifying whether it is public or private. If you do this, the class member will be mostly public but not quite. If you had a program spanning multiple packages, then the variable would be accessible only within classes in the same package. We're not concerned with such multi-package programs in this book, though, so don't worry about this detail: Simply remember always to declare each class member as public or private.

14.4. Another example: Fractions

Let's consider another example: Let's suppose we want to write a program that computes fractions. Of course, we could use doubles, but a double doesn't represent the number involved precisely, and the correct denominator for a particular double isn't immediately available. Thus we'd like to introduce a new type — the Fraction type — to represent a fraction properly.

Introducing a class has some disadvantages relative to just using doubles, though. We'll have to use new to create new Fraction objects, rather than type numbers directly. An even bigger pain is that Java doesn't allow the standard operators, like '+' or '*', to manipulate objects. (The built-in String class, which permits '+' to be applied, is a special exception to this rule.) Instead, we'll have to use instance methods. Thus, to compute ½ + ¼, we'll end up having to use an instance method to request an addition. We'll call the method add, and it will take a fraction as a parameter, add it to the fraction on which the method was invoked, and return the resulting fraction. To express the computation ½ + ¼, then, we would use the following rather cumbersome Java code.

Fraction half = new Fraction(1, 2);
Fraction third = new Fraction(1, 3);
Fraction result = half.add(third);

As we begin to consider writing the Fraction class, a useful question with which to start is to ask what each individual fraction would need to know in order to understand its identity. In this case, each Fraction object is determined by two integers — a numerator and a denominator. These two integers will therefore be the instance variables for the Fraction class.

Figure 14.6 contains the definition of a Fraction class. Notice that this class isn't defined to extend any other class, since there doesn't really seem to be an appropriate superclass. When the superclass is left unspecified, then Java will make Object be the superclass. (We could explicitly write extends Object, which achieves the same thing.)

Figure 14.6: The Fraction class.

  1  public class Fraction {
  2      private int num;
  3      private int den;
  4  
  5      public Fraction(int value) {
  6          num = value;
  7          den = 1;
  8      }
  9  
 10      public Fraction(int numeratorint denominator) {
 11          num = numerator;
 12          den = denominator;
 13      }
 14  
 15      public Fraction multiply(Fraction other) {
 16          return new Fraction(this.num * other.numthis.den * other.den);
 17      }
 18  
 19      public Fraction add(Fraction other) {
 20          int n = this.num * other.den + other.num * this.den;
 21          return new Fraction(nthis.den * other.den);
 22      }
 23  
 24      public String toString() {
 25          return num + "/" + den;
 26      }
 27  }

The Fraction class defines two constructors and three instance methods. The two constructors allow us either to construct a Fraction corresponding to an integer, or to construct a Fraction corresponding to a known numerator and denominator. The three instance methods allow us to ask a fraction to add itself to another fraction, to multiply itself by another fraction, or to compute a string representation. (The toString method in fact replaces the toString method that Fraction has inherited from the Object class. We'll revisit this in a later chapter.)

Aside

An alternative way to write the first constructor would be:

public Fraction(int value) {
    this(value, 1);
}

When the first line of a constructor looks like this(), Java invokes another constructor with the indicated parameters. Thus, if we create a fraction with new Fraction(2), which invokes the one-argument constructor, the computer would see the this() line at that constructor's beginning and promptly forward the invocation to the two-argument constructor, with 1 being the second argument. This is a fairly minor feature of Java, but later we'll see a slightly different form (substituting super for this), which will be fairly important.

Another thing that you'll notice is that the multiply and add methods talk about this.num and this.den. This is a long-winded alternative to writing num and den. We could delete this. every time it occurs, and the class would work identically. But for the aesthetic principle of symmetry, my personal preference is to opt for the long-winded version when a method accessing the same instance variables in a different object of the same class.

That brings us to another interesting point to observe about this class. Even though num and den are private, we are still allowed to reference their values on Fraction objects other than this. Thus, the references to other.num and other.den are legal. The word private prevents accessing those variables only from within other classes. In this case, other is a Fraction, and we are writing a Fraction instance method, so writing other.num is legitimate. The rationale underlying this design in Java is that privateness is to insulate other classes from being able to access internal design decisions, not to insulate a class's design from itself.