Hands on Testing Java: Lab #11b

Inheritance

Introduction

One of the greatest things about inheritance is that someone can create a general program to solve a whole set of problems, and then with inheritance you can, at your leisure, plug new classes into the program without having to change any (or very little) of the existing code.

Consider the concrete problem for this lab exercise: graphing mathematical functions. There are an infinite number of functions that can be graphed, and one programmer couldn't write the code for them all. But a programmer could write a general program where other programmers (like yourself) can provide specific code for a specific function.

Inheritance in Java makes this possible. This other programmer can write all of his code using an interface as a data type; this allows all of the base code to compile. You can come along later and implement this interface so that you can implement your own behaviors using the existing application

Getting Started

Do this...

Do this...
Compile the code, and run the unit tests for a green bar.

The unit tests test a complex-number class.

Getting Acquainted with the Code

PointComputationGUIDriver is the GUI driver for this lab exercise. This is the class that you'll run all throughout this lab exercise. It's fairly boring since it makes a PointComputationFrame object do all of the real work.

The PointComputationFrame establishes the look of the GUI. This is the start of the code you will not need to touch. It really doesn't matter if you've played around with GUIs or not. The purpose of this code is to handle all of the GUI work for you. Through the magic of inheritance, you can plug into this program with little effort. (And spend all of your effort on coming up with interesting computations.)

PointComputationGUIDriver#POINT_COMPUTATIONS is an array of these computations; for the rest of this lab, you'll just need to create new classes and add to this array. No other code should change.

The PointComputation Interface

You'll be using an inheritance hierarchy that's already been started. The PointComputation interface is the base class for the inheritance hiearchy; that is, you will use variables declared as type PointComputation (like the POINT_COMPUTATIONS array). However, an interface provides no way of creating instances; instead you will create instances of subclasses of the interface; these instances implement the interface's methods, so they deliver on the interface's promises.

Consider the PointComputationGUIDriver#POINT_COMPUTATIONS array:

Do this...
Take a look at this array, and observe the inheritance here. You'll be adding to this array declaration, so get familiar with it now!

This is inheritance. PointComputation is an interface which these other classes implement to satisfy a need in the rest of the code. The PointComputation interface makes certain promises; in particular, it promises the ability to plot a mathematical function. In order to deliver on these promises, the subclasses of this interface must provide concrete definitions for these promises.

Do this...
Out of the box, the GUI driver will display a solid-color image. Run the driver to see this. Use the selection box and the Replot! button to try some of the other computations. Change the x and y coordinates to zoom in and out of the graph.

Look more closely at the PointComputation interface. It follows this pattern:

interface-declaration pattern
public interface InterfaceName {
  ConstantDeclarations
  AbstractMethodDeclarations
}
  • InterfaceName is the name of the new data type.
  • ConstantDeclarations is a list of constant declarations.
  • AbstractMethodDeclarations is a list of abstract methods.
  • No variables; only abstract instance methods.
  • The name of an interface follows the same conventions as the name of a class.

PointComputationFrame and its support classes need computations with a particular behavior: plot a color at a given (x,y) coordinate. The "frame" class and the other GUI code does not need to know what to do; it needs to know that it can be done. The computation object itself will worry about the actual computation at runtime.

Inheritance with an interface delivers exactly this ability. An interface is essentially a class that can provide only constants and abstract methods---no variables, no constructors, no method bodies. The syntax for an interface replaces the class keyword with the interface keyword.

An abstract method provides a signature but no behavior:

abstract-method-declaration pattern
public abstract returnType methodName(parameters);
  • returnType is the data type of the value that the method returns.
  • methodName is the name of the method.
  • parameters is a list of declarations of parameters.

By default all of the methods in an interface are abstract, so it is not necessary to add the abstract keyword to the declaration, although you can include it if you like. (In an abstract class, the abstract keyword is necessary.)

To become a subclass of the PointComputation interface, a class must implement the interface:

class-declaration-implementing-interfaces pattern
public class ClassName implements InterfaceList {
}
  • ClassName is the name of the new data type.
  • InterfaceList is a comma-separate list of interface names.

In the class, you must must provide a concrete definition for all methods in the interface. So for the subclasses of PointComputation, they must all implement a plotPoint(double,double,double,double) method.

There are three subclasses already:

  1. SolidComputation computes a image with the same, solid color over the whole image. It's pretty boring, but it'll show how the inheritance works.
  2. GridComputation computes a checkerboard pattern. It's based on the even-odd parity of the x and y coordinates.
  3. AxisComputation computes a solid image with lines for the x and y axes.

Since these classes all implement the PointComputation interface, the program can use any one of them to create an element in the POINT_COMPUTATIONS array.

Interface Inheritance

Consider the SolidComputation class. It is a class because we want to create concrete instances of it. In the class declaration, note that it implements the PointComputation interface.

