Espresso: A Concentrated Introduction to Java


Writing Your Own Classes

Summary: We describe how and why to create your own classes that serve as templates for the creation of new objects.

Prerequisites: Basics of Java.

Contents:

Making the Transition From Imperative to Object-Oriented

You have probably heard that Java is an object-oriented language. What does that mean? It means that the focus of programming in Java is supposed to be on the design of interacting objects. (To some, it means that Java supports objects, encapsulation, inheritance, and polymorphism.)

You may find that emphasis surprising, given that most of the programs you've written have focused primarily on using objects, rather than on designing objects. It turns out that Java supports two models of problem solving, imperative and object-oriented. Computer science educators have mixed opinions on which model to teach first. Espresso originally took an objects-early approach. However, as I've used and expanded Espresso, I've found that students seem to do better by mastering the basic imperative issues first.

You have now encountered most of the imperative aspects of Java. It is therefore time to move on to the object-oriented paradigm. We begin by considering how to build and use template classes in Java.

What is a Template Class?

Different object-oriented languages provide a variety of mechanisms by which programmers describe objects. In Java, as in many languages, programmers typically describe objects through template classes. A template class is a class that provides a template for building individual objects. Template classes typically contain three parts:

We will consider each in turn.

The Structure of a Java Template Class

In Java, a template class typically has the following structure.

package username.grouping;
import package.name.RelatedClass;
/**
 * A helpful introductory comment that describes the class.
 */
public class ClassName
{
  // +--------+--------------------------------------------------
  // | Fields |
  // +--------+

  Field declarations

  // +--------------+--------------------------------------------
  // | Constructors |
  // +--------------+

  Constructors

  // +---------+-------------------------------------------------
  // | Methods |
  // +---------+

  Methods

} // class ClassName

Object Fields

As suggested above, object fields provide the data associated with an object. They may be information the object contains (e.g., a list might contain a particular value as its first element), or they may describe the object (e.g., a book has a title).

An important thing to understand about object fields is that each object that is constructed stores its own copy of each field. For example, if a book class has a field title, and three book objects are constructed from that class, then each book stores its own title.

Each field declaration has three parts:

Field declarations have the form:

access-level type name;

For example, to say that we have a field named title with type String and private access, we would write

private String title;

The custom in Java is to have field names begin with a lowercase letter. When you reference a field within a class, you typically preface the name of the field with the keyword this. For example, we would refer to the title field as this.title.

You may note that field declarations look a lot like variable declarations. In fact, a field is a special type of variable. Although we have not yet introduced the term, the variables we have seen so far are "local variables." This means that they are declared within a method definition, and they are available only within that method (i.e., they are "local" to the method). Fields, on the other hand, are declared directly within the class definition, and they are available anywhere within the class. As a result, fields are available to any method in the class. In addition, you will have noticed that while fields have access levels, local variables do not.

An Example: A Vector in Two-Space

As an example, let's consider a simple class that we see in many sciences, a vector in two space rooted at the origin. If you have no idea what a vector in two space is, think of it as a line from the origin, (0,0), to a point, (x,y).

What fields might we have for a vector in two space? One possibility is to have the location of the head, that is, the x and y coordinates. Another is to use the angle of the vector from the positive x axis and the radius. Which do we choose? It depends on our application. (In fact, you will find that choosing the appropriate fields is one of the difficult aspects of object design.) For now, let's just use the radius and the angle.

  /** The length of the vector. */
  private double radius;
  /** The angle of the vector from the positive X axis in radians. */
  private double theta;

Access Levels

The field declarations above include one part that we have not yet considered in any depth: an access level (sometimes also called a protection level). The access level is one mechanism Java provides for encapsulation: it allows you to limit what other classes can access the fields of an object. There are four possible access levels as follows:

It is typical to give object fields private access. This ensures that code outside the class cannot directly access or modify the values stored in an object's fields.

As we will soon see, the same four access levels can be applied to object methods and constructors as well as to fields.

Constructors

How do object fields get initialized? Traditionally, they get initialized when you build (or construct) a new object in the class. A constructor is a special type of method whose purpose is to construct an object in the class. Therefore, we can include code in the constructor to initialize (i.e., fill in initial values for) the object's fields.

The form of a constructor is fairly simple,

access-level NameOfClass(parameters)
  OptionalThrowsClause
{
  body;
} // NameOfClass(ParameterTypes)

The body of the constructor contains a sequence of imperative operations to fill in the fields and do other initialization. The parameters provide the information necessary to fill in those fields.

It is typical to give constructors public access. This means that any class is able to construct a new object in the class.

As you've seen in the past, you typically call a constructor using the new keyword, as in

  PrintWriter pen = new PrintWriter(System.out, true);

Constructing Vectors

For the case of vectors in two-space, we might want an angle and a radius to build a vector. Here's the definition of such a constructor, assuming that we've called the class Vec2D,

public Vec2D(double _theta, double _radius)
{
  this.theta = _theta;
  this.radius = _radius;
} // Vec2D(double, double)

You'll note that I've given the parameters similar names to the fields (the main difference being that they begin with an underscore). You may choose whatever name you want for the parameters, but I find that when the parameters match the fields, it's convenient to choose similar names.

We might call this constructor as follows:

  Vec2D alpha = new Vec2D(Math.PI/2, 1.0);

We can create more than one constructor. In this case, we might also want to permit clients to construct a unit vector (that is, with a radius of 1.0) at any angle.

public Vec2D(double _theta)
{
  this.theta = _theta;
  this.radius = 1.0;
} // Vec2D(double)

