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 PointComputation
s.PointComputation
s! They're
SolidComputation
s, GridComputation
s, and
LineComputation
s.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 GridComputation
s 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 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)
- 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 LineComputation
s 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 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
= 512complex 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.0z 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:
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
x
andy
(as well asepsilonX
andepsilonY
).- Let
c
be the complex numberx
+y
i.- Let
z
be the complex number0
+0
i.- Let
iteration
be 0.- Loop while
iteration
<MAX_ITERATIONS
and the magnitude ofz
is less thanMAX_MAGNITUDE
:End loop.
- Let
zSquared
bez
squared.- Set
z
to bezSquared
plusc
.- 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
+y
i.
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
x
andy
.- Let
z
be the complex numberx
+y
i.- Let
iteration
be 0.- Loop while
iteration
<MAX_ITERATIONS
and the magnitude ofz
is less thanMAX_MAGNITUDE
:End loop.
- Let
zSquared
bez
squared.- Set
z
to bezSquared
plusmyC
.- 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