The PointComputation interface promises just one method. Take a look at the interface. The plotPoint(double,double,double,double) method receives x and y coordinates, and the method must return a color for that point. (There are two other parameters, but we'll discuss them later.)

Now go back to the SolidCompuation class. If the mathematical function is to plot the same value throughout the whole plane, then it doesn't matter what point is being plotted, the solid computation returns the same color. Consequently, SolidComputation#plotPoint(double,double,double,double) ignores all of its parameters. The constructor for this class receives a java.awt.Color object for the solid color.

Do this...
Add another SolidComputation object to the POINT_COMPUTATIONS array with a different color for the background. You'll find it helpful to consult the on-line API for the java.awt.Color class; there are constants declared in that class for a variety of colors.

Run the GUI driver, and try out this new addition to the driver's plotting options.

Another Subclass

Do this...
Now run the driver again, and plot a grid computation.

The GridComputation class also implements the PointComputation interface---that's the only way it would work in this program! But compared to the SolidComputataion, the grid computation is a little more involved. This class uses two different colors to compute the grid. The idea is this: if the parity of the x and y coordinates are the same, then the point is one color; if the parity is different, then the point is the other color.

Parity here corresponds to the even and odd properties of the coordinates. This isn't strictly observed because the coordinates can be negative, and the coloring parity has to be tweaked when x and y go negative. That's the purpose of the first two statements in the method.

The rest of the method tests the parity and returns the appropriate color.

This class also has a name attribute so that you can keep track of different color choices in the GUI driver.

Do this...
Add two more GridComputations to the POINT_COMPUTATIONS array. Run the driver, and test both of the new computations out.

A Subclass That Uses the Epsilons

The last class that you've been provided with computes an image where the x and y axes are drawn as lines.

Do this...
Run the driver, and plot the axis computation.

The basic idea for the AxisComputation#plotPoint(double,double,double,double) method is this:

if (x == X_AXIS)
    return X_AXIS_COLOR;
else if (y == Y_AXIS)
    return Y_AXIS_COLOR;
else
    return BACKGROUND_COLOR;

However, floating-point numbers are ultimately inaccurate. It's very rare to do any floating-point computations and end up with exact equality. So it's highly unlikely, for example, that x will exactly equal X_AXIS.

The standard trick is to actually test a range around the target value. Instead of testing for equality, we want to make sure that x is somewhat close to X_AXIS. That's where xEpsilon comes in; it's a fudge factor to describe how close x should be to X_AXIS in order for us to consider them "practically equal".

This is computed with this expression:

Math.abs(x - X_AXIS) < epsilonX

epsilonX is a really small value, and if the x given to the method is that close to X_AXIS, then that's good enough for us. So the method returns the color of the x axis.

A similar computation is done for the y coordinate.

If the x and y coordinates are not on either axis, then the background color is returned.

Function Plotting

Perhaps you'd like to plot a computed straight line. Open up any high school algebra or college calculus book to find hundred (thousands?) of examples of formulas for a line:

y = 5x+3
y = 3/4x-2
etc., etc., etc.

It'd be nice if you could plot these functions.

However, you'd also like to plot the x and y axes as well so that the line has some context. You could copy over all of the code from the AxisComputation class, but doesn't that defeat the purpose of that class? That code already exists in a class, shouldn't there be some way to reuse it without copying into another class? Sure! Inheritance!

Let's see how this will work.

Do this...
Create a computation class LineComputation that extends the AxisComputation class.

By extending AxisComputation, you indirectly implement the PointComputation interface, so you will be able to add instances of this class to PointComputationGUIDriver#POINT_COMPUTATIONS.

You need to override the plotPoint(double,double,double,double) method inherited from AxisComputation. The only thing you have to worry about in LineComputation#plotPoint(double,double,double,double) is the computation of the line; if the x and y coordinates are not on the line, then we can let AxisComputation#plotPoint(double,double,double,double) deal with the computation.

You also need to override the toString() method so that you get a meaningful entry in the combo box where the user selects a computation.

First, a line has three attributes:

Attributes of LineCompuation
Description Type Name
the slope of the line double mySlope
the value of y when x = 0 (i.e., the y intercept) double myYIntercept
the color of the line java.awt.Color myColor

Do this...
Create instance variables for these attributes, and write a constructor that receives values for these attributes and initializes the instance variables appropriately.

Let's make sure you get some interesting output in the combo box in the GUI. Assert this with unit tests:

public void testToString() {
  assertEquals("slope of 5, y intercept 3", "y = 5.0x+3.0",
      new LineComputation(5.0, 3.0).toString());
  assertEquals("slope of 3/4, y intercept -2", "y = 0.75x+-2.0",
      new LineComputation(3.0 / 4.0, -2.0).toString());
}

Do this...
Create a test-case class LineComputationTest. Implement the testToString() method above in this class. Write LineCompuation#toString(). Run the unit tests for a green bar.

Now here's the algorithm for LineComputation#plotPoint(double,double,double,double):

Algorithm of LineCompuation#plotPoint(double,double,double,double)
  1. If the absolute value of (y - (mySlope * x + myYIntercept)) is less than epsilonY,
        then return myColor.
    Otherwise,
        return the superclass's plotPoint(double,double,double,double).

Do this...
Write the code for this method.

Do this...
Now add three LineComputations to the PointComputationGUIDriver#POINT_COMPUTATIONS, each time setting the slope and y-intercept to values of your choice. (Hint: keep the y-intercept between -5 and +5.) Run the GUI, and test all three plots.

Make sure that the color of the line is different from the axis-computation's background color!

Any simple plot from high-school algebra can be graphed this same way; just store different instance variables and change the formula in the if.

Mandelbrot Set

Do this...
Create a computation class MandelbrotSetComputation that implements the PointComputation interface.

A Mandelbrot set wikipedia is created from complex numbers wikipedia. A complex number consists of two real numbers: a "real" component and an "imaginary" component. This is often written a+bi, where a is the real component, and b is the imaginary component. b is considered "imaginary" because i represents the square root of -1. Normally you cannot take the square root of a negative number, but mathematicians discovered that if we imagine that we could take the square root of negative number, then some interesting things result. The Mandelbrot set is one of those interesting things.

The basic idea for the Mandelbrot set (and other fractals wikipedia based on complex numbers) is that you repeatedly compute a function, feeding one answer back into the function over and over and over. The Mandelbrot set uses this formula:

z = z2 + c

Start with z = 0+0i and c = x+yi. Compute a new value for z, and feed that back into the formula. Repeat. One of two things will happen:

  1. If the magnitude of z gets too large (i.e., too far away from 0+0i), the current value for c is not in the Mandelbrot set.
  2. If the magnitude of z never gets too large (even after many iterations), c is in the Mandelbrot set.

Your implementation of a Mandelbrot set does not have any attributes (i.e., z always starts at 0+0i, and c is always determined by the current x and y). So you don't need any instance variables, but to keep things explicit, you should still have at least one constructor.

Do this...
Define a default constructor for the Mandelbrot class. It doesn't do anything.

The provided code supplies you with a Complex class to make writing the code for a Mandelbrot set (and other computations based on complex numbers) easier.

Objects of MandelbrotSetComputation#plotPoint(double,double,double,double)
Description Type Kind Movement Name
the x-coordinate double variable in x
the x-dimension epsilon double variable in epsilonX
the y-coordinate double variable in y
the y-dimension epsilon double variable in epsilonY
the number of iterations int variable local iteration
the maximum number of iterations int constant class MAX_ITERATIONS = 512
complex number c (from the formula) Complex variable local c
complex number z (from the formula) Complex variable local z
maximum magnitude of z double constant class MAX_MAGNITUDE = 2.0
z squared Complex variable local zSquared
default color (for the set itself) java.awt.Color constant class DEFAULT_COLOR
colors for points outside the set java.awt.Color[] constant class COLORS

Traditionally, DEFAULT_COLOR is black.

The COLORS array will be used for the points that are not in the Mandelbrot set. A color is assigned to each (x,y) based on how many iterations it takes to blow up the magnitude of z. You have (at least) two options in setting up this array:

  1. Initialize COLORS in the same way that PointComputationGUIDriver#POINT_COMPUTATIONS is allocated and initialized. You can put Color.BLUE and other Color constants in the intialization. This can (and should) be done when you declare the constant.
  2. Create a range of colors like this:
    for (int i = 0; i < COLORS.length; i++) {
      COLORS[i] = new Color(
                   0,
                   ((128 + i)) * 255 / ((128 + COLORS.length)),
                   0);
    }
    
    This makes all of the colors some shade of green. Allocate an array (of length MAX_ITERATIONS) when you declare COLORS; put the loop in the constructor. (Feel free to tweak this loop after you have it working.)

The more colors you have in COLORS the more details you will see in your pictures.

Do this...
Declare the constants in this object list in the MandelbrotSetComputation as class constants (remember: public static final).

You want to make sure that useful information comes up in the combo box:

Do this...
Create a test-case class MandelbrotSetComputationTest. Implement MandelbrotSetComputationTest#testToString() to make sure that MandelbrotSetComputation#toString() returns "Mandelbrot set". Fix your code so that the tests green bar.

Read the Complex class and its comments to figure out the operations in this algorithm:

Algorithm of MandelbrotSetComputation#plotPoint(double,double,double,double)
  1. Receive x and y (as well as epsilonX and epsilonY).
  2. Let c be the complex number x+yi.
  3. Let z be the complex number 0+0i.
  4. Let iteration be 0.
  5. Loop while iteration < MAX_ITERATIONS and the magnitude of z is less than MAX_MAGNITUDE:
    1. Let zSquared be z squared.
    2. Set z to be zSquared plus c.
    3. Increment iteration.
    End loop.
  6. If iteration >= MAX_ITERATIONS,
        then return DEFAULT_COLOR.
    Otherwise,
        return COLORS[iteration % (length of COLORS)].
    End if.

This algorithm safely ignores the epsilons. However, the method must still receive them since that is the promise made in the interface! Despite your algorithm, you must use the exact same method signature for plotPoint(double,double,double,double) in your subclasses, especially the order of the arguments. It doesn't matter if you're going to ignore a parameter or two, you must still receive them.

Do this...
Write the code for MandelbrotSetComputation#plotPoint(double,double,double,double).

Do this...
Add a MandelbrotSetComputation object to the PointComputationGUIDriver#POINT_COMPUTATIONS array, and run the GUI to test it.

This computation may take a long time, so be patient.

Julia Sets

A Julia set wikipedia is computed similarly to a Mandelbrot set with one significant difference---significant enough that it's worthwhile to create a separate class with an almost-identical algorithm.

Before you create a class to compute Julia sets, though, it's helpful to know that the color array and the default color that you have in MandelbrotSetComputation will also be useful in computing colors for a Julia set. It would be really nice if you could reuse those constants.

Helpful hint: J2SE 5.0 provides static imports to make inheritance of constants unnecssary. Some frown on using interfaces and inheritance to get direct access to constants like this, but because of all of the legacy code out there which does use this technique, it's worth knowing.

You could just refer to MandelbrotSetComputation.COLORS inside your JuliaSet class, but what's so special about the MandelbrotSetComputation that it gets to hold on to that color array? Neither of these classes should have it; it should go into a common class.

Fortunately, interfaces provide a slick solution to this problem. In addition to promising methods, an interface can also declare constants.

Do this...
Create an interface named FractalConstants. Then cut all four constants out of MandelbrotSetComputation, and paste them into FractalConstants. If you're using a loop to initialize the COLORS array, move that loop to the beginning of PointComputationGUIDriver#main(String[]).

You'll get errors in MandelbrotSetComputation (and PointComputationGUIDriver) because the constants are no longer declared there. This is fixed by implementing the right interface.

Do this...
In the implements clause of MandelbrotSetComputation, add FractalConstants to the list of interfaces that it implements. (This list is separated with commas.) Do the same to PointComputationGUIDriver if you need to. Now the code should compile correctly, and the GUI should run as before.

And now on to the Julia set computation.

Do this...
Create a computation class JuliaSetComputation that implements both the PointComputation and FractalConstants interfaces.

The main difference between the Mandelbrot set and a Julia set is that a Julia set has a fixed value for c. The value for c in the Mandelbrot set computation changes from point to point (the x and y passed into plotPoint(double,double,double,double)). In a Julia set, c stays fixed for all points; z is set to x+yi.

So the JuliaSetComputation class has one attribute, a value for c.

Do this...
Create an instance variable named myC, a Complex. Create a constructor to initialize the instance variable with a value passed in as a parameter.

Do this...
Create a test-case class JuliaSetComputationTest. Implement JuliaSetComputationTest#testToString() to assert that JuliaSetComputation#toString() returns strings like "Julia Set at 1.0+1.0i" or "Julia Set at -2.3+-4.3i". Fix your code so that the unit tests green bar.

Complex#toString() is already written, so use it!

Here's the computation:

Algorithm of JuliaSetComputation#plotPoint(double,double,double,double)
  1. Receive x and y.
  2. Let z be the complex number x+yi.
  3. Let iteration be 0.
  4. Loop while iteration < MAX_ITERATIONS and the magnitude of z is less than MAX_MAGNITUDE:
    1. Let zSquared be z squared.
    2. Set z to be zSquared plus myC.
    3. Increment iteration.
    End loop.
  5. If iteration >= MAX_ITERATIONS,
        then return DEFAULT_COLOR.
    Otherwise,
        return COLORS[iteration % (length of COLORS)].
    End if.

Do this...
Implement JuliaSetComputation#plotPoint(double,double,double,double). Add three different JuliaSet objects to the PointComputationGUIDriver#POINT_COMPUTATIONS array. Run the driver, and test these computations.

Submit

Submit copies of your code (including classes you didn't modify). Turn in screen captures of two line plots, two interesting regions of the Mandelbrot set, and three interesting Julia sets.

Terminology

abstract method, base class, implement (an interface), inheritance hierarchy, interface, Julia set, Mandelbrot set, parity, subclass