Understanding the Code

After Jamie introduced the four classes (driver, Simulation controller, BoundedGrid, and Fish) that form the conceptual core of the MBS design, I mentioned that I had already looked at the code for the SimpleMBSDemo2 driver and Simulation classes. I felt ready to move on, but Jamie just laughed and suggested we look at the real driver: MBSApp. I was about to discover what that laughter was about.

The MBSApp Class

Read the code for MBSApp.java as you read this section!

I had already noticed that SimpleMBSDemo2 had slightly less code than SimpleMBSDemo1, but I wasn't prepared for the sight of MBSApp. This class has only four lines of code, none of which construct a grid, a Simulation object, nor any fish. Jamie explained that all that this driver does is set up the graphical user interface. Its first two statements combine to specify what types of objects can be put in the grid (only Fish objects, for now), the third says how to display a fish, and the fourth finally constructs the user interface.

The user interface then constructs the grid and fish when the user chooses an initial configuration file or constructs and edits a grid manually. It also plays the role of the simulation controller, managing the run and step functions in response to button clicks on the user interface.

The graphical user interface provides nice functionality for the simulation, but I was glad that I had looked at the SimpleMBSDemo2 driver and its Simulation class first, so I could better understand the overall program logic.

Exercise: MBSApp

The BoundedGrid Class (Grid Package)

Now that I had looked at both the SimpleMBSDemo2 and MBSApp drivers, I was ready to learn more about BoundedGrid and Fish. The code for the BoundedGrid class is buried in the Grid Package, though, so it would be a "black box" for me.

The best reference for the Grid Package classes, including the BoundedGrid class, is the class documentation. To get me started, Jamie gave me a list of useful BoundedGrid methods, including the constructor I had seen used in the demo programs.

These methods are also documented on the MBS Quick Reference Sheet.
public BoundedGrid(int rows, int cols) Constructs an empty grid with the specified dimensions.
Accessor methods dealing with grid dimensions, locations, and navigation
public int numRows() Returns number of rows in grid.
public int numCols() Returns number of columns in grid.
public boolean isEmpty(Location loc) Returns true if loc is a valid location in this grid and is empty; otherwise returns false.
public boolean isValid(Location loc) Returns true if loc is a valid location in this grid; otherwise returns false.
public Direction randomDirection() Generates a random direction.
public Direction getDirection(Location fromLoc, Location toLoc) Returns the direction from one location to another.
public Location getNeighbor(Location fromLoc, Direction compassDir) Returns the adjacent neighbor of a location in the specified direction.
public ArrayList neighborsOf(Location ofLoc) Returns a list of the neighbors to the north, south, east, and west of the given location
Methods to add/retrieve/remove objects
public void add(GridObject obj, Location loc) Adds the specified object to the grid at the specified location.
public GridObject objectAt(Location loc) Returns the object at location loc
public void remove(Location loc) Removes whatever object is at the specified location in this grid.

Some of these methods are used by the driver to construct the environment, some (such as numRows, numCols, objectAt) are used by the graphical user interface to display the contents of the grid, and some (such as neighborsOf, isEmpty) are used by the Fish class when fish are attempting to move.

The Location and Direction Classes (Grid Package)

The Location class, commonly used with a BoundedGrid, provides a way to specify the row and column of a cell in the grid. The Location constructor takes a row and a column as parameters and creates a single object encapsulating them, while the row() and col() methods provide access to the individual attributes.

public Location(int row, int col) Constructs a location object with the specified row and column.
public int row() Returns the row associated with this location.
public int col() Returns the column associated with this location.

The Direction class represents compass directions, providing several constants, such as Direction.NORTH and Direction.SOUTH, and a small group of methods.

Analysis Questions: Location and Direction

  1. Look over the list of 15 edu.kzoo.grid classes in the Grid Package class documentation. (You will have to click on the edu.kzoo.grid package name in the upper-left panel to bring the number of classes down from 75 to 15.) The Marine Biology Simulation primarily uses the BoundedGrid, Location, Direction, and GridObject classes. Which other classes have you used in previous labs and projects?
  2. Look at the class documentation for the Grid Package Location class. It has several other methods, in addition to the ones mentioned by Jamie. What do the equals and compareTo methods do? What about the toString method? (You could run the simulation program and hover the mouse over a fish. What do you see? Does this seem to be related to the Location toString method?)
  3. Look at the class documentation for the Grid Package Direction class. What methods does it have that return Direction objects?
  4. Assume that grid is a valid 10 x 10 BoundedGrid object. Consider the following code segment.
        Location loc1 = new Location(7, 3);
        Location loc2 = new Location(7, 4);
        Direction dir1 = grid.getDirection(loc1, loc2);
        Direction dir2 = dir1.toRight(90);
        Direction dir3 = dir2.reverse();
        Location loc3 = grid.getNeighbor(loc1, dir3);
        Location loc4 = grid.getNeighbor(new Location(5, 2), dir1);
    
    West
    North
    grid with highlighted locations
    South
    East
    1. What locations would you expect in the ArrayList returned by a call to grid.neighborsOf(loc1) after this code segment?
    2. What should be the value of dir1?
    3. What should be the value of dir2? of dir3?
    4. What location should loc3 refer to?
    5. What location should loc4 refer to?
