Experiments in Java


Session J3: Building Your Own Classes

Class capabilities

In the previous laboratory session, we used a number of classes including a Point class that represented points on the Euclidean plane. We also developed our own class, PointPrinter, which provided a simple method for printing information about points.

You may have noted that Point provided many capabilities that we have not yet seen how to add to PointPrinter. In particular, you may have noted that Points have a state: each point has x and y coordinates and maintains those coordinates throughout the execution of the program. In addition, Points have parameterized constructors: when you create a new point, you can specify the initial position of the point. Most classes provide a number of methods, some of which have return values: when you call a method such as getX(), the method not only gets the x coordinate, but also returns it to the calling method. We will consider each of these issues in this laboratory session.

Program state

Before we look into the state of objects, let us think a little bit about the state of a program. For example, suppose you were asked to describe everything that should be known about the following function at the point the ``What do we know?'' comment appears. What would you say?

  public void doStuff(SimpleOutput out) {
    int a = 1;
    int b = 3;
    int d = a + b * 2;
    a = d + 2;
    // What do we know?
    b = a + a;
    out.println("b is " + b);
  } // doStuff()

Hopefully, you would have described the values of the variables and parameters. For example, you might say something like ``d has the value 7, a has the value 9, b has the value 3, and out has an unknown value, corresponding to the SimpleOutput object passed as a parameter to the function''.

In Experiment J3.1 you will investigate program state.

Object state

It turns out that the state of an object is also represented by things that are very similar to the variables that we use in methods. These ``things'' are often called attributes or fields. Fields look very much like variables except that they are associated with classes (and objects), rather than methods. To declare a field in a class, write

Are there any fields that we might use with PointPrinter? Yes. We might want to keep track of the number of points that we have printed out, so that we print out not only the point, but also a number of each point. For our PointPrinter class, we would define this field with

public class PointPrinter {
  /**
   * The number of points we've printed.
   */
  protected int numprinted = 0;
  ...
} // PointPrinter

What can we do with this field now that we've added it? We need to update it and perhaps even use it. When referring to the fields of an object, you use the keyword this, a dot, and the name of the field. For example, we'll update the numprinted field in the print method of PointPrinter by adding the line

  this.numprinted = this.numprinted + 1;

The this indicates that the field belongs to the current object. While not strictly necessary, usage improves clarity. You should always use this in your code.

How might we use the new numprinted field? We can print it out whenever we print a point. For example, we might use

  out.print(this.numprinted + ": ");
  ...

In Experiment J3.2 and Experiment J3.3 you will investigate fields.

Constructors

In our example above, we initialized the numprinted field to give it an initial value. You may have noted that it is often the case when we create an integer variable, we initialize it. Often, you will initialize your fields to a default value.

However, this does lead to a question. When we create a new object, how do we specify what we want the value of that object to be? That is, if we can create an integer with initial value 1, shouldn't we also be able to create a Point with initial value (2,3) or a PointPrinter that starts numbering points at 10?

You may recall that we used constructors to set the initial values for the Points we created in a previous session. For example, to create a new Point with the initial value (2,3), we might use

Point p = new Point (2,3);

Similarly, if there was an appropriate constructor defined for PointPrinter (we have not yet defined one, but we will), one might write

PointPrinter start_with_10 = new PointPrinter(10);

We will now consider how one defines constructors. In effect, a constructor is simply a method that uses its parameters to set the values of an object's fields. If you've done experiment 3.4, you've already created a method that does that. So, how do constructors differ from regular methods? They differ in their name and their type. When you create a constructor for a class,

For example, we might declare a constructor for PointPrinter as follows

public class PointPrinter {
  ...
  /**
   * Create a new PointPrinter that counts printed points
   * starting with value start_val.
   */
  public PointPrinter(int start_val) {
    this.numprinted = start_val;
  } // PointPrinter(start_val) ...
} // class PointPrinter

Can we declare constructors with no parameters? Certainly. For PointPrinter, we might build a constructor that initializes numprinted to 1 with

public class PointPrinter {
  ...
  /** 
   * Create a new PointPrinter that numbers points starting at 1.
   */
  public PointPrinter()  {
    this.numprinted = 1;
  } // PointPrinter() 
  ...
} // class PointPrinter

Note that there are two ways to initialize fields to default values: we can use assignment statements (using equals signs), or we can use parameterless constructors (constructors without parameters). Using constructors is the preferred mechanism.

We have defined two constructors for PointPrinter, one which has a single integer parameter and one which has no parameters. Both seem to have the same name. Is this legal? Yes. Java permits multiple constructors provided it is possible to tell them apart by the parameters. If you could think of a reason, it would be legal to create additional constructors with two integer parameters or with a string parameter. However, it would not be legal to create another constructor with a single integer parameter, because Java would not be able to tell which one to use.

What happens if we do not define constructors for one of our classes? It turns out that Java creates a default constructor with no parameters that, in effect, sets all fields to "reasonable" defaults. This is why we did not need to create a constructor for PointPrinter or MyPoint. These default constructors are nice, but they also have some drawbacks. In particular, if you define your own constructors, then the default constructor is no longer available. This point is important enough point that we'll repeat it: if you define any constructor for a class (even a parameterized constructor), then there is no longer a default, parameterless constructor for the class. We will explore this issue further in the experiments.

In Experiment J3.4 you will add constructors to the PointPrinter class and investigate some of the issues surrounding the no-argument constructors. In Experiment J3.5, you will consider other issues relating to constructors.

