Before we can ask the Java interpreter to build really complex things for us we first have much to learn about its capabilities. We need to know what it already understands before we can help it to understand more.
So to learn more about our tools, let's build a lamp.
Every lamp must be able to at least turn on and turn off,
and know whether it's on or off.
To model the lamp's behavior we need methods to turn it on and off.
To model the lamp's state (that of being on or off)
we need a variable, say, lampIsOn
,
to remember its current state.
Since this variable only needs two states
it's reasonable to make it a boolean
,
with the understanding that when it's true
, the lamp is on,
and when it's false
, the lamp is off.
The program is in two parts. First, here is a class defining what it means to be a lamp:
class Lamp
{
/*
Define a lamp that can turn on and off.
*/
//this lamp is either on or off
boolean lampIsOn;
void turnOn()
{
/*
Turn this lamp on.
*/
lampIsOn = true;
}
void turnOff()
{
/*
Turn this lamp off.
*/
lampIsOn = false;
}
}
And here is a class that uses such lamps:
import Lamp;
class FiddleWithLamps
{
/*
Create some lamps and fiddle with them.
*/
public static void main(String[] parameters)
{
Lamp kandinsky;
kandinsky = new Lamp();
kandinsky.turnOn();
kandinsky.turnOff();
kandinsky.lampIsOn = true;
Lamp bauhaus;
bauhaus = new Lamp();
bauhaus.turnOn();
bauhaus.turnOff();
}
}
Programs and Classes
Unlike our first program, FredsScript
,
this program has two classes, rather than only one.
Further, class Lamp
doesn't have a main()
method,
while class FiddleWithlamps
does.
Every Java program we write must have at least one class
where we declare a main()
method as follows:
public static void main(String[] parameters)
As we've seen, such a method is Act I, Scene I for the whole play.
Not every class, however, needs a main()
method.
For example, if class A
objects
can create class B
objects,
and if class B
objects
can create class C
objects,
then neither class B
nor class C
needs a main()
method.
Class A
may not need one either,
if yet other classes of objects can create objects of its class.
Writing a Java program is like writing a play script
then separating each actor's lines and stage directions into separate chunks.
These chunks are Java classes;
they become the actors' separate scripts.
Each such script defines a role that an actor may portray,
and we can create as many actors as we wish to play the same role.
Each such actor will follow its script exactly.
Importing Classes
When defining a class that needs other classes to function,
we should precede the class definition
with a statement specifying any other class, or classes, the class needs.
We signal that to the Java interpreter with the word import
.
Class FiddleWithLamps
needs class Lamp
to work properly, so we should import
class Lamp
,
like so:
import Lamp;
This statement alerts the Java interpreter that FiddleWithLamps
uses Lamp
objects.
Such import
statements
must appear before the definition of the class
that needs the classes we're import
ing.
Controlling Access
Class Lamp
isn't a good model of a real lamp.
For example, objects can turn a Lamp
object on or off
in two different ways.
One object could ask a lamp to execute its turnOn()
method,
while another could set the object's lampIsOn
variable
to false
.
The first object thinks the lamp is on, the second thinks it's off.
Which is right?
Altering the lamp's lampIsOn
variable
without going through either of the two methods
seems like hotwiring a car rather than using its key.
We can ensure that no non-Lamp
object
can turn a lamp on or off that way
by making its lampIsOn
variable private
.
While we're at it, let's make both Lamp
methods
public
.
This guarantees that any object that can create a lamp
can also ask that lamp to execute either of its Lamp
methods.
class Lamp
{
/*
Define a lamp that can turn on and off.
*/
//this lamp is either on or off
private boolean lampIsOn;
public void turnOn()
{
/*
Turn this lamp on.
*/
lampIsOn = true;
}
public void turnOff()
{
/*
Turn this lamp off.
*/
lampIsOn = false;
}
}
Objects of classes that use Lamp
objects
now cannot access a lamp's copy of its lampIsOn
variable
(it's private
),
but they can always ask the lamp
to execute its turnOn()
and turnOff
methods
(they're public
):
import Lamp;
class FiddleWithLamps
{
/*
Create some lamps and fiddle with them.
*/
public static void main(String[] parameters)
{
Lamp kandinsky;
kandinsky = new Lamp();
kandinsky.turnOn();
kandinsky.turnOff();
//kandinsky.lampIsOn = true; //this is now illegal
}
}
Understanding Privacy in Java
It's tempting to think that making a prop private
asks the stage manager to make each copy of the prop
private to its own specific lamp.
This is not so.
It's true that the stage manager won't let any non-Lamp
actor
alter the prop,
but it will still let every Lamp
actor alter the prop.
So, in terms of privacy,
the best we can do is ask the stage manager to not let
actors following other scripts alter a particular prop
that we define in this script.
So the statement:
private boolean lampIsOn;
declares that only Lamp
objects
can access the boolean
variable lampIsOn
.
We can also make methods private
, and with the same effect.
Normally, though, it's better to ask the stage manager
to let any actor at all request a lamp to execute a particular method,
whether the actor obeys the Lamp
script or not.
We do that using the word public
, like so:
public void turnOn()
It's good style to make all variables private
and all methods public
.
Later, though, we'll discover some small modifications to that rule.
Reporting State
Now that we've made every copy of lampIsOn
private
,
no non-Lamp
object can find out the internal state of any lamp.
The effect is to complete seal every lamp.
Except for another lamp,
the only object that can know whether a particular lamp is on or off
is the same object that turned the lamp on or off in the first place.
If other objects need to know whether a particular lamp is on or off
we need some way for a lamp to report its internal state.
Let's add a new public
method to class Lamp
so that any object can query a lamp
to find out whether it's currently on or off:
class Lamp
{
/*
Define a lamp that can turn on and off
and report its state.
*/
//this lamp is either on or off
private boolean lampIsOn;
public void turnOn()
{
/*
Turn this lamp on.
*/
lampIsOn = true;
}
public void turnOff()
{
/*
Turn this lamp off.
*/
lampIsOn = false;
}
public boolean isOn()
{
/*
Report whether this lamp is on or off.
*/
return lampIsOn;
}
}
The new method, isOn()
,
returns a boolean
value,
so we must declare its return type as boolean
,
just as we had to declare the return type of
divideApplesAmongPeople()
as double
.
The other two methods don't return values,
so we must declare them void
,
just as we declare main()
.
Encapsulating Lamps
It may seem silly to make the lampIsOn
variable
private
then turn around and add a method to report its state,
but doing it this way gives the lamp
more control over who it reports its state to.
When its variables aren't private
,
any object at all can come along
and alter what should be its internal state.
By protecting its variables and providing well-defined access points to them only through methods, each object takes more control over its own state, and so makes it less likely that another object could incorrectly alter its state.
This encapsulation idea isn't that important
when the program is very small,
but it becomes more and more important
as the program grows larger and more complicated.
The more complex a program becomes,
the more likely it is that someone, somewhere, will goof.
The more protection we can put in to prevent that goof,
the more likely it is that the goof won't harm the program's users.
Using boolean
Expressions
So far, we've seen two if
statements in FredsScript
:
if (numberOfPeople <= 0)
return 0;
and
if (fred.finishedProcessing == false)
return;
The general form:
if (booleanExpression)
[some statement]
is all one statement, possibly spread over several lines,
and with no separate semicolon to terminate it.
It asks the Java interpreter to evaluate the booleanExpression
,
and if it is true
then to execute the enclosed statement
(the statement immediately following the if
),
otherwise to jump past the whole if
statement
and continue execution with the subsequent statement, if any.
We can also select which of two statements we execute next using a statement of the following form:
if (booleanExpression)
[some statement]
else
[some other statement]
This says that if the booleanExpression
is true
we execute the first enclosed statement next,
otherwise we execute the second enclosed statement next.
Just as with the first kind of if
statement,
an if-else
statement is all one statement
even though it's continued over several lines
and doesn't have its own separate semicolon to terminate it.
As with class and method definitions,
the Java interpreter can figure out where it ends for itself
since it's all in one fixed format.
We can use the if-else
statement
to ask objects of the new version of class Lamp
if they're on or not:
import Lamp;
class FiddleWithLamps
{
/*
Create some lamps and fiddle with them.
*/
public static void main(String[] parameters)
{
Lamp kandinsky;
kandinsky = new Lamp();
kandinsky.turnOn();
kandinsky.turnOff();
if (kandinsky.isOn() == true)
System.out.println("kandinsky is on.");
else
System.out.println("kandinsky is off.");
}
}
Reporting State Changes
Our lamps now respond when other objects ask them their current state, but they don't tell anybody when they change state. One day, though, we might decide to display our lamps on the screen. If so, we'll need some way for them to signal the object displaying them that they are changing state. Let's add that capability next:
class Lamp
{
/*
Define a lamp that can turn on and off,
report its state, and report its state changes.
*/
//this lamp is either on or off
private boolean lampIsOn;
public void turnOn()
{
/*
Turn this lamp on.
*/
lampIsOn = true;
reportState();
}
public void turnOff()
{
/*
Turn this lamp off.
*/
lampIsOn = false;
reportState();
}
public boolean isOn()
{
/*
Report whether this lamp is on or off.
*/
return lampIsOn;
}
public void reportState()
{
/*
Report this lamp's status.
*/
if (lampIsOn == true)
System.out.println("I am on.");
else
System.out.println("I am off.");
}
}
Requesting Methods Within Methods
Having lamps report their state changes brings up a puzzle.
After altering the lampIsOn
variable,
both of Lamp
's new
turnOn()
and turnOff()
methods
now request execution of the reportState()
method.
As we've seen, however, we can only execute a method
by asking an object of the class we define the method in
to execute the method.
So when an object is executing its turnOn()
method,
what object is it asking to execute the reportState()
method?
The answer is the same object that is currently executing
the turnOn()
method.
That object is asking itself
to execute its own reportState()
method.
The stage manager makes sure that each of our method execution requests
is a request to some particular object to execute the method.
If we don't specify an object to execute the method,
the stage manager secretly puts a self-reference
in front of the method execution request.
That self-reference is the word "this
".
It's the same as saying that whoever is presently executing the method
should also be the one to execute the new method request.
Think of this
as short for
theObjectExecutingThisStatement
.
There is no such thing as an actorless action in Java.
An actor executes its scenes only when asked to by another actor
(or by itself, or by the stage manager).
So what looks like a bare request
to execute some generic reportState()
method
is a normal method execution request to an object,
just as we're used to so far.
In other words, the statement:
reportState();
is exactly the same as the statement:
this.reportState();
An actor executing a scene containing this statement
is asking itself to execute its reportState()
scene next.
The same is true for accessing variables.
Thus, the following two methods are exactly the same:
|
|