(A Markdown template for writing up answers to these Analysis Questions is here.)

Optional Programming Exercise: Location and Direction

The Fish Class

Next, Jamie turned to the Fish class, whose code I needed to understand well in order to modify it effectively.

A Fish object has several attributes, or features. It has an identifying number, its ID, which is useful for keeping track of the different fish in the simulation, especially during debugging. It has a color, which is used when displaying the environment. A fish also keeps track of the environment in which it lives, its location in the environment, and the direction it is facing. The Fish class encapsulates this basic information about a fish with its behavior. A list of methods from the Fish class appears below.

These methods are also documented on the MBS Quick Reference Sheet.
public Fish(Grid env, Location loc, Direction dir) Constructs a fish at the specified location and direction in a given environment. (one of 3 constructors)
Accessor methods
public int id() Returns this fish's ID.
public Color color() Returns this fish's color.
public boolean isInAGrid() Checks whether this object is in a grid.
public Grid grid() Returns the grid in which this grid object exists.
public Location location() Returns this fish's location.
public Direction direction() Returns this fish's direction.
public String toString() Returns a string representing key information about this fish.
Action and modifier methods
public void act() Acts for one step in the simulation.
Protected helper methods used by public methods
protected void addToGrid(Grid grid, Location loc) Adds this object to the specified grid at the specified location. (used indirectly by constructor)
protected void move() Moves this fish in its environment. (used by act method)
protected Location nextLocation() Returns the row associated with this location. (used by move method)
protected ArrayList emptyNeighbors() Finds empty locations adjacent to this fish. (used indirectly by move method)
protected void changeLocation(Location newLoc) Modifies this fish's location and notifies the grid. (used by move method)
protected void changeDirection(Direction newDir) Modifies this fish's direction. (used by move method)

One thing I noticed about the methods labeled "Helper Methods" in this list was that they are declared protected, rather than public or private. I was not familiar with the protected keyword. Jamie told me that it would be useful when I started creating new types of fish, but that in the meantime I could pretend that the helper methods are private. Like private methods, they are internal methods provided to help methods in the Fish class get their job done, and are not meant to be used by external client code.

Constructors and Fields (Instance and Class Variables)

The Fish class actually has three constructors, even though only one is shown in the list above. All three require that the client code constructing the fish specify the environment (grid) in which the fish will live and its initial location in that environment. The second constructor allows the client code to specify the fish’s direction, while the third allows the client code to also specify the color. Rather than repeating the code to initialize a new fish’s state in all three constructors, Jamie took advantage of the special this constructor call, which allows one constructor to call another specifying additional parameters. Jamie only had to write all of the details for Fish construction in the constructor with the most parameters, and then call that constructor from the other two. Those constructors pass random directions and colors for fish whose direction and color were not provided by the client code. The BoundedGrid class has a randomDirection method, which the first Fish constructor uses to randomly choose a direction for the fish. Unfortunately, the Color class does not provide a randomColor method, but the Grid Package provides a NamedColor class which does have a getRandomColor method. Both of the first two Fish constructors call that method to pass a random color to the third constructor.

The client code of a class is code outside of that class that uses its objects and methods. For example, if we have two classes, A and B, and there is code in class A that constructs objects of class B and invokes methods from class B on those objects, then class A is client code of class B. Of course, there might be another class, MainApp, that constructs one or more objects of class A and invokes A's methods; in that case, MainApp would be client code of class A. The code in class A is both client code of class B, and internal code for class A.

Analysis Questions: Client Code

  1. Where have you seen client code of the Fish class? Where have you seen client code of the Simulation class?
  2. Which Fish constructor was called by client code you have seen? How did calling that constructor manage to initialize all of the attributes for the newly constructed fish?
(A Markdown template for writing up answers to Fish-related Analysis Questions is here.)

