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
Experiment with the other fish display classes mentioned in
MBSApp.
Experiment with creating and populating new grids manually,
if you haven't already. What do you think is the connection
between the "grid editor" window (adding new objects manually)
and the code in 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.
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
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?
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?)
Look at the class documentation for the Grid Package
Direction class. What methods does it have
that return Direction objects?
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
NorthSouth
East
What locations would you expect in the ArrayList returned by a
call to grid.neighborsOf(loc1) after this code segment?
Optional Programming Exercise: Location and Direction
Write a simple driver program (similar to the one in
SimpleMBSDemo1) that constructs a
BoundedGrid environment and then
test your answers to the Analysis Questions above. The
ArrayList, Location, and
Direction classes all implement the
toString method to provide useful information,
so you can use System.out.println to see the values
you are changing.
In your driver program, use the inDegrees method
from the Direction class to discover the degree
representations for the Direction constants
NORTH, SOUTH, EAST, and
WEST. What is the value of dir3 in
degrees?
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.
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
Where have you seen client code of the Fish class?
Where have you seen client code of the Simulation
class?
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
Look over the code for the Fish class.
How can you tell that nextAvailableID is a class
variable?
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?
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
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?
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
subclassinherits 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
GridObjecttoString 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
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 FishtoString 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.
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
Can a fish (or other grid object) exist outside of a grid?
Look at the methods in the GridObject class to
substantiate your answer.
Consider the following variable definitions.
Location loc1 = new Location(7, 3);
Location loc2 = new Location(2, 6);
Location loc3 = new Location(7, 3);
What does the expression loc1 == loc1 evaluate to?
What about loc1.equals(loc1)?
What does the expression loc1 == loc2 evaluate to?
What about loc1.equals(loc2)?
What does the expression loc1 == loc3 evaluate to?
What about loc1.equals(loc3)?
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.)
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
Why does the Fish class need an
emptyNeighbors method? Why
doesn’t nextLocation just call the
neighborsOf method from the
BoundedGrid class?
How many neighboring locations would you expect there to be in
the list generated by emptyNeighbors? Does it
always return the same size list?
Consider the following bounded grids containing fish.
How many neighboring locations would the
BoundedGridneighborsOf method
return for location (1, 0) in each grid?
How many neighboring locations would
emptyNeighbors return for the fish in
location (1, 0) in each grid?
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)?
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.
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
Look at the class documentation for the MBS Fish
class and the
ColorBlock, and TextCell classes in
the Grid
Package.
How can you tell whether they extend another class?
How deep is the inheritance "family tree" for these
classes?
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.
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?
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.
Look at the class documentation for the MBSGUI
class in the mbsgui folder. How deep is its
inheritance "family tree"?
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?
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?