CMPU 102, Fall 2005 Lecture 4

In this lecture we discuss the concepts of inheritance and polymorphism in more depth, and work through a complete example showing how they can be used to solve common programming problems.

Along the way we will make note of some guidelines for writing object-oriented programs.

Outline

Problem: a calculator

Say that we want to write a calculator program to solve simple arithmetic problems.  For example:

Addition: x + y
Subtraction: x - y

We will have the calculator read simple binary expressions of the form

operand operand operator
For example:
4 5 +
7 6 -

You might be wondering why the operator is specified after the operands rather than between them.  This is known as postfix notation, and it has some useful properties that we will discuss later in the semester.

Writing the driver

We'll start by writing a driver program to read arithmetic expressions from System.in, and compute the result.  We will start by writing a calculator that can handle addition only.

package edu.vassar.cs.cs102.calc;

import java.util.Scanner;

public class Calculator {
  public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    
    while (scanner.hasNext()) {
      double left = scanner.nextDouble();
      double right = scanner.nextDouble();
      
      String operator = scanner.next();
      
      if (operator.equals("+")) {
        Addition addition = new Addition(left, right);
      
        System.out.println(addition.toString() + " = " + addition.evaluate());
      } else {
        throw new UnsupportedOperationException(
            "Unknown operator: " + operator);
      }
    }
  }
}

Note that we have used a class called Addition, even though we haven't defined it yet.  This brings us to the first guideline:

1. When writing a class, sometimes it is easier to start with the code that uses the class
By deciding how the class (Addition) is going to be used, it helps us decide what fields and methods the class needs.  In the code above, we see that Addition needs a constructor that takes two double values representing the left and right operands, a toString() method to return a human-readable version of the expression being calculated, and an evaluate() method to return the result of the addition.

Also note that when the calculator encounters an unexpected operator, it throws an exception.  This brings us to another guideline:

2. If the program receives input it can't handle, throw an exception

Implementing addition

We can implement the Addition class as follows:

package edu.vassar.cs.cs102.calc;

public class Addition {
  private double left;
  private double right;

  public Addition(double left, double right) {
    this.left = left;
    this.right = right;
  }

  public double evaluate() {
    return left + right;
  }

  public String toString() {
    return "(" + left + " + " + right + ")";
  }
}

Now we can run the main method of the Calculator class.  The bold text represents what the user types:

4 5 +
(4.0 + 5.0) = 9.0

Implementing subtraction

Now we can add support for another kind of arithmetic expression, subtraction. First, let's change the driver:

package edu.vassar.cs.cs102.calc;

import java.util.Scanner;

public class Calculator {
  public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    
    while (scanner.hasNext()) {
      double left = scanner.nextDouble();
      double right = scanner.nextDouble();
      
      String operator = scanner.next();
      
      if (operator.equals("+")) {
        Addition addition = new Addition(left, right);
      
        System.out.println(addition.toString() + " = " + addition.evaluate());
      } else if (operator.equals("-")) {
        Subtraction subtraction = new Subtraction(left, right);

        System.out.println(subtraction.toString() + " = " + subtraction.evaluate());
      } else {
        throw new UnsupportedOperationException(
            "Unknown operator: " + operator);
      }
    }
  }
}

Next, we implement the Subtraction class:

package edu.vassar.cs.cs102.calc;

public class Subtraction {
  private double left;
  private double right;

  public Subtraction(double left, double right) {
    this.left = left;
    this.right = right;
  }

  public double evaluate() {
    return left - right;
  }

  public String toString() {
    return "(" + left + " - " + right + ")";
  }
}

Now the calculator supports two kinds of arithmetic expressions:

4 5 +
(4.0 + 5.0) = 9.0
6 7 -
(6.0 - 7.0) = -1.0

Defining an interface for methods common to multiple classes

Let's look at the implementation of the Calculator class: in particular, the code that handles addition and subtraction:

if (operator.equals("+")) {
  Addition addition = new Addition(left, right);

  System.out.println(addition.toString() + " = " + addition.evaluate());
} else if (operator.equals("-")) {
  Subtraction subtraction = new Subtraction(left, right);

  System.out.println(subtraction.toString() + " = " + subtraction.evaluate());
} else {
Aside from the names of the variables used to store the object representing the addition or subtraction, there is nothing different about the two cases except the names of the classes.  This brings us to another guideline:
3. When you see duplicated code, figure out what the duplicated code fragments have in common, and find a way to combine them.

In this case, the fact that the cases for addition and subtraction are so similar suggests that the two classes are related in an important way: each supports the same methods (evaluate and toString), although they are defined in different ways.

In Java, we can define an interface to define common methods implemented by multiple classes.  The interface is a kind of base class: however, it is a base class that is completely abstract, meaning that it cannot define fields or provide an implementation of any method.  This is actually a good thing: an interface defines what methods a class should implement, but does not place any constraints on how they should be implemented.  This brings us to another guideline:

4. Use an interface to define the methods that will be implemented by a family of related classes.

We will call the interface Expression because all of the classes that implement the interface represent arithmetic expressions:

package edu.vassar.cs.cs102.calc;

public interface Expression {
  public double evaluate();
  public String toString();
}

Now we can rewrite Calculator to make use of the new Expression class:

package edu.vassar.cs.cs102.calc;

import java.util.Scanner;

public class Calculator {
  public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    
    while (scanner.hasNext()) {
      double left = scanner.nextDouble();
      double right = scanner.nextDouble();
      
      String operator = scanner.next();

      Expression expression;
      
      if (operator.equals("+")) {
        expression = new Addition(left, right);
      } else if (operator.equals("-")) {
        expression = new Subtraction(left, right);
      } else {
        throw new UnsupportedOperationException(
            "Unknown operator: " + operator);
      }
      
      System.out.println(expression.toString() + " = " + expression.evaluate());
    }
  }
}
Note how the code for the main loop is now simpler.  We are relying on the fact that Addition and Subtraction are now subclasses of Expression, which you can see from the following lines of code:
expression = new Addition(left, right);