The third constructor — the one that takes four parameters to specify the grid, fish location, direction, and colors — is responsible for fully initializing the fish. It only actually initializes three instance variables, though, which are all the instance variables defined in the Fish class. Even though a fish must keep track of its ID, its environment, its location in the environment, its direction, and its color, only the ID, direction, and color are stored in instance variables in the Fish class, and initialized directly in the Fish constructor.

The key to understanding this is to recognize that the Fish class is a subclass of the GridObject class from the Grid Package. A GridObject is what the Grid Package expects to store in a grid. After all, the package doesn't know anything about the client code that will use it; will it be storing fish, or color blocks, or physics particles, or marching band members? All it knows is that all objects in a grid need to know their own location in the grid, and they might need to act in some way. The GridObject class encapsulates these basic needs. It has instance variables that know about the grid and the object's location in the grid, and it provides methods to add an object to a grid, report on an object's location in the grid, and change an object's location in the grid. It even provides a placeholder act method (which doesn't actually do anything).

The Fish class extends the GridObject class, including additional instance variables for the fish's ID, direction, and color, and adding accessor methods and methods that allow the fish to move when asked to act. The Fish constructor uses the special super keyword to call the superclass (GridObject) constructor, which initializes the inherited instance variables, then initializes the additional instance variables that are specific to the Fish class. To initialize the myID instance variable, it uses the Fish class variable, nextAvailableID, which is a single integer that all Fish objects have access to. Each fish initializes its own ID to the current value of nextAvailableID and then increments the class variable for the next fish.

Since nextAvailableID is a class variable and not tied to any single object of the Fish class, it must be initialized when it is declared in the class rather than in the Fish constructor.

Exercise and Analysis Questions: Instance and Class Variables

  1. Look over the code for the Fish class. How can you tell that nextAvailableID is a class variable?
  2. Run the simulation program with either of the two demo programs. Hover the mouse over the fish to see their ID numbers? What are they? Is that what you expected?
  3. What would the ID numbers for the those fish be if the nextAvailableID class variable were an instance variable instead? Run the experiment to test your intuition and understanding of class variables.

Analysis Questions: Constructors

  1. Based on what you saw in the fish.dat data file, which Fish constructor do you think is used by the graphical user interface when it reads an initial configuration from a file?
  2. If you haven't already, run the Marine Biology Simulation program and construct and populate the environment manually, rather than from a file. Based on what you see when you manually place new fish in the grid, which Fish constructor do you think is used by this aspect of the graphical user interface?

Inheriting and Redefining Methods

The list of Fish methods above included methods inherited from GridObject as well as methods defined in the Fish class. The list below shows the same methods, but indicates which methods are inherited from GridObject (greyed out), which are new in the Fish class, and which were defined in the GridObject class but are redefined in the Fish class (act and toString).

public Fish(Grid env, Location loc, Direction dir) Constructs a fish at the specified location and direction in a given environment. (one of 3 constructors)
Accessor methods
public int id() Returns this fish's ID.
public Color color() Returns this fish's color.
public boolean isInAGrid() Checks whether this object is in a grid. (inherited from GridObject)
public Grid grid() Returns the grid in which this grid object exists. (inherited from GridObject)
public Location location() Returns this fish's location. (inherited from GridObject)
public Direction direction() Returns this fish's direction.
public String toString() Returns a string representing key information about this fish. (redefines behavior in GridObject)
Action and modifier methods
public void act() Acts for one step in the simulation. (redefines behavior in GridObject)
Protected helper methods used by public methods
protected void addToGrid(Grid grid, Location loc) Adds this object to the specified grid at the specified location. (used indirectly by constructor) (inherited from GridObject)
protected void move() Moves this fish in its environment. (used by act method)
protected Location nextLocation() Returns the row associated with this location. (used by move method)
protected ArrayList emptyNeighbors() Finds empty locations adjacent to this fish. (used indirectly by move method)
protected void changeLocation(Location newLoc) Modifies this fish's location and notifies the grid. (used by move method) (inherited from GridObject)
protected void changeDirection(Direction newDir) Modifies this fish's direction. (used by move method)
Inheritance: A subclass inherits all of the data and behavior (instance variables and methods) of the class it extends (its superclass). It may also add new data and behavior. Finally, it may redefine some of the behavior of its superclass by providing new implementations of those methods in the subclass. (This is also known as overriding the superclass method.)
 
For example, the Fish class inherits a private instance variable called myLoc from the GridObject class, although it can only access it through the inherited location() method. Since the location is private, code in the Fish class can only set or change the location through the inherited addToGrid and changeLocation methods. The Fish class also redefines (overrides) the GridObject toString method to provide more fish-specific information, and redefines the act method to move the fish.

At this point, Jamie told me about the difference between the private and protected keywords. Private instance variables and methods can only be used within the class in which they are defined. Protected instance variables and methods can be used in that class and also in any subclasses. So, the private myLoc instance variable can only be accessed directly within the GridObject class, but the protected addToGrid and changeLocation methods can be accessed by code in the Fish class (or any other subclasses).

Exercise and Analysis Question: Inheriting and Redefining Methods

  1. Run the simulation program and hover the mouse over a fish. What do you see? How is this related to the toString method defined in the Fish class? Temporarily comment-out the toString method in Fish (don't delete it!), and re-compile and run the program. How (and why) has the behavior changed? (Then uncomment the Fish toString method.)

Fish Movement

act: The act method is the key to fish movement, or any other behavior, since it is the only public non-accessor (or non-"observer") method. The act method does almost nothing, though. It checks to make sure the fish is in a grid and, if it is, tells the fish to move.

Illustration of fish in grid

move: The protected move helper method is the real heart of this class. It first looks for a location to which the fish can move, using the nextLocation method. If the fish can’t move, nextLocation returns the current location. If the new location is not the current location (so the fish can move), move calls another helper method, changeLocation, to actually move there. The fish also changes direction to reflect the direction it moved. For example, if a fish facing north at location (4, 7) were to move west to location (4, 6), it would change its direction to west. In pseudo-code, we could write:

  Move method:
        Get next location.  (call nextLocation)
        If next location not equal to current location,
            Move to the next location.  (call changeLocation)
            Change direction if necessary.  (call changeDirection)

The diagram to the right shows the relationship between these methods. The act calls the move method, which calls the nextLocation, changeLocation, and changeDirection methods. The diagram also shows that nextLocation returns a location to the move method, which passes that down to the changeLocation method. The move method passes a direction to changeDirection.

In the actual code, the move method uses the equals method to compare locations rather than == or != operators because it only cares whether the row and column values are the same, which is what the equals method checks, not whether the two locations are exactly the same Location object.

In addition to the code that implements the informal pseudo-code above, the move method and the methods it calls include several calls to Debug.println. The Debug class is a utility class in the Grid Package. Debug.println is like System.out.println, except that the string will only be printed if debugging has been turned on. There are other methods in the Debug class to turn debugging on and off.

Aside: I asked Jamie why the logic for fish movement is broken down into these little subtasks (some of which are broken down still further), rather than putting all of it right in the move method. Jamie said that this is an example of "best practices" in software development. (This one is called the "template method pattern.") Breaking a task into smaller tasks at different levels of abstraction helps the programmer to manage complexity, makes testing easier, and, as I discovered later in my internship, helps to reduce duplication of code across related classes.

Exercises and Analysis Questions: Move Method Details

  1. Can a fish (or other grid object) exist outside of a grid? Look at the methods in the GridObject class to substantiate your answer.
  2. Consider the following variable definitions.
        Location loc1 = new Location(7, 3);
        Location loc2 = new Location(2, 6);
        Location loc3 = new Location(7, 3);
    
    1. What does the expression loc1 == loc1 evaluate to? What about loc1.equals(loc1)?
    2. What does the expression loc1 == loc2 evaluate to? What about loc1.equals(loc2)?
    3. What does the expression loc1 == loc3 evaluate to? What about loc1.equals(loc3)?
  3. Edit either SimpleMBSDemo1 or SimpleMBSDemo2 and add the line

        edu.kzoo.util.Debug.turnOn();
    
    somewhere before your code that causes fish to move. Run the program to see the effects of the Debug.println statements. (If you import edu.kzoo.util.Debug at the top of your demo driver, you can turn on debugging with just Debug.turnOn().)

    (You may then want to comment out the Debug.turnOn() statement or call Debug.restoreState(). Debugging messages can be useful to illuminate what is happening in small sections of code, but they can be overwhelming if they come from every part of a program.)

Illustration of fish in grid

nextLocation: The nextLocation method finds the next location for the fish. The fish the biologists were simulating are equally likely to swim forward or to either side, but virtually never turn completely around to swim backwards. In the simulation, therefore, a fish may move to the cell immediately in front of it or to either side of it, so long as the cell it is moving to is valid (in the environment) and empty. It never moves to the cell behind it.

The first thing nextLocation does, therefore, is to get a list of all the neighboring locations that are in the environment and that are empty. It calls the emptyNeighbors helper method to do this. Then it removes the location behind the fish from the list. Finally, nextLocation randomly chooses one of the valid empty locations and returns it (or returns the current location, if the fish can’t move). The pseudo-code below summarizes the activities of the nextLocation method.

  Next Location method:
        Get list of neighboring empty locations.  (call emptyNeighbors)
        Remove the location behind the fish.
        If there are any empty neighboring locations,
            Return a randomly chosen one.
        Otherwise,
            Return the current location.

emptyNeighbors: In emptyNeighbors, the fish first asks the grid for a list of all its neighboring locations. Since the neighboring locations are not necessarily empty, emptyNeighbors constructs a new ArrayList and copies all the neighboring locations that happen to be empty into that list. When it's done, it returns the new list of empty neighboring locations.

The diagram to the right shows this relationship between the neighborsOf method in the grid, which return a list of all of the neighboring locations (empty or not) to emptyNeighbors, which in turn returns a list of only empty neighbors to nextLocation, which chooses one empty neighbor to be the next location.

Analysis Questions: Next Location and Empty Neighbors

  1. Why does the Fish class need an emptyNeighbors method? Why doesn’t nextLocation just call the neighborsOf method from the BoundedGrid class?
  2. How many neighboring locations would you expect there to be in the list generated by emptyNeighbors? Does it always return the same size list?
  3. Consider the following bounded grids containing fish.

    4 empty neighbor exercises

    1. How many neighboring locations would the BoundedGrid neighborsOf method return for location (1, 0) in each grid?
    2. How many neighboring locations would emptyNeighbors return for the fish in location (1, 0) in each grid?
  4. Are the neighboring locations returned by emptyNeighbors always going to be valid locations? In other words, will they always be locations in the grid, or might it include some invalid locations (outside the bounds of the grid)?
  5. After emptyNeighbors returns to nextLocation, how does the nextLocation method remove the location behind the fish from the list of locations it might move to?
    Note: There are two remove methods in ArrayList. One takes an integer index as a parameter and removes whatever object is at that index; the other takes an object as its parameter and finds and removes the equivalent object from the list.
  6. The Grid Package provides a RandNumGenerator class that is similar to (it actually extends) the java.util.Random class that you have used in previous projects. The difference is that it provides a getInstance method that always returns the same, single random generator object. Anywhere in your code, you can call RandNumGenerator.getInstance() to get a reference to the same generator object, rather than constructing new ones every time you need one. What is the advantage of this? (If you're not sure, you may want to read the class documentation for the RandNumGenerator class, which is in the edu.kzoo.util package.)

changeLocation: Once the move method knows where to move (as determined by the nextLocation method), it calls the changeLocation method it inherits from GridObject to actually move there. The changeLocation method notifies the grid of the change, since the grid keeps track of the locations of all its objects. It is critical that the fish and the grid agree where the fish is. (This is true for any object in the grid.)

changeDirection: If the fish had to turn to move (it did not move forward), move calls the changeDirection method to update the fish's direction. Unlike changeLocation, the changeDirection method is not inherited from GridObject. The Grid Package does not assume that objects in the grid keep track of a direction.

Overall Notes on the Fish Class

Note that many of the methods in the Fish class use the class's accessor methods, such as grid(), location(), and direction(), rather than accessing instance variables directly. This is good programming practice, even though it’s a little harder to read, because it limits the number of places in the code that depend on the internal representation of the object’s data. (It also turned out to be useful later when implementing other types of fish.) Obviously, the accessor methods themselves, and the methods that changed instance variable values, have to access the instance variables directly.

Analysis Questions: Exploring Inheritance

  1. Look at the class documentation for the MBS Fish class and the ColorBlock, and TextCell classes in the Grid Package.
    1. How can you tell whether they extend another class?
    2. How deep is the inheritance "family tree" for these classes?
    3. How can you tell what methods they inherit and what methods they define?
    Remember that the full set of methods for any class is the combination of all inherited and new methods. Also, notice that many method names within the Grid Package are clickable, so you can follow a link to get more information on inherited methods.
  2. The Location class is obviously not a subclass of GridObject (one doesn't place Location objects into the grid). Does it extend some other class? What is its complete list of methods?
  3. Not all of the methods listed above for the BoundedGrid class are actually defined in that class. How many are? What is the inheritance "family tree" for BoundedGrid? Identify which methods come from where.
  4. Look at the class documentation for the MBSGUI class in the mbsgui folder. How deep is its inheritance "family tree"?
    1. Look at the methods inherited from the class right above it in the class hierarchy, and at the methods inherited from the class at the top of the hierarchy. Do any of them look familiar?
    2. The class has almost no methods in it. (You can see this in the class documentation, but you can look at the code, too, if you want.) Where are the methods that would correspond to the step and run functions you saw in the Simulation class?


Alyce Brady, Kalamazoo College