Week 12

Classes, Objects, And Methods (Part II)
Version 0


 

Object-Oriented Programming

  Last lecture we looked at objects that had storage but no behavior. In essence they were just a rich data type (what some languages refer to as a structure or record) for storing several pieces of related data together. In this lecture we will look at objects in their full glory, with both storage and behavior. We will finally begin to see what object-oriented programming is all about.

The idea in object-oriented programming is to turn the traditional view of programming (of breaking down a procedure into sub-procedures according to top-down design) on its ear. Instead of focusing on tasks, we focus on the different sorts of things (or, in the terminology, objects) that the program deals with. Thinking about those objects as active participants in the program, we ask what behaviors they should have. Finally, we attach those behaviors to the individual objects by adding them to the class definitions. A complete program consists of the individual object class definitions, together with a driver object that you invoke to run the overall program. You can think of the driver object as the main object, just like we have had a main method tieing together the other methods in a program up until now.

A Non Object-Oriented Example

  Lets look at a simple example. Consider a program which will do some simple geometric and trigonometric calculations. We expect that this program will frequently need to manipulate points on the plane. Some of the things we want the program to be able to do are:
  1. Compute a point's distance from origin.
  2. Compute a point's distance from another point.

Rather than always passing around the x and y coordinates in separate variables, we could use the ideas presented in the last lecture and build a point object that has two fields, as in:

// Class:   Point
// Author:  Joshua S. Hodas
// Date:    December 3, 1997
// Purpose: To store a point
 
class Point {
 
  public double x;
  public double y;
 
}

Here is a program that uses this definition to do the things we want:

// Class:   PointTest
// Author:  Joshua S. Hodas
// Date:    December 3, 1997
// Purpose: To use the Point class
 
class PointTest {
 
  public static void main(String args[]) {
 
    // Some code that uses the methods below to manipulate points
  }
   
   
  public static double distancePointToOrigin(Point p) {
  
    return Math.sqrt((p.x * p.x) + (p.y * p.y));
  }
 
 
  public static double distancePointToPoint(Point p1, Point p2) {
  
    return Math.sqrt(((p1.x - p2.x) * (p1.x - p2.x)) + 
                     ((p1.y - p2.y) * (p1.y - p2.y)));
  }
  
}

An Object-Oriented Example

  Another way of looking at the problem is to think in terms of the behaviors a point object should have in order to be useful in the larger program. Some desired behaviors (based on the methods we wrote in the last example) are:
  1. Be able to compute (and return) its distance from origin.
  2. Be able to compute (and return) its distance from another point.

These behaviors are added as methods to the definition of the Point class. Here's the new class definition. We'll discuss the key points below.

// Class:   Point
// Author:  Joshua S. Hodas
// Date:    December 3, 1997
// Purpose: To implement a Point class
 
class Point {
 
