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
Do this...
edu.institution.username.hotj.lab11b
Design/lab11b.txt.Exercise Questions/lab11b.txt.The unit tests test a complex-number class.
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.
PointComputation InterfaceYou'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:
POINT_COMPUTATIONS is declared as an
array of PointComputations.PointComputations! They're
SolidComputations, GridComputations, and
LineComputations.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
|
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
|
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
|
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:
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.GridComputation computes a checkerboard pattern.
It's based on the even-odd parity of the x and y coordinates.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.
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.
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.
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.
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 LineCompuationDescription Type Name the slope of the line doublemySlopethe value of y when x = 0 (i.e., the y intercept) doublemyYInterceptthe color of the line java.awt.ColormyColor
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)
- If the absolute value of (
y- (mySlope*x+myYIntercept)) is less thanepsilonY,
then returnmyColor.
Otherwise,
return the superclass'splotPoint(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.
Do this...
Create a computation class
MandelbrotSetComputation that implements the
PointComputation interface.
A Mandelbrot set
is created from complex numbers
. 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
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:
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 doublevariable in xthe x-dimension epsilon doublevariable in epsilonXthe y-coordinate doublevariable in ythe y-dimension epsilon doublevariable in epsilonYthe number of iterations intvariable local iterationthe maximum number of iterations intconstant class MAX_ITERATIONS= 512complex number c (from the formula) Complexvariable local ccomplex number z (from the formula) Complexvariable local zmaximum magnitude of z doubleconstant class MAX_MAGNITUDE= 2.0z squared Complexvariable local zSquareddefault color (for the set itself) java.awt.Colorconstant class DEFAULT_COLORcolors 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:
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.
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)
- Receive
xandy(as well asepsilonXandepsilonY).- Let
cbe the complex numberx+yi.- Let
zbe the complex number0+0i.- Let
iterationbe 0.- Loop while
iteration<MAX_ITERATIONSand the magnitude ofzis less thanMAX_MAGNITUDE:End loop.
- Let
zSquaredbezsquared.- Set
zto bezSquaredplusc.- Increment
iteration.- If
iteration>=MAX_ITERATIONS,
then returnDEFAULT_COLOR.
Otherwise,
returnCOLORS[iteration% (length ofCOLORS)].
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.
A Julia set
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)
- Receive
xandy.- Let
zbe the complex numberx+yi.- Let
iterationbe 0.- Loop while
iteration<MAX_ITERATIONSand the magnitude ofzis less thanMAX_MAGNITUDE:End loop.
- Let
zSquaredbezsquared.- Set
zto bezSquaredplusmyC.- Increment
iteration.- If
iteration>=MAX_ITERATIONS,
then returnDEFAULT_COLOR.
Otherwise,
returnCOLORS[iteration% (length ofCOLORS)].
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 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.
abstract method, base class, implement (an interface), inheritance hierarchy, interface, Julia set, Mandelbrot set, parity, subclass