We can call this constructor with

  Vec2D northeast = new Vec2D(Math.PI/4);

We might even want to permit clients to construct vectors given the x and y position of the head of the vector. It would be tempting to write such a constructor as

public Vec2D(double x, double y)
{
  this.theta = Math.atan(y/x);
  this.radius = Math.sqrt(x*x + y*y);
} // Vec2D(double, double)

Unfortunately, Java will not permit you to write two constructors that take identical types as parameters. After all, if the constructors have the same names and the same types, how will it tell which one you mean? What is the alternative? We can create a special method that builds the vector (which we describe below) or we can choose different parameter types.

public Vec2D(int x, int y)
{
  this.theta = Math.atan(((double) y)/((double) x));
  this.radius = Math.sqrt(x*x + y*y);
} // Vec2D(int, int)

Object Methods

Finally, we're ready to consider the methods of the class.

The methods that we have written up until this point have been "static methods," although we may not have used that term before. Static methods are primarily used in classes, such as main classes and utility classes, from which we do not intend to construct objects. (This is not their only usage, but it is their primary usage.) We specify that a method is a static method by including the keyword static in its declaration.

In contrast, object methods are specifically used in classes from which we do intend to construct objects. The purpose of object methods is to manipulate (i.e., access and/or modify) object fields. Static methods cannot do this. To specify that a method is an object method, we simply omit the keyword static.

The general form of an object method is therefore

/**
 * Helpful introductory comment.
 */
access-level Type methodName(parameters)
  optionalThrowsClause
{
  body;
} // methodName(parameterTypes)

We typically give object methods public access, to allow other classes to call them.

Calling a method is fairly simple: You write the name of the object (that you've already constructed), followed by a "dot", followed by the name of the method, followed by parameters. For example,

  pen.println("Hi!");

Some Methods for 2D Vectors

The simplest methods that we can call are the methods that extract simple information from an object. Such methods are typically called accessors or, when the extracted information matches a field, getters. Here's one that gets the radius.

/**
 * Determine the radius of this vector.
 */
public double getRadius()
{
  return this.radius;
} // getRadius()

Similarly, here's one that gets the angle from the positive x axis.

/**
 * Determine the angle of this vector from the positive
 * x axis.
 */
public double getTheta()
{
  return this.theta;
} // getTheta()

Here's a silly program fragment that uses those two methods.

  int x = ...;
  int y = ...;
  Vec2D sample = new Vec2D(x,y);
  pen.println("Considering the point "(" + x + "," + y + ")");
  pen.println("  That point is " + sample.getRadius() 
    + " units from the origin.");
  pen.println("  The angle between the X-axis and the line to that point is "
    + sample.getTheta());

We can certainly write methods that compute and return new values, too.

/**
 * Compute the x position of the head of this vector.
 */
public double getX()
{
  return this.radius * Math.cos(this.theta);
} // getX()

We can even write methods that build new objects.

/**
 * Scale this vector by a scalar (real) value.
 */
public Vec2D scale(double scalefactor)
{
  return new Vec2D(this.theta, this.radius * scalefactor);
} // scale(double)

Given that we can build new objects, we might even write methods that act very much like constructers.

/**
 * Build a vector to the point (x,y)
 */
public Vec2D vectorTo(double x, double y)
{
  return new Vec2D(Math.atan(y/x), Math.sqrt(x*x + y*y));
} // vectorTo(double,double)
Another simple thing we commonly do with object methods is to set the value of an object field. Again, because they are so common, these methods have a nickname: they are called modifiers, or sometimes mutators. For example, if we wish to allow a client class to rotate a 2D vector, we can provide a method like the following for this purpose.
/**
 * Set the angle of this vector from the positive
 * x axis.
 */
public double setTheta(double _theta)
{
  this.theta = _theta;
} // setTheta()

By providing accessor and modifier methods, we allow client programs access to the information within an object, without allowing direct access to the fields themselves.

Other operations that we typically provide in an object include

We will discuss methods such as these in a subsequent reading.

Finally, most classes include object methods that are specific to the workings of that particular class. These, of course, will arise on a case-by-case basis.

Methods that Operate on Multiple Objects

Recall that each object stores its own copy of all object fields. Recall also that when we call an object method we must specify which object should perform the operation. For example, suppose we have two Strings named str1 and str2. To get a capitalized version of str1 we call str1.toUpperCase(). Finally, recall that within an object method, we refer to the fields of the object on which the method was called using this.fieldname.

What if we want a method that operates on two objects of the same type? For example, suppose we want to add two 2D Vectors together? To accomplish this in Java, we write an object method in the class which accepts a parameter of the same class type. Then a client of the class can call the method on one object and pass another object to it. We have seen this before. Consider how we add two BigIntegers named big and bigger.

BigInteger biggest = big.add(bigger);

Now let's consider the code inside such a method. This code needs to operate on the fields of two objects in the class simultaneously. How can we refer to the fields of the two objects and distinguish them from one another? To solve this problem, we refer to a field belonging to the object on which the method was called with this.fieldname, and we refer to a field of the object passed in as a parameter with parameterName.fieldname.

Here is an example in which we add two 2D Vectors.

/**
 * Add another vector to this vector.
 */
public Vec2D add(Vec2D addend)
{
  return new Vec2D(this.getX() + addend.getX(),
                   this.getY() + addend.getY());
} // add(Vec2D)

Written and revised by Samuel A. Rebelsky, 2005-2006.
Revised further by Marge M. Coahran, 2006.
Samuel A. Rebelsky
rebelsky@grinnell.edu
-->