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.

- Problem: a calculator
- Writing the driver
- Implementing addition
- Implementing subtraction
- Defining an interface for methods common to multiple classes
- Defining a base class for common fields and methods
- Supporting more complicated expressions

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

For example:operandoperandoperator

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.

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:

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.

1. When writing a class, sometimes it is easier to start with the code that uses the class

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

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

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.06 7 -(6.0 - 7.0) = -1.0

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

Aside from the names of the variables used to store the object representing the addition or subtraction, there is nothing different about the two casesif (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 {

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:

Note how the code for the main loop is now simpler. We are relying on the fact thatpackage 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()); } } }

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:

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:

- It is
*abstract*, meaning that it is not possible to create instances of the class using the new operator. - The fields "left" and "right, as well as the constructor, have
**protected**access. This means that only subclasses of**BinaryExpressiom**will be able to use them. **BinaryExpression**implements the**Expression**interface, even though it does not implement any of the methods of this interface. This is possible because BinaryExpression is an abstract 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:

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 **BinaryExpression**s 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.