For some problems, it would be convenient if we could extend the basic behaviors of Jeroos (or other objects). Java allows us to write programmer-defined methods that extend the behavior of every object created from a given class.
The concepts of behavior and method were defined in Chapter 1 and are repeated here. A behavior is an action that an object can take or a task that it can perform in response to a request from an external source. A method is a collection of statements that are written in some programming language to describe a specific behavior.
These definitions imply that the creation of a method is a two-part process. First, we need to define and name the new behavior. Second, we need to write the source code for the method.
The first question we must ask is "How do I decide on a good behavior?" There is no fixed answer to this question, but there are some guidelines to follow.
Examine the high-level algorithm. Any complex, but well-defined, step is a candidate for a new behavior, especially if two or more Jeroos need to perform that step.
Examine the detailed-algorithm. Any sequence of steps that occur several times is a candidate for a new behavior.
These guidelines serve as a starting point, but experience is a good teacher. Examine your own programs and those of others. A good behavior has a very clear definition and is used more than once in the program.
A Jeroo method contains the source code that describes what an arbitrary Jeroo needs to do to carry out the corresponding behavior. The form of a Jeroo method is:
The methodIdentifier on the first line (the header line) is a name that the programmer chooses for the method. The name should indicate the corresponding behavior. The rules for creating an identifier for a method are the same as those given in Chapter 2--but remember that we always start method names with a lowercase letter. In every method, we should indent every line between the opening and closing braces.
Since a Jeroo method defines a behavior that applies to every Jeroo, we cannot send a message to a specific Jeroo. Instead, we use the special Java name this, which is like a pronoun that refers to the Jeroo that is performing the entire method.
If we wanted to add a method to cause a Jeroo to turn around, we
need a class to place it in. We have to create our own subclass of
Jeroo
to hold our code. In Greenfoot4Sofia, you can
right-click on the Jeroo
class and create a new subclass
with a name of your own choosing. In that new subclass, you could add
a method to turn the jeroo around:
// ---------------------------------------------------------- /** * Turn the jeroo around 180 degrees so it faces the opposite * direction. */ public void turnAround() { this.turn(LEFT); this.turn(LEFT); }
This example introduces two new behaviors: planting four flowers in a row, and planting two adjacent rows with four flowers per row.
// ---------------------------------------------------------- /** * Plant four flowers in a row, starting at the current location. */ public void plantFour() { this.plant(); // -- one --- this.hop(); this.plant(); // -- two --- this.hop(); this.plant(); // -- three --- this.hop(); this.plant(); // -- four --- } // ---------------------------------------------------------- /** * Plant two adjacent rows of flowers. */ public void plantRowsOfFour() { // --- Plant first row --- this.plantFour(); // --- Move into position for next row --- this.turn(RIGHT); this.hop(); this.turn(RIGHT); // --- Plant second row (in opposite direction) --- this.plantFour(); }
A Jeroo method is used just like any other method. In our island's
myProgram()
method, we just have to be sure to create
a jeroo from our special subclass that contains the new methods we
want to use. Then we send a message to a specific Jeroo object,
requesting that Jeroo to perform the task associated with the method.
As an example, suppose we had created our own Jeroo
subclass called PlantingJeroo
, and added the
plantFour()
and plantRowsOfFour()
methods
to it. Then in our island subclass, we could have a new Jeroo named
Ali plant two rows of flowers, south and east of (5, 5):
public void myProgram() { PlantingJeroo ali = new PlantingJeroo(5, 5, 8); this.add(ali); ali.plantRowsOfFour(); }
We know
that when we create a subclass
that it inherits all of the
methods and attributes from the class that it
extends. If you create a
subclass of Jeroo
called PlantingJeroo
, then
any PlantingJeroo
object can perform all of the methods
that any Jeroo
knows--because a PlantingJeroo
is a special kind of Jeroo
. The PlantingJeroo
class inherits all of
the methods and attributes from the class Jeroo
, and also
understands any new ones you write, such as the
platRowsOfFour()
method. Computer scientists sometimes
call this an is-a relationship,
because every PlantingJeroo
object is a
Jeroo
at the same time--just a Jeroo that can do more.
Also, as we have already read in Chapter
3, a constructor is a special
kind of method that is used to initialize a brand new object. But,
while a subclass automatically inherits all of the (plain) methods
and attributes from its superclass, it does not inherit
constructors. That means that the object instantiation for
Ali in the previous example will not actually compile--unless we
provide an appropriate constructor for our
PlantingJeroo
subclass.
One reason that subclasses do not automatically inherit constructors is because subclasses can add new attributes in addition to new methods, and those attributes must be initialized, no matter what. But any constructor from a superclass won't know anything about the subclass' new attributes and can't initialize them appropriately. So subclasses have to explicitly define every constructor they support, all the time.
Fortunately, while constructors are not inherited, there is a
simple pattern for defining them. In our PlantingJeroo
,
we can add the following constructor:
// ---------------------------------------------------------- /** * Create a new Jeroo facing east. * @param x The x-coordinate of the Jeroo's location. * @param y The y-coordinate of the Jeroo's location. * @param flowers The number of flowers the Jeroo is holding. */ public PlantingJeroo(int x, int y, int flowers) { super(x, y, flowers); }
While we have not yet covered all of the features in this small
piece of code, the gist is straightforward. A constructor is
declared like a regular method, except that we omit the word
void
and its name is exactly the same as the
class name. Here, we are defining a constructor for our
PlantingJeroo
subclass that takes three numbers (integers)
as arguments, representing the x and y coordinates of the Jeroo's
location and the number of flowers in its pouch.
The body of this constructor contains only a single line that uses
the special Java keyword super
. This word can only
be used as the first word inside a subclass constructor, and it allows
us to invoke a superclass constructor, passing it any information it
might need. So here, we are saying that the first (and only) action
in our PlantingJeroo
constructor is to call the
constructor for its superclass (Jeroo
), passing the
x and y coordinates and number of flowers. This allows the superclass
to initialize all of its attributes correctly with the given information.
If our subclass needed more initialization, we would perform that in
following statements in the subclass constructor's body.
But for now, this constructor is enough for our
PlantingJeroo
class. It will allow us to create a
PlantingJeroo
object by specifying its location and
number of flowers. That will in turn allow us to instantiate the
Ali Jeroo in the previous example without problems.
We should always define a behavior carefully before we write the code for the corresponding method. A complete definition for a behavior must include a statement of the preconditions and the postconditions.
A precondition for a method is something that is assumed to be true before the method is invoked. The portion of the code that invokes the method is responsible for ensuring that all preconditions are satisfied before the method is invoked.
A postcondition for a method is something that is true after the method has been executed. The code within the method is responsible for ensuring that all postconditions are met.
The process of determining good preconditions and postconditions can be difficult, but it is easier if we remember a few characteristics of objects and methods.
All work is done by sending messages to objects.
Exactly one object executes a method in response to a message.
A method can modify the attributes of the object that executes the method, but cannot directly modify the attributes of any other object.
One method can send messages to several different objects, and those messages can lead to modifications in their receivers.
Using the previous list of characteristics as a guide, we can use
the following questions as a basis for writing preconditions and
postconditions. When we are working with Jeroos, we need to consider
how a method can change the attributes of the Jeroo object that executes
the method. In some cases, Jeroo actions like pick()
,
plant()
, and toss()
can change the attributes
of the world by adding or removing objects, although we normally don't
send messages to these other object directly. Behind the scenes,
the pick()
, plant()
, and
toss()
methods send appropriate messages to the island in
order to add or remove objects corresponding to the desired behavior.
Precondition Questions | Postcondition Questions |
---|---|
Do any of the attributes of the receiving object need
to have special values?
Location Direction Flowers |
How does this method affect the attributes of the receiving
object?
Location Direction Flowers |
Are the contents of certain island cells important? | Have the contents of any island cells changed? |
The preconditions and postconditions can be created rather informally, but the final versions should be stated in a comment block at the beginning of the source code for the method. As an example, consider the method from the previous section to plant four flowers in a row:
// ---------------------------------------------------------- /** * Plant four flowers in a row, starting at the current location. * * @precondition The three spaces directly ahead of the Jeroo are clear. * @precondition The Jeroo has at least four flowers. * @postcondition The Jeroo has planted four flowers, starting at its * current location and proceeding straight ahead. * @postcondition The Jeroo is standing on the last flower, and facing in * its original direction. */ public void plantFour() { this.plant(); // -- one --- this.hop(); this.plant(); // -- two --- this.hop(); this.plant(); // -- three --- this.hop(); this.plant(); // -- four --- }
The section contains an extended example that demonstrates the algorithm development process, and shows a recommended process for developing source code that contains Jeroo methods.
A Jeroo starts at (4, 1) facing North with 5 flowers in its pouch. There are two nets immediately South of the Jeroo at locations (4, 2) and (4, 3). There is a flower directly South of the second net. Write a program that directs the Jeroo to disable the nets and pick the flower. After picking the flower, the Jeroo should return to its starting location and face South.
Start | Finish |
---|---|
The Jeroo must turn around to locate the first net
Each net is directly South of the previous one
The first net is directly South of the Jeroo
The flower is at location (4, 4)
The Jeroo must finish facing South at location (4, 1)
The Jeroo should finish with 5 - 2 + 1 = 4 flowers
Let's name the Jeroo Kim. Kim should do the following:
Turn around // now at (4, 1) facing South
Disable two nets in a rowTossGet the flower
Hop once // now at (4, 2) facing South Toss
Hop once // now at (4, 3) facing SouthHop once // now on flower at (4, 4) facing SouthGo back to (4, 1) and turn around
PickTurn around // now at (4, 4) facing North
Hop 3 times // now at (4, 1) facing North
Turn around // now at (4, 1) facing South
The high-level algorithm helps manage the details.
We used a "turn around" step in example 4.2. We can use the same logic here.
The act of turning around appears as a step in the high-level algorithm and as part of the "Go back to (4, 1) and turn around" step. Interesting!
"Turn around" is used three times
The sequence "Toss, Hop" is used two times in the "Disable nets" step.
We will create a custom Jeroo
subclass and write a
Jeroo method for each of these behaviors, but first, we need to define
a purpose, preconditions, and postconditions for each method. This can
be done informally, because we will write these things in a comment
block at the beginning of each method.
turnAround()
tossAndHop()
The last postcondition of the tossAndHop()
method simply
says that the Jeroo is facing the direction it was facing at the start
of the method. It does not prohibit the Jeroo from changing direction
during the course of the method as long as the Jeroo returns to its
original direction at the end.
As before, we should develop the code as a series of builds. To
start this process, create a new scenario using Greenfoot4Sofia and
select the Jeroo palette from the Edit
Palettes menu. Then
right-click on the island class and create a new subclass called
ClearNetsAndPick
for this example. Also, create a
new subclass of Jeroo
called ClearingJeroo
to hold your Jeroo methods.
Once you have these classes created, make sure they are compiled
and then right-click on your ClearNetsAndPick
class and
create an instance of it, which will then fill the world view. Then
right-click on the flower and net classes to create new objects and
drop them at the appropriate starting locations on the island. Finally,
right-click on an unoccupied cell on the island and choose "Save the
world" from the popup menu in order to save the starting configuration
as part of your ClearNetsAndPick
class.
The recommended first build contains three things:
The myProgram()
method in your island subclass
that creates and sends messages to the Jeroo.
Declaration and instantiation of every Jeroo that will be
used. This implies adding an appropriate constructor to our
Jeroo
subclass.
The high-level algorithm in the form of comments.
Skeletons for each of the Jeroo methods in your Jeroo subclass. These skeletons are often called stubs.
The myProgram()
method goes inside your
ClearNetsAndPick
class:
public void myProgram() { ClearingJeroo kim = new ClearingJeroo(4, 1, NORTH, 5); this.add(kim); // --- Turn around --- // --- Disable nets --- // --- Get the flower --- // --- Go back to (4, 1) and turn around --- }
An appropriate constructor and the new Jeroo methods go inside your
ClearingJeroo
class:
// ---------------------------------------------------------- /** * Create a new Jeroo. * @param x The x-coordinate of the Jeroo's location. * @param y The y-coordinate of the Jeroo's location. * @param direction The direction the Jeroo is facing. * @param flowers The number of flowers the Jeroo is holding. */ public ClearingJeroo(int x, int y, CompassDirection direction, int flowers) { super(x, y, direction, flowers); // Let the superclass initialize these } // ---------------------------------------------------------- /** * Turn the jeroo around 180 degrees so it faces the opposite * direction. * * @precondition None. * * @postcondition The Jeroo has turned 180 degrees. * @postcondition The Jeroo is at the same location. */ public void turnAround() { } // ---------------------------------------------------------- /** * Disable a net and move to the newly cleared location. * * @precondition There is a net ahead. * @precondition The Jeroo has at least one flower. * * @postcondition The net has been disabled. * @postcondition The Jeroo has one less flower. * @postcondition The Jeroo is at the location originally occupied by the net. * @postcondition The Jeroo has not changed direction. */ public void tossAndHop() { }
This build finishes the turnAround()
method and uses it
in the myProgram()
method. It would be wise to test this
method four times, each time start with Kim facing in a different
direction. Once we are comfortable that this method works correctly, we
can proceed with the next build.
In the ClearNetsAndPick
class:
public void myProgram() { ClearingJeroo kim = new ClearingJeroo(4, 1, NORTH, 5); this.add(kim); // --- Turn around --- kim.turnAround(); // new code // --- Disable nets --- // --- Get the flower --- // --- Go back to (4, 1) and turn around --- }
In the ClearingJeroo
class:
// ---------------------------------------------------------- /** * Turn the jeroo around 180 degrees so it faces the opposite * direction. * * @precondition None. * * @postcondition The Jeroo has turned 180 degrees. * @postcondition The Jeroo is at the same location. */ public void turnAround() { this.turn(LEFT); // new code this.turn(LEFT); // new code } // ---------------------------------------------------------- /** * Disable a net and move to the newly cleared location. * * @precondition There is a net ahead. * @precondition The Jeroo has at least one flower. * * @postcondition The net has been disabled. * @postcondition The Jeroo has one less flower. * @postcondition The Jeroo is at the location originally occupied by the net. * @postcondition The Jeroo has not changed direction. */ public void tossAndHop() { }
This build finishes the tossAndHop()
method and uses it
in the myProgram()
method. Our focus is on destroying the
two nets.
In the ClearNetsAndPick
class:
public void myProgram() { ClearingJeroo kim = new ClearingJeroo(4, 1, NORTH, 5); this.add(kim); // --- Turn around --- kim.turnAround(); // --- Disable nets --- kim.tossAndHop(); // new code kim.tossAndHop(); // new code // --- Get the flower --- // --- Go back to (4, 1) and turn around --- }
In the ClearingJeroo
class:
// ---------------------------------------------------------- /** * Turn the jeroo around 180 degrees so it faces the opposite * direction. * * @precondition None. * * @postcondition The Jeroo has turned 180 degrees. * @postcondition The Jeroo is at the same location. */ public void turnAround() { this.turn(LEFT); this.turn(LEFT); } // ---------------------------------------------------------- /** * Disable a net and move to the newly cleared location. * * @precondition There is a net ahead. * @precondition The Jeroo has at least one flower. * * @postcondition The net has been disabled. * @postcondition The Jeroo has one less flower. * @postcondition The Jeroo is at the location originally occupied by the net. * @postcondition The Jeroo has not changed direction. */ public void tossAndHop() { this.toss(); // new code this.hop(); // new code }
This build finishes the myProgram()
method. We need
to check to see that Kim has the correct number of flowers at the
end.
In the ClearNetsAndPick
class:
public void myProgram() { ClearingJeroo kim = new ClearingJeroo(4, 1, NORTH, 5); this.add(kim); // --- Turn around --- kim.turnAround(); // --- Disable nets --- kim.tossAndHop(); kim.tossAndHop(); // --- Get the flower --- kim.hop(); // new code kim.pick(); // new code // --- Go back to (4, 1) and turn around --- kim.turnAround(); // new code kim.hop(3); // new code kim.turnAround(); // new code }