Overloading

You've seen that it's possible to have multiple constructors for the same class. Since the name of a constructor is the same as the name of the class, it seems like we have two ``methods'' (constructor methods) in the same class with the same name.

Similarly, we've seen that different classes can provide methods with the same name. For example, there is a print method in the SimpleOutput class and a print method in the PointPrinter class.

Can we have two methods with the same name in the same class (other than constructors)? Yes! This is a common programming technique called overloading.

Would we ever want to have different methods in the same class with the same name? Yes. Consider the SimpleOutput class. This class provides a different print method for Strings, ints, longs, doubles, and all the other things you might need to print. Imagine what would happen if you needed to use a different method name for each type. Not only would you often need to look up the appropriate name, depending on what you wanted to print, you would also find it difficult to modify your code to work in slightly different situations.

How do you overload a method? You simply define the method multiple times, using different types of arguments each time. For example, we might want to add a read method to PointReader that reads the X and Y coordinates without prompting. Here is such a method.

    /**
     * Read a point without prompting.
     */
    public static Point read(SimpleInput in) {
      float x;	// The x coordinate.
      float y;	// The y coordinate.
      x = in.readFloat();
      y = in.readFloat();
      return new Point(x,y);
    } // read(SimpleInput)

How does Java decide which version of an overloaded method to use? First, it looks at the class of the object whose method it calls. For example, if you ask for printer.print and printer is a PointPrinter, then it will look in PointPrinter. If, however, printer is a SimpleOutput, then it will look in SimpleOutput. After determining which class to use, Java looks at the signature, the name of the method plus the types of the arguments. For example, if we call the print method of a SimpleOutput object with an int as a parameter, then Java looks for a method with signature print(int).

If Java can't find an exact match, it looks for an approximate match. For example, if there is no print(int) method, but there is a print(double), Java will use that instead because Java knows how to convert ints to doubles.

In Experiment J3.6, you will add the overloaded print method to PointReader. In Experiment J3.7, you will investigate some of the limitations of overloading.

Creating a bordered square class

This section is optional and is intended for classes emphasizing applets or pursuing a simultaneous discussion of applications and applets.

How can objects and classes help us as we build applets? We've already seen that it's helpful to have the predefined Font and Color classes. It is also useful to create our own classes. For example, we can use classes to make it easier to draw repeatedly a greater variety of figures. For example, we might create classes that represent stick figures, more-interesting basic shapes, and similar components for our drawings. We'll begin with a simple shape: a filled square with a different-color border.

When designing a class, we need to consider fields, constructors, and methods.

What fields will a BorderedSquare class need? It will certainly need the color of the square and the color of the border. It is also helpful to have the left edge, the top edge, and the length of each side. (You may want to consider how we'd do without those values.)

What constructors will this class need? We'll start with one. It will take five parameters, corresponding to the five fields.

  public BorderedSquare(
    int left, int top,
    int side,
    Color mainColor, Color border)
  {
    ...
  } // BorderedSquare(int,int,int,Color,Color)

What methods will this class need? We'll start with one. We'll need a way to paint the square. We'll call this method paint. What parameters does it need? It needs the graphics paintbrush for doing the actual painting.

  public void paint(Graphics paintBrush) {
    ...
  } // paint(Graphics)

How do we paint the square? We'll simply use fillRect filling in the left edge, top edge, and side length.

    paintBrush.fillRect(left, top, sideLength, sideLength);

How might we draw a border around the square? You might assume that we can use similar dimensions for a call to drawRect that you were using for fillRect, decreasing the left and top edges by 1 and increasing the width and height by 2. For example, suppose we wanted to explicitly draw a 40x40 black square with a red border. We might write

  public void paint(Graphics paintBrush) {
    paintBrush.setColor(Color.black);
    paintBrush.fillRect(5,5,40,40);
    paintBrush.setColor(Color.red);
    paintBrush.drawRect(4,4,42,42);
  } // paint(Graphics)

However, this is not precisely correct.

As you may recall, Java's coordinate system is somewhat odd, with the upper-left-corner being (0,0) and coordinates increasing to the right and downward. This should not affect our border. However, this is also not the only way in which Java's drawing conventions defy many beginning programmers' initial assumptions.

Surprisingly, the coordinates on the grid are not where the ink appears. Rather, the ink appears between these points. For example, the top-left pixel is not at (0,0). Rather, it appears between the four points (0,0), (0,1), (1,0), and (1,1). In fact, the ``graphics pen'' is placed down and to the right of the path it traverses.

Why does this make a difference? Because fillRect and fillOval color only the pixels within the area described by the parameters, whereas drawRect and drawOval color the pixels to the left and down from the object described. For example, the lower-right pixel drawn by fillRect(0,0,3,2) is bounded by (2,1), (2,2), (3,1), and (3,2). However, the lower-right pixel drawn by drawRect(0,0,3,2) falls to the right of and below the lower-right corner (3,2) and therefore is bounded by (3,2), (3,3), (4,2), and (4,3). What is the moral? Filled shapes are typically one pixel smaller horizontally and vertically then drawn shapes.

In Experiment J3.8, you will consider this drawing difficulty. In Experiment J3.9, you will create and use a bordered square class.


Copyright (c) 1998 Samuel A. Rebelsky. All rights reserved.

Source text last modified Mon Oct 25 15:18:20 1999.

This page generated on Tue Oct 26 15:38:22 1999 by Siteweaver.

Contact our webmaster at rebelsky@math.grin.edu