Goal of this homework: Practice in designing a simple system of related classes in Java. You will use inheritance, method overriding, an abstract class, and test methods. You will also use arrays.
Collaboration policy for this homework:
You must follow the CS
Department Academic Integrity Code, except that for this
homework, you may optionally work with one other student in the
class. If you do so, you must say who your partner was at the top of
your Readme
file, and both of you will receive the same grade
for the assignment.
Readme
file.
What to hand in:
Grass.java
Lawn.java
ReboundGrass.java
ReboundLawn.java
Vegetation.java
(this is given to you)
Please follow the style guidelines in section 1.9.2 of the textbook. For an example of well-commented code, see the Toilet class. That code uses javadoc-style comments, which is encouraged, though it's not required for this homework.
Readme
file that contains your answers to
miscellaneous questions on the homework (i.e., answers that don't
go in any other file). Usually these are English paragraphs
or small fragments of code.
Readme
what you did.
If you haven't used java or javadoc much before, first try
compiling and running the Toilet code we
studied in class. You don't have to write any new code for this.
Just follow the directions in the Readme
file. You may also
want to study the code to be sure you understand what's going on.
There is lots of grass in the world, and it grows at a geometric rate - a surprisingly fast 2% to 9% per day, depending on the species. Americans just love to grow it and mow it.
In this assignment, you'll write some classes for grasses. These aren't really data structures, because we won't be using them to store external data. (Toilets are more like data structures: you can put data in, query it, and delete it.) But they're good practice for object-oriented programming in Java.
Let's start with the simplest object: a single blade of grass.
Below is a skeleton for the Grass class. I've written a few bits of it
for you; you should fill in the rest so that the main
method works. For safety, all fields should be private
or
protected
.
Here are some rules about how grass works:
h
,
construct one of height h * randomMultiplier()
.
AVG_DAILY_GROWTH * randomMultiplier()
. Thus, on a
given day, a given blade of grass might grow 6% or 4% rather
than exactly 5%.
You can represent numbers internally as float
or
double
, as you prefer. (Which uses less memory?)
However, the public interface to your class should work correctly
with the main
method provided. (This will give
you some practice with casting.)
height()
method should return a
float
. This way, main
's print statements
will print only about 8 significant digits instead of about 17.
main
calls methods with
double
arguments (0.2
rather than
the less convenient 0.2F
). You must support this.
0.3
to a float
variable, you will lose some
digits of precision, so Java makes you explicitly cast it:
float f = (float)0.3
.
Here are some questions to answer in the Readme
file you
will submit:
AVG_DAILY_GROWTH
is
declared static
. What design decision does this
represent?
3 * AVG_DAILY_GROWTH *
randomMultiplier()
.
/** A blade of grass. Put some documentation here. * * @author [your name here] * @version 1.0, due 2003-02-07 at 4pm */ public class Grass { /** A value of 0.05 means that grass gets about 5% taller every day. */ private static final double AVG_DAILY_GROWTH = 0.05; /** A random-number generator, shared by all blades of Grass * (i.e., all instances of this class). */ private static final java.util.Random random = new java.util.Random(); --- HERE FILL IN FIELDS, CONSTRUCTORS, METHODS --- /** @return A positive random number with average 1. */ protected static double randomMultiplier() { return Math.exp(random.nextGaussian() - 0.5); // don't worry, this works } /** A test method for this class. * @param args Command-line arguments, currently ignored. */ public static void main(String args[]) { System.out.println("Planting tiny blade of grass of height about 0.2"); Grass g = new Grass(0.2); System.out.println("height = "+g.height()+" initially"); System.out.println("----------"); for (int i=0; i < 10; i++) { g.grow(1); System.out.println("height = "+g.height()+" after waiting a day"); } System.out.println("----------"); for (int i=0; i < 10; i++) { g.grow(7); System.out.println("height = "+g.height()+" after waiting a week"); g.mow(2); System.out.println("height = "+g.height()+" after mowing to max height 2"); } } } |
And here is a sample run of java Grass
:
Planting tiny blade of grass of height about 0.2 height = 0.21811609 initially ---------- height = 0.26172248 after waiting a day height = 0.28944865 after waiting a day height = 0.3171882 after waiting a day height = 0.3250959 after waiting a day height = 0.33057296 after waiting a day height = 0.34701943 after waiting a day height = 0.4538842 after waiting a day height = 0.49012244 after waiting a day height = 0.5025614 after waiting a day height = 0.5035262 after waiting a day ---------- height = 0.7059034 after waiting a week height = 0.7059034 after mowing to max height 2 height = 0.9477783 after waiting a week height = 0.9477783 after mowing to max height 2 height = 1.8177209 after waiting a week height = 1.8177209 after mowing to max height 2 height = 2.4983006 after waiting a week height = 2.0 after mowing to max height 2 height = 2.7584171 after waiting a week height = 2.0 after mowing to max height 2 height = 2.9286404 after waiting a week height = 2.0 after mowing to max height 2 height = 2.5609012 after waiting a week height = 2.0 after mowing to max height 2 height = 2.6664488 after waiting a week height = 2.0 after mowing to max height 2 height = 2.9346337 after waiting a week height = 2.0 after mowing to max height 2 height = 2.7400591 after waiting a week height = 2.0 after mowing to max height 2 |
Now make a class called Lawn
. Internally, each lawn
object will store an array of many Grass
objects. But this
array should be private
or protected
.
Like Grass
, Lawn
implements
grow
, mow
, and height
methods.
The grow
and mow
methods should just grow or
mow each blade of grass in the lawn, by invoking each blade's
grow
and mow
methods. The
height
method should return the
average height of the blades of grass on the lawn.
Give your Lawn
class a main
function like
this:
public static void main(String args[]) { System.out.println("Planting lawn of 1,000,000 blades of height about 0.2"); System.out.println("Note: We will report average height."); Lawn l = new Lawn(1000000, 0.2); System.out.println("height = "+l.height()+" initially"); System.out.println("----------"); for (int i=0; i < 10; i++) { l.grow(1); System.out.println("height = "+l.height()+" after waiting a day"); } System.out.println("----------"); for (int i=0; i < 10; i++) { l.grow(7); System.out.println("height = "+l.height()+" after waiting a week"); l.mow(2); System.out.println("height = "+l.height()+" after mowing to max height 2"); } } |
Since allocating memory is generally slow, the slowest part of
main
is creating a million blades of grass. Mowing them
down is comparatively quick. Making them grow is slow because
randomMultiplier
does some fancy math internally.
Waiting for them to grow for a whole week is, well, sort of like
watching grass grow ...
The output of java Lawn
should be something like the
following. Notice that by the law of averages, the average height of
the million blades of grass really does start very close to 0.2, and
really does grow at very close to 5% per day. Of course, the
individual blades of grass are all different heights, giving the
lawn a raggedy appearance that makes us want to mow it.
Planting lawn of 1,000,000 blades of height about 0.2 average height = 0.20006523 initially ---------- average height = 0.21008006 after waiting a day average height = 0.22059338 after waiting a day average height = 0.23163944 after waiting a day average height = 0.24326608 after waiting a day average height = 0.25542048 after waiting a day average height = 0.26820368 after waiting a day average height = 0.2816669 after waiting a day average height = 0.2957375 after waiting a day average height = 0.31054994 after waiting a day average height = 0.32607242 after waiting a day ---------- average height = 0.4588957 after waiting a week average height = 0.42764112 after mowing to height 2 average height = 0.60186344 after waiting a week average height = 0.5684981 after mowing to height 2 average height = 0.80000013 after waiting a week average height = 0.73548037 after mowing to height 2 average height = 1.0347948 after waiting a week average height = 0.922672 after mowing to height 2 average height = 1.298656 after waiting a week average height = 1.119024 after mowing to height 2 average height = 1.5741484 after waiting a week average height = 1.3116245 after mowing to height 2 average height = 1.8459921 after waiting a week average height = 1.4882632 after mowing to height 2 average height = 2.094302 after waiting a week average height = 1.6390508 after mowing to height 2 average height = 2.3066342 after waiting a week average height = 1.7592006 after mowing to height 2 average height = 2.474227 after waiting a week average height = 1.8480842 after mowing to height 2 |
More questions to answer in your Readme
file:
grow
method for Grass
override the grow
method for Lawn
?
The Meadow, a band of radical environmental scientists, has just developed a new form of sneaky "rebound grass" that doesn't like to be mown! If you have a 2.7-inch blade of rebound grass, you can mow it to 2 inches, just like ordinary grass. But it remembers that it used to be 2.7 inches tall! The next time you let it grow, it rebounds instantly to 2.7 inches (and then grows by about 5% per day, as usual).
Implement ReboundGrass
as a subclass of
Grass
. Try to figure it out yourself before
reading the following hints:
grow
and
mow
methods. The new versions should call
super.grow
and super.mow
to do most
of the work.
ReboundGrass
must change the height of the grass.
If Grass
stores that height in a private
variable, you're in trouble. You have one of two choices:
Grass
to make that variable protected
instead, so that subclasses like
ReboundGrass
can modify it directly.
Grass
a protected
method setHeight
that ReboundGrass
can use to modify the height.
ReboundGrass(double
height)
. It can just call super(height)
.
(Yes, I was correct when I said in class that constructors are
not inherited by subclasses. For the tricky details, see
the section on "Constructor Chaining" in chapter 3 of Java in a
Nutshell.)
main
. For now,
just copy Grass.main
to
ReboundGrass.main
, and change new
Grass(0.2)
to new ReboundGrass(0.2)
.
grow
twice in a row
... will your code do the right thing? Also implement ReboundLawn
as a subclass of
Lawn
. This is very simple and does not need any
new fields. When you're constructing the
lawn, just fill its internal array with new ReboundGrass
objects instead of new Grass
objects.
Here is sample output from java ReboundLawn
. Notice
that this lawn will eventually take over the planet unless time
stops, or unless it is destroyed by some means other than
mowing.
Planting rebound lawn of 1,000,000 blades of height about 0.2 Note: We will report average height. height = 0.20041 initially ---------- height = 0.21044478 after waiting a day height = 0.2210069 after waiting a day height = 0.2320385 after waiting a day height = 0.24365321 after waiting a day height = 0.25583458 after waiting a day height = 0.26863134 after waiting a day height = 0.2820916 after waiting a day height = 0.2962052 after waiting a day height = 0.3110312 after waiting a day height = 0.32654363 after waiting a day ---------- height = 0.45959333 after waiting a week height = 0.42838797 after mowing to max height 2 height = 0.6464658 after waiting a week height = 0.56931674 after mowing to max height 2 height = 0.9095441 after waiting a week height = 0.73651487 after mowing to max height 2 height = 1.2797776 after waiting a week height = 0.9237885 after mowing to max height 2 height = 1.8006048 after waiting a week height = 1.1202614 after mowing to max height 2 height = 2.5326838 after waiting a week height = 1.3132799 after mowing to max height 2 height = 3.5649536 after waiting a week height = 1.4900717 after mowing to max height 2 height = 5.0168314 after waiting a week height = 1.6407142 after mowing to max height 2 height = 7.061061 after waiting a week height = 1.7606539 after mowing to max height 2 height = 9.935806 after waiting a week height = 1.8493849 after mowing to max height 2 |
More questions to answer in your Readme
file:
ReboundLawn
does not override most of Lawn
's
methods. So Lawn
's original methods will get called.
But those methods were designed to work with an array full of
Grass
es, not ReboundGrass
es. So why
don't they crash now?
ReboundLawn
would not rebound; it
would act just like an ordinary Lawn
. Explain.
You may have noticed one really ugly thing about the code so far:
you have 4 classes with nearly identical main
test
functions!
In lecture, I said that you should strive to eliminate duplicate code from your program. If you are duplicating code, it signals to you that you've missed something in the design. There must be some important concept that you have not packaged up as a method or class. Duplicate code is also dangerous, because you or a colleague might change one copy and forget to change the others.
In this case, what we are missing is the notion that all four
classes have a common interface. They all support
grow
, mow
, and height
methods.
That is why we test them all in pretty much the same way.
Here is one way that we could use Java to express this common interface. (See section 2.4 of the textbook.)
public interface Vegetation { // all interface methods are always "abstract" and "public" // (in fact we are allowed to leave out those keywords) public abstract float height(); public abstract void grow(int days); public abstract void mow(double height); } public class Grass implements Vegetation { ... } public class Lawn implements Vegetation { ... } /* We don't need to say that ReboundGrass and ReboundLawn also implement Vegetation. That is automatic since they are just specialized kinds of Grass and Lawn: */ public class ReboundGrass extends Grass { ... } public class ReboundLawn extends Lawn { ... } |
The above declarations make a commitment that any
Vegetation
object must implement height
,
mow
, and grow
methods. We can then write
a shared test
function, like this:
/* This function can be called on a blade of Grass, a Lawn, a ReboundGrass, or a ReboundLawn! They all support the methods we need. */ public void test(Vegetation v) { ... v.grow(7); v.mow(2); System.out.println(v.height()); ... } |
But what class should test
be defined in?
Aesthetically, we would like test
to be a method of
Vegetation
. But if Vegetation
is an
interface, it can't have any methods at all (not even
static
ones).
A solution is to define Vegetation
as an abstract
class. Abstract classes are almost exactly like interfaces, but they
are more powerful because they are allowed to have their own methods.
(They are also less powerful because Java does not allow multiple
inheritance from classes, just from interfaces.) Here's the
definition, which you can just copy if you promise to study it:
public abstract class Vegetation { public abstract float height(); public abstract void grow(int days); public abstract void mow(double height); public void test() { // may as well make it an instance method System.out.println("height = "+height()+" initially"); System.out.println("----------"); for (int i=0; i < 10; i++) { grow(1); System.out.println("height = "+height()+" after waiting a day"); } System.out.println("----------"); for (int i=0; i < 10; i++) { grow(7); System.out.println("height = "+height()+" after waiting a week"); mow(2); System.out.println("height = "+height()+" after mowing to max height 2"); } } } public class Grass extends Vegetation { ... } public class Lawn extends Vegetation { ... } |
Note: Since interfaces and abstract classes both contain
undefined ("abstract") methods, you can't say v = new
Vegetation()
, since then what would v.mow(2)
do?
You have to specifically say v = new Grass
or v =
new ReboundLawn
-- any object you create has to have all its
required methods defined. But this more specific object that you
create is certainly an instanceof Vegetation
, so you can
perform any action on it that is allowed for Vegetation
objects. In particular, you can run test
on it as well
as grow
and mow
.
Your final task in this assignment is to replace each of the four
ugly main
methods with something simple that just uses
Vegetation.test
to do the work that all the tests have
in common. I'll give you one of them for free:
public static void main(String args[]) { System.out.println("Planting lawn of 1,000,000 blades of height about 0.2"); System.out.println("Note: We will report average height."); Lawn l = new Lawn(1000000, 0.2); l.test(); // method inherited from Vegetation } |
Check that everything works as before, and you're done!
For a bit of fun and extra credit, extend these classes to do
something else interesting, and tell us about it in your
Readme
file. For example, maybe weather and fertilizer
should have an effect on the growth rate. Or you could allow a lawn
to contain weeds that compete with the grass. Another possibility is
to use Java's graphics classes to plot the height of the lawn over
time.
jason@cs.jhu.edu
- $Date: 2004/02/02 07:42:02 $