expression = new Subtraction(left, right);

Now we need to change the Addition and Subtraction classes so that they implement the Expression interface.  Because they already implement all of the methods specified by the interface, this is very easy.  We just add implements Expression after the declaration of the class name.  For example, here is how we would change Addition:

package edu.vassar.cs.cs102.calc;

public class Addition implements Expression {

Finally, here is the inheritance diagram, showing the "Is-A" relationships between Addition/Subtraction and Expression:

Defining a base class for common fields and methods

Our Addition and Subtraction classes are still very similar.  In particular, they both define fields called "left" and "right", and they both define a two-argument constructor to initialize those fields.  Keeping in line with guideline number 3, it would be nice to elimiate this duplication.

We can do so by moving these fields, and their initialization code, into a base class.  In this case, we can observe that having left and right operands is a feature of all binary (two operand) expressions.  So, we'll define a base class called BinaryExpression:

package edu.vassar.cs.cs102.calc;

public abstract class BinaryExpression implements Expression {
  protected double left;
  protected double right;

  protected BinaryExpression(double left, double right) {
    this.left = left;
    this.right = right;
  }
}

Note several interesting features of this class:

This class illustrates two guidelines.

5. When possible, make base classes abstract.
6. Use a base class to implement common functionality (such as fields) shared by a family of related classes

Now we can modify Addition and Subtraction to inherit from BinaryExpression, which allows us to eliminate the duplicated fields declared in each of the classes, as well as the duplicated initialization code in their constructors.  Here is Addition:

package edu.vassar.cs.cs102.calc;

public class Addition extends BinaryExpression {
  public Addition(double left, double right) {
    super(left, right);
  }

  public double evaluate() {
    return left + right;
  }

  public String toString() {
    return "(" + left + " + " + right + ")";
  }
}

The inheritance hierarchy now looks like this:

Supporting more complicated expressions

So far we have been able to support simple expressions consisting of two operands of type double and one operator.  What if we want to allow more complicated expressions, such as

(2 + 3) - (4 + 5)

Note that operands to the subtraction operator are themselves expressions: the addition "2 + 3" and the addition "4 + 5".  This suggests that the operands of a binary expression must themselves also be expressions.

First we can change BinaryExpression so that its "left" and "right" fields have type Expression:

package edu.vassar.cs.cs102.calc;

public abstract class BinaryExpression implements Expression {
  protected Expression left;
  protected Expression right;

  protected BinaryExpression(Expression left, Expression right) {
    this.left = left;
    this.right = right;
  }
}

The Addition and Subtraction classes must now be modified to reflect this change.  The only change we need to make is that rather than reading the numeric value of the "left" and "right" fields directly, we must call the evaluate to compute their values---because now operands are expressions whose value must be computed.  Here is Addition:

package edu.vassar.cs.cs102.calc;

public class Addition extends BinaryExpression {
  public Addition(Expression left, Expression right) {
    super(left, right);
  }

  public double evaluate() {
    return left.evaluate() + right.evaluate();
  }

  public String toString() {
    return "(" + left + " + " + right + ")";
  }
}

Because BinaryExpressions no longer directly contain the numeric value of their operands, we now need some way to represent literal numeric values (like "3" and "4").  We can do this with a new class, which is also an Expression.

package edu.vassar.cs.cs102.calc;

public class Literal implements Expression {
  private double value;

  public Literal(double value) {
    this.value = value;
  }

  public double evaluate() {
    return value;
  }

  public String toString() {
    return "" + value;
  }
}

Here is a simple example of how we can combine arbitrary expressions into a more complicated expression:

public class ComplexExpressionDemo {
  public static void main(String[] args) {
    // Represent (2 + 3)
    Expression left = new Addition(new Literal(2), new Literal(3));

    // Represent (4 + 5)
    Expression right = new Addition(new Literal(4), new Literal(5));

    // Represent (2 + 3) - (4 + 5)
    Expression complex = new Subtraction(left, right);

    System.out.println(complex.toString() + " = " + complex.evaluate());
  }
}

When executed, this program prints

((2.0 + 3.0) - (4.0 + 5.0)) = -4.0

With the addition of the Literal class, the inheritance hierarchy looks like this:

Using this design, it is easy to add classes to represent other types of binary expressions, such as multiplication and division, or more exotic functions such as sine and cosine, which we could make subclasses of a UnaryExpression base class.