600.226 Data Structures (Spring 2004 - Prof. Eisner)

Homework 1 ... due 4pm Friday 6 Feb 2004

Summary

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.

How to hand in your work: instructions, upload page. Make sure to name your partner (if you had one) at the top of your Readme file.

What to hand in:

Practice (optional)

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.

Introduction

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.

A Blade of Grass

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:

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.)

Here are some questions to answer in the Readme file you will submit:

  1. In the code, constant AVG_DAILY_GROWTH is declared static. What design decision does this represent?
  2. Which of the following is the best way to make a blade of grass grow for 3 days?
    1. Use a loop to make it grow for 1 day, then 1 day, then 1 day.
    2. Make it grow only once, but use a triple growth rate of 3 * AVG_DAILY_GROWTH * randomMultiplier().
    Explain your answer. (This is more of a math question than a Java question, but it emphasizes the need to think carefully when you program!)
/** 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

A Whole Lawn

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:

  1. Why doesn't the grow method for Grass override the grow method for Lawn?
  2. The height of the lawn is only about 0.46 the first time we mow it to height 2. So why does mowing have any effect?
  3. The lawn gets taller and taller over time even once you start mowing it regularly. Why?
  4. What will the lawn eventually look like if we continue mowing it every week?

The Grass Came Back, the Very Next Day ...

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:

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:

  1. 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 Grasses, not ReboundGrasses. So why don't they crash now?
  2. If Java didn't have dynamic method lookup for instance methods, a ReboundLawn would not rebound; it would act just like an ordinary Lawn. Explain.

Cleaning Up the Code

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!

Extra Credit (optional)

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 Eisner - jason@cs.jhu.edu - $Date: 2004/02/02 07:42:02 $