  public double x;
  public double y;
 
   
  public double distanceToOrigin() {
  
    return Math.sqrt((x * x) + (y * y));
  }
 
 
  public double distanceToPoint(Point p) {
  
    return Math.sqrt(((x - p.x) * (x - p.x)) + 
                     ((y - p.y) * (y - p.y)));
  }
  
}

  The first thing to notice is that after thirteen weeks of beginning all our methods with that magical incantation public static, these method definitions do not include the keyword static. The reason is that static methods (and data fields) belong to the class, whereas methods without that restriction belong to the individual objects of the class that we create. For a method to be able to access the fields of an individual object it must not be a static method. The use of static methods and fields within class definitions for real classes is beyond the scope of this course.

  Notice that the first method does not take any parameters, and yet (even though it also doesn't declare any local variables) it refers to the variables x and y. That is because these methods will always, implicitly, be operating on a particular object of the Point class. Each object of that class can be thought of as though it had its own copy of each of the methods. The methods, in turn, have direct access to the values stored in the fields of the object they are attached to. Thus, when the first method refers to the variables x and y, it means the values of those fields in the particular Point object to which this copy of the method belongs. In the second method, references to x and y refer to the fields of the Point object receiving the message, whereas p.x and p.y refer to the fields of the Point object p that the method receives as a parameter.

Invoking A Method By Sending A Message As we have said, the behaviors defined by an object's methods are connected to the individual objects of the class. In order to invoke a method attached to an object, we send the object an appropriate message. This is done by writing the name of the object we want to send the message to, a dot (or period), and the name of the method we want to invoke, together with any parameters to the method. So, for example, if we have the following variable declaration and initialization:
Point p = new Point();
p.x = 3.4;
p.y = -2.35; 
then if we wish to find the distance of that point from the origin, we might write:
double d = p.distanceToOrigin(); 
That is, we are asking Point object p "What is your distance from the origin?". The method computes the answer by looking at the coordinates of the Point object it belongs to. Similarly, if we also had the declaration/definition:
Point p1 = new Point();
p1.x = 4.4;
p1.y = 4.4; 
then if we wished to find the distance between the two points, we could write:
double d1 = p.distanceToPoint(p1); 

Or, since two points are equidistant from one another in standard geometry, we could also write:

double d2 = p1.distanceToPoint(p); 

Constructor Methods In many, if not most, instances, the first thing you will want to do after you create a new object is set the values of some of its fields, as we did above with the Point object p. To make this easier, Java allows you to provide a constructor method as part of your class definition. The constructor is called each time a new object of the given class is created with a call to new. Parameters are sent to the constructor by putting them in the parentheses after the type name in the call to new (which explains why we put the parentheses there in the first place).

The constructor method definition is, by standard agreement, the first method listed in the class definition. Its header has a special form: the name of the class is used as the name of the method, and there are no keywords like public or static, nor is there a return type. The methods parameters are specified like those of any other method. So, for example, we could add the following constructor method to the Point class definition:

Point(double x1, double y1) {
 
  x = x1;
  y = y1;
 
} 
With this method in place, we can replace the three lines we used to create the Point object p above with:
Point p = new Point(3.4,-2.35); 

Note that the constructor method is not limited to setting the values of the objects fields. It can contain any code that you want to have executed that is appropriate to setting up the object for use. This code will be executed once for each object created from that class.

Even if you are only using objects in the way they were described in the last lecture (with just storage, and no behavior) it is useful, and good style, to provide constructors for your objects.

Note that with constructor methods available, it becomes tempting, and convenient, to create objects on the fly for short-term use. For example, the distance of a point from the origin is really just a special case of the distance from a point to another point, where that other point is at the origin. Using the constructor for Point objects, we can redefine the distanceToOrigin method as:

public double distanceToOrigin() {
 
  return distanceToPoint(new Point(0,0));
 
} 

Multiple Constructors It is actually possible to define more than one constructor method for an object. When the object is created the constructor chosen is the one whose formal parameters best match the parameters given in terms of types. For example, we could have one constructor for student records that takes two strings (presumably the first and last name) and another that takes an integer (the student id).

The most common use of having multiple constructors is to define one constructor for the case that the user does not specify any initializing values in the call to new. This provides an opportunity to specify default values as well as doing any other setup work needed for the object. So, for instance, in addition to the last constructor, we could also define the following default constructor:

Point() {
 
  x = 0.0;
  y = 0.0;
 
} 

In general it is considered better style to use default constructors than to assign initialization values to the objects fields at the point the fields are defined.

Note that once you define any constructors for a class, the default constructor (the one that takes no parameters and simply allocates space on the heap for the object) is lost. This way Java allows you to require that the user use the constructors you define, and not just use the default constructor. If we wanted a default constructor for the class Point that took no parameters and just let the space be allocated for the fields, we would write the last example as a method with an empty body. That is::

Point() {

  // This constructor does nothing more than let the space be allocated.

}

Method Overloading Actually, having multiple constructor methods for a method is just a specific example of a much more general concept in Java called overloading. Any method you define in Java can have multiple definitions distinguished by the types of their parameters. For example, we could define several versions of the max method which returns the larger of its two arguments. This saves us from having to call them each by a different name, even though, logically, they all do the same thing. The different versions would differ only in the declared types of their parameters, and how they deal with the parameters to determine which is the larger one. We could include all of the following definitions of this method in one program:
public static int max(int a, int b) {
 
  if (a >= b)
    return a;
  else
    return b;
 
} 
public static double max(double a, double b) {
 
  if (a >= b)
    return a;
  else
    return b;
 
} 
public static String max(String a, String b) {
 
  if (a.compareTo(b) > 0)
    return a;
  else
    return b;
 
} 
public static Point max(Point a, Point b) {
 
  if (a.distanceToOrigin() > b.distanceToOrigin())
    return a;
  else
    return b;
 
} 

Finalizer Methods Just as the constructor is called each time an object is created, a class can define a finalizer method to be called each time an object of the class is destroyed. This method always has the name finalize and has no parameters and no return value. When are objects destroyed? Well, whenever a method exits, any objects that were created in the method that do not have a reason to continue to exist (like they are being returned from the method) are destroyed. Any time it is discovered that there is no longer any way for any part of the program to refer to an object, it is destroyed in a process known as garbage collection. The objects we have used as examples have no particular need for finalizer methods, so we will not show any actual examples. A finalizer method is typically needed when an object has taken hold of some system resource that should be released before the object is destroyed. For example, an object that writes into a file on disk should have a finalizer method that ensures that the file is closed before the object is destroyed. Otherwise other processes will not be able to access the file.

The Special Identifier this The use of the names x1 and y1 in the first Point constructor above was a bit awkward and artificial. It would be much more natural to have those parameters named x and y instead. Unfortunately this raises a problem. Having parameters (or local variables) with those names would block access to the object's fields (which have the same names) from within the method. A reference to x within the method would be a reference to the parameter x of the method, not the field x of the object. We can't refer to the field p.x in the method because the name p does not exist in the context of the method. The name p is just the name some other method is referring to this object by.

Fortunately, Java, and every other object-oriented language, provides a solution to this problem. Within the methods of an object, the object to which the method belongs can be referred to by the special name this. It is a java reserved word, and, thus, you are not allowed to create any classes or variables with that name. Using this new way of accessing the objects fields, we can rewrite the constructor method as:

Point(double x, double y) {
 
  this.x = x;
  this.y = y;
 
} 
Note that the identifier this is available in all of an object's method, not just the constructor method.

The identifier this does have one special use in the constructor method. Suppose we are defining a default constructor (for the case when the user does not supply initializing values in the call to new). If the regular constructor does additional work beyond just setting the initial values, then all that code would need to be present in the default constructor as well. It would be easier, instead, to have the default constructor call the regular constructor, giving the default values it wants to set. But it is not legal to refer to the constructor by its name (which is the same as the class name) other than in a call to new. We do not want to call new here on this class, because the object has already been allocated, we are just filling in its fields.

The solution is that inside a constructor, if the name this is used as though it were a method name (that is, it is followed by a pair of parentheses, possibly containing parameters) then that is taken to be a call to the appropriate constructor method.

So, we can rewrite the default constructor for the Point class given earlier as:

Point() {
 
  this(0.0,0.0);
 
} 

Putting It All Together Putting all these ideas together, here is our final definition of the Point class.
// Class:   Point
// Author:  Joshua S. Hodas
// Date:    December 3, 1997
// Purpose: To implement a Point class
 
class Point {
 
  public double x;
  public double y;
 
   
  Point(double x, double y) {
    
    this.x = x;
    this.y = y;
  }
 
 
  Point() {
 
    this(0.0,0.0);
  }
 
 
  public double distanceToOrigin() {
  
    return distanceToPoint(new Point(0.0,0.0));
  }
 
 
  public double distanceToPoint(Point p) {
  
    return Math.sqrt(((x - p.x) * (x - p.x)) + 
                     ((y - p.y) * (y - p.y)));
  }
  
}

Specializing A Class Via Inheritance

  One of the great strengths of object oriented programming is what is known as inheritance, the ability to define one class as a subclass of another class. The subclass inherits the fields and methods of the superclass which it extends. The subclass can augment the storage and behaviors of the superclass by adding additional fields and methods. It can also override the behavior of the superclass by redefining methods already defined in the superclass.

The notion of inheritance is a natural one. Much of our thought process is based on constructing hierarchical taxonomies of the objects in our world: motor vehicles are a special case (or subclass) of vehicles, cars are a subclass of motor vehicles, and limousines are a subclass of cars. We infer some of the properties of a limousine from what we know about motor vehicles in general, some from what we know about cars, and some from specific properties of limousines.

From a programming standpoint, inheritance provides a rich means of organizing code. Suppose we are writing a program to manage the accounts at a bank. We could begin by defining a fairly generic class of account objects. Such an object would have a field for the balance, a field for the account number, and some other fields for information such as the account owner's name, the date the account was opened, and other such bits of information. The class would also define methods for executing a withdrawal and a deposit.

A savings account is just a special kind of account that earns interest. Thus it can be defined as a subclass of the account class. All that has to be added is a field for the interest rate, and a method for crediting an interest payment. A checking account could similarly be defined as a subclass of account.

A Subclass of Point Looking at our Point example, consider a variant of points with an odd behavior: they live in a world where all lines are parallel to either the x or y axis; no diagonal lines are allowed. Thus the distance from the origin is the x value plus the y value, not the length of the straight line to the origin, since that may not be drawn. This is known as the taxicab metric world, because it is the constraint operating on cabs driving around on a rectalinear grid of city streets.

Taxicab points can be defined as a subclass of our existing Point class. They have the same storage fields as Points. However, the method for computing the distance to another point must be overriden since it describes the wrong behavior. On the other hand, the behavior for computing the distance to the origin was written in such a way that it will work as long as the distance to another point is computed correctly. Therefore, this method does not need to be overriden.

The definition of TaxicabPoint is given by the following code. Note that in the class header we have added the phrase extends Point to indicate that this is a subclass of the Point class.

// Class:   TaxicabPoint
// Author:  Joshua S. Hodas
// Date:    December 3, 1997
// Purpose: To implement a Taxicab Point class
 
class TaxicabPoint extends Point {
 
  public double distanceToPoint(Point p) {
  
    return (Math.abs(x - p.x) + Math.abs(y - p.y));
  }
  
}

The Special Identifier super Sometimes, it becomes useful for a subclass to access the behaviors of the superclass, even after those behaviors have been overriden. For example, suppose we want a TaxicabPoint to have a new method which returns its "true" (i.e. straight-line) distance to the origin. This behavior is already available in the method distanceToOrigin so it would be silly to reproduce that code. Unfortunately, that name has been redefined in this subclass to compute the taxicab-distance, so it seems out of reach.

The solution is an identifier named super. Like this, the identifier super refers to the object executing the current method. However, when a method refers to this identifier it is in effect saying: "I would like to perform the following action, but while I determine what it means to do that action, I'd like to pretend to be a member of my super class, rather than my actual class".

Thus we could add the following method to the class definition for TaxicabPoint

public double trueDistanceToOrigin() {
 
  return super.distanceToOrigin();
 
} 

A common situation is that the redefinition of the behavior of a method in a subclass involves adding some extra steps to the existing behavior. This is implemented by having the new definition perform the extra steps, at which point it then calls the version of the method in the superclass. I.e.:

public void foo() {
 
  // new steps...
  super.foo();
 
} 

A particularly common use of super is in constructors. When you define a new class (as a subclass of some other class), you get, for free a new default constructor for the subclass, that just allocates space for an object of that subclass. So, once we have defined TaxicabPoints, we can create a new one by calling:

TaxicabPoint tcp = new TaxicabPoint();
and then filling in the fields manually. Unfortunately, the subclass does not inherit the constructors form the parent class, so we can't just write:
TaxicabPoint tcp = new TaxicabPoint(2.3,4.2);
toallocate the object and fill in its fields automatically. We could write the following:
TaxicabPoint tcp = new Point(2.3,4.2);
but this code would not compile, since in general a Point is not always a TaxicabPoint, so we can't assign a Point object (as created by the call to new) to a TaxicabPoint object. To make this work, we would need to use an explicit cast, as in:
TaxicabPoint tcp = (TaxicabPoint) new Point(2.3,4.2); 
which is a bit awkward.

It is therefore better to define new constructors in each subclass. However, most of the time these constructors will base their behavior on the superclass's constructors. All we want to do is the same thing our superclass does, but returning a member of this class. We can use super to accomplich this. So, for example, the constructors for the TaxicabPoint class would be:

TaxicabPoint(double x, double y) {
 
  super(x,y);
 
} 
TaxicabPoint() {
 
  super();
 
} 
(Of course, we could have just given the code for these constructors directly, instead of appealing to the behavior of the superclass. But this is generally the prefered way of doing things. This way, if we decide to change the behavior of the constructors for all the point classes, it only needs to be changed in the root Point class. Any subclasses will reflect the change in their behavior automatically.

Here is the complete code for the TaxicabPoint class:

// Class:   TaxicabPoint
// Author:  Joshua S. Hodas
// Date:    December 3, 1997
// Purpose: To implement a Taxicab Point class
 
class TaxicabPoint extends Point {
 
  TaxicabPoint(double x, double y) {
    
    super(x,y);
  }
 
 
  TaxicabPoint() {
 
    super();
  }
 
 
  public double distanceToPoint(Point p) {
  
    return (Math.abs(x - p.x) + Math.abs(y - p.y));
  }
  
}

Putting It All Together Again Here is a simple driver program that demonstrates the behavior of the Point and TaxicabPoint classes:
Click Here To Run This Program On Its Own Page
// Class:   PointTest2
// Author:  Joshua S. Hodas
// Date:    December 3, 1997
// Purpose: To use the Point and TaxicabPoint classes
 
import HMC.HMCSupport;
 
class PointTest2 {
 
  public static void main(String args[]) {
 
    double inx, iny;
    Point p;
    TaxicabPoint tcp;
 
    HMCSupport.out.print("Please enter the coordinates of " +
                         "the first (ordinary) point: ");
    inx = HMCSupport.in.nextDouble();
    iny = HMCSupport.in.nextDouble();
    p = new Point(inx,iny);
 
    HMCSupport.out.print("Please enter the coordinates of " +
                         "the second (taxicab) point: ");
    inx = HMCSupport.in.nextDouble();
    iny = HMCSupport.in.nextDouble();
    tcp = new TaxicabPoint(inx,iny);
 
    HMCSupport.out.println("The first point is " + 
                           p.distanceToOrigin() + 
                           " units from the origin.");
 
    HMCSupport.out.println("The second point is " + 
                           tcp.distanceToOrigin() + 
                           " taxicab units from the origin.");
 
    HMCSupport.out.println("The first point is " + 
                           p.distanceToPoint(tcp) + 
                           " units from the second point.");
 
    HMCSupport.out.println("The second point is " + 
                           tcp.distanceToPoint(p) + 
                           " taxicab units from the first point.");
  }
  
}

Notice that the two points in the above program report themselves to be different distances from one another, since they use different methods to compute that distance. A key feature is that even though a TaxicabPoint inherits its method for computing its distance to the origin from the ordinary Point class, even if you enter the same coordinates for the two points in the above program, they will report themselves as being different distances from the origin. That is because even while the TaxicabPoint is using the Point class's definition for computing distance to the origin, it remembers that it is, at heart, a TaxicabPoint. When the time comes in that method to compute the distance to another point (the one at <0.0,0.0>) the TaxicabPoint remembers to use its own method for computing that distance. Any time an object is asked to execute a method, the search for the definition of that method begins at the object's own class.

It is important to remember that this is just a simple example. The methods and fields of an object can be as complex as we desire. In addition, we may use the objects we define in any way that we can use a built in type. we can make arrays of them, and we can use them as the fields of other objects. We can pass them as arguments to methods and return them as the return values of methods.

Last modified August 28 for Fall 99 cs5 by fleck@cs.hmc.edu


This page copyright ©1997 by Joshua S. Hodas. It was built with Frontier on a Macintosh . Last rebuilt on Sun, Dec 7, 1997 at 10:39:46 AM.
http://www.cs.hmc.edu/~hodas/courses/cs5/week_14/lecture/lecture.html