The whole trick behind elegant programming is to take some simple stuff and some simple operators defined for that stuff and make more complex stuff out of it. Then we take that more complex stuff and make yet more complex stuff out of it. We keep building on what we have, creating ever more complex stuff, until the stuff is complex enough to express a solution to our present problem simply, clearly, beautifully.
So far we've seen ways to link statements into methods,
to link operators and operands into expressions,
and, of course, to link methods and variables into classes.
Soon we'll learn how to link symbols into String
s,
variables into arrays,
and classes into packages.
Before that, though, we should first find out more about linking statements;
that is the armature that makes everything else possible.
We'll start with simple grouping.
Grouping Statements
Methods aren't the only way we can group statements.
We can group any sequence of statements
with braces ({}
) to form blocks of statements.
Wherever we can put a single statement in a method,
we can put an entire block of statements.
A block is almost the same as a method except that it has no name,
which means it has no type.
Further, because blocks are nameless,
an actor can only execute them when it gets to them sequentially,
which means blocks can't have return
statements, either.
Lacking a name for a block,
we can't ask an actor to execute it at any time,
or return from it at any time,
as we can with a method.
Inside a block we can declare variables
and ask actors to do any computations we wish,
just as if they were inside a method.
Further,
since wherever we can put a single statement we can put an entire block,
we can put one block inside another,
and another inside that subblock,
and so on for as long as we want.
Blocks and Scope
Declaring a variable in a block makes the extent of that block its scope, so we can access it in any subblocks of that block, but not outside the block we declare it in. Once we declare a variable in any block the Java interpreter won't let us redeclare it in a subblock.
For example,
class Blocks
{
/*
This is the start of a class;
it is also the start of a block.
*/
private int firstVariable;
public void demonstrateBlocks()
{
/*
This is the start of a method;
it is also the start of a block.
*/
//Although we declared firstVariable
//in an outer block (the class this method
//belongs to), we can still access it inside
//this block; its scope is global to all blocks
//in the class.
firstVariable = 0;
int secondVariable;
//Declaring secondVariable here makes it accessible
//in this block (that is, this method), and inside
//any subblocks we define inside this method;
//its scope is global to all subblocks of this method.
secondVariable = 1;
{
/*
This is the start of a block within the block
defining the method, which is a block inside
the block defining the class.
*/
int thirdVariable;
thirdVariable = 2;
//We can still access firstVariable
//and secondVariable here since we declared
//them in outer blocks of this block,
//so they are still within their scopes.
System.out.println(firstVariable);
System.out.println(secondVariable);
System.out.println(thirdVariable);
firstVariable = firstVariable + 10;
secondVariable = secondVariable + 10;
}
//Although we altered secondVariable in a subblock
//of this block, it still has its altered value now.
//It is still within its scope.
//Because we declared thirdVariable in a subblock
//of this block, is no longer accessible.
//It is out of its scope.
System.out.println(firstVariable);
System.out.println(secondVariable);
}
public static void main(String[] parameters)
{
/*
This is the start of yet another block.
Neither secondVariable nor thirdVariable exist
in this block. They are out of their scope.
firstVariable doesn't exist either, even though
it's within its scope; it needs an object to exist
in and this is a class method so there is no object
to hold it. We could only use it directly here if
it were a static (or class) variable. We could also,
of course, create a Blocks object and access that
object's firstVariable.
*/
Blocks blocks;
blocks = new Blocks();
blocks.demonstrateBlocks();
blocks.firstVariable = 100;
System.out.println(blocks.firstVariable);
}
}
In general, a more local variable hides a more global variable.
This is true whether the "more local" variable is a method parameter and the
"more global" variable is global to all methods in the class,
or whether "more local" means a variable we declare
inside a subblock of a method
and "more global" means a variable we declare inside the method as a whole.
We will see other examples of this general rule later.
Classes, Methods, Blocks, and Scope
Like a method, a class is also a named block of statements since we must name it and define it inside braces. (Later we'll see that we needn't name a class if we don't want to.) However, while we can ask an object to execute one of its methods, we can't ask an object to execute a class.
Further, although we can nest blocks one inside another as much as we want, and, as we shall see later, we can nest classes inside other classes, we can't nest methods inside other methods.
Finally, any variables we define inside methods (or subblocks of methods)
can be final
but they cannot be private
or public
the way that variables we define inside classes as a whole can be.
Variables we declare inside methods are transient,
they only exist while some object is executing the method.
Other objects can never see them at all.
Variables we declare in the class as a whole, though, are permanent;
once an object exists, it will always have its copies of those variables.
The Five Statement Linkers
To help us generate more complex requests from the simple requests we start with, the stage manager lets us sequence, select, group, name, and repeat statements. These five mechanisms let us link statements to make the performance of some dependent on the outcome of others. The linking statements determine when, which, if, and how many times, other statements should be executed.
We've already seen examples of four of the five mechanisms.
First, the stage manager executes our requests sequentially, unless we ask it to do otherwise with other linking mechanisms, so everything happens in sequence naturally.
Second, the stage manager lets us select one statement,
or choose between two statements,
using if
or if-else
statements.
Third, the stage manager lets us group a collection of statements into a block or method.
Fourth, the stage manager lets us name a method.
Naming it lets us ask objects to execute it with one request.
That's essentially what a method is:
a unified, named action made up of a collection of simpler actions.
To that capability it adds a way for us to ask objects to execute their methods
(by sending messages)
and to stop executing those methods
(the return
statement).
The fifth linking mechanism is repetition:
we need a way to repeat statements.
Repeating Statements
We can repeat a statement or block of statements
with a while
statement:
while (booleanExpression)
[statement or block]
An actor executing the while
statement
will repeat the enclosed statement
for as long as booleanExpression
is true
.
As soon as booleanExpression
becomes false
execution continues with the next statement
following the while
statement as a whole.
If booleanExpression
isn't true
when the while
statement is first reached,
the enclosed statement or block will not be executed at all
and execution continues with the next statement
following the while
statement as a whole.
Finally, if booleanExpression
never becomes false
,
the loop will repeat over and over and over, forever.
For example, the following piece of code will print the numbers from 1 to 100:
int number = 1;
while (number <= 100)
{
System.out.println(number);
number = number + 1;
}
On the other hand, the following piece of code prints nothing at all:
int number = 1;
while (number >= 100)
{
System.out.println(number);
number = number + 1;
}
Finally, the following piece of code tries to print numbers forever:
int number = 1;
while (number >= 1)
{
System.out.println(number);
number = number + 1;
}
The Java interpreter never tries to figure out
if we really meant to ask it to loop forever,
even though we usually don't mean to.
Computers aren't very smart.
Often, we will know exactly how many times
we want to repeat the enclosed code.
In such cases it's common to use a for
statement instead.
for (initialStatement; booleanExpression; repeatedStatement)
[statement or block]
There is no semicolon after repeatedStatement
and no extra semicolon after the enclosed statement or block.
As with the if
and if-else
statements,
neither the while
nor the for
statement
needs its own separate semicolon to terminate it.
The Java interpreter can figure out where it ends for itself
because it's a fixed form.
The for
statement is roughly equivalent to
the following code:
initialStatement;
while (booleanExpression)
{
[statement or block]
repeatedStatement;
}
For example, the following piece of code will print the numbers from 1 to 100:
int number = 1;
for (number = 1; number <= 100; number = number + 1)
System.out.println(number);
Once we know the exact number of times to repeat the enclosed code
it's usually better to have a for
statement
rather than a while
statement.
Using a for
statement
usually makes it more clear how many times the loop will repeat
and also usually decreases the chance that we forget something and
inadvertently ask the computer to loop forever.
Complexifying Lamps
Let's apply what we've learned so far to create a more realistic lamp. Suppose, for instance, that we wanted a variable-brightness lamp. A lamp now needs a dimmer knob to turn its brightness up or down in stages.
To model a lamp's current brightness we'll need a new variable,
let's say an int
, called, say, brightness
.
That takes care of the lamp's new state.
For its behavior we need new methods to brighten or darken the lamp.
Here's some of the new code to add to class Lamp
:
//this lamp has a certain brightness, if it's on
private int brightness;
public void brighten()
{
/*
If this lamp is on, make it brighter.
*/
if (this.isOn() == true)
brightness = brightness + 1;
reportState();
}
public void darken()
{
/*
If this lamp is on, make it darker.
*/
if (this.isOn() == true)
brightness = brightness - 1;
reportState();
}
public void brighten(int brightnessIncrease)
{
/*
Make this lamp brighter in steps.
*/
int index = 1;
while (index <= brightnessIncrease)
{
brighten();
index = index + 1;
}
}
public void darken(int brightnessDecrease)
{
/*
Make this lamp darker in steps.
*/
int index = 1;
while (index <= brightnessDecrease)
{
darken();
index = index + 1;
}
}
Method Overloading
Class Lamp
now has two brighten()
and two darken()
methods.
Although they both have the same name,
one has an int
as a parameter and the other does not.
They have differing signatures---that is,
either the number or the type, or both, of their parameters is different.
That's enough of a clue for the Java interpreter to figure out
which method we really want a Lamp
actor to execute.
If we say:
bauhaus.brighten(6);
we're asking bauhaus
to execute its brighten()
method that takes an int
parameter.
If we say:
bauhaus.brighten();
we're asking bauhaus
to execute its brighten()
method that takes no parameters.
We've already seen an example of constructor overloading (having multiple constructors, each with its own unique signature); here is an example of method overloading.
The signature of a method or constructor
is not just the number of parameters it takes,
but also their type and their sequence.
So three methods that take two int
values
and a boolean
value in the following orders:
(int, int, boolean)
,
(int, boolean, int)
,
and (boolean, int, int)
all have different signatures.
The names of a method's parameters aren't part of its signature.
So our Lamp
class could never have both of
the following two methods:
public void brighten(int brightnessIncrease)
{
}
public void brighten(int wattageIncrease)
{
}
Although the parameter names are different, the signatures are the same;
so the Java interpreter can't figure out which method we want to execute
if we ask a lamp to, for example, brighten(15)
.
Switching To
for
Statements
Instead of using while
statements for indexed loops
it's usually better style to use for
statements
since we know how many times the loop will repeat.
Here's what the second version of darken()
looks like with a for
statement:
public void darken(int brightnessDecrease)
{
/*
Make this lamp darker in steps.
*/
int index = 1;
for (index = 1; index <= brightnessDecrease;
index = index + 1)
darken();
}
The for
statement is especially useful
when we want to step through a set of things with an int
index.
That happens so often that the Java interpreter lets us declare
the loop's index in the statement itself, like so:
for (int index = 1; index <= brightnessDecrease;
index = index + 1)
The variable index
declared inside the for
statement
is local to the block the for
statement controls.
We can't access it outside that block.
Once the for
loop ends, variable index
vanishes.
Further, the Java interpreter understands a special operator
when we want to increase
an int
(or double
) variable by one,
since that is also very common.
The statement:
index++;
is short for the statement:
index = index + 1;
So the above darken()
method now looks like this:
public void darken(int brightnessDecrease)
{
/*
Make this lamp darker in steps.
*/
for (int index = 1; index <= brightnessDecrease; index++)
darken();
}
The Java interpreter also lets use say
index--;
when we mean:
index = index - 1;
Both the increment operator (++
)
and the decrement operator (--
)
only apply to int
or double
variables.
Simplifying
boolean
Expressions
The second time we saw an if
(which was in FredsScript
)
we used it to compare
fred
's boolean
variable
finishedProcessing
against the value false
.
Instead, we can do this more simply.
Since all boolean
expressions can only have the value
true
or the value false
,
we can use them directly in if
statements.
So, instead of testing the result of kandinsky.isOn()
against
true
or false
,
we can simply test it directly, as in:
if (kandinsky.isOn())
System.out.println("kandinsky is on.")
else
System.out.println("kandinsky is off.")
This if-else
statement asks is the value of
kandinsky.isOn()
is true
.
If it is, the first enclosed statement executes next,
otherwise, the second enclosed statement executes next.
Declaring Constants
public
Our present version of the Lamp
class has a private
class constant, MAXIMUM_WATTAGE
,
and a public
class method getMaximumWattage()
to report it.
This is unnecessary since it's quite safe
to make constants public
.
No one can change them,
so making them public
can do no harm.
That would let us get rid of the
class method getMaximumWattage()
as well.
This is an amendment to our earlier style rule
about making all variables private
and all methods public
.
When a variable isn't "variable" at all,
it's okay for it to be public
.
So far we haven't seen a case where a method shouldn't be made
public
, but we will.
These small exceptions don't invalidate the style rule, however;
generally speaking, all variables should be private
and all methods public
.
Finally, we should name (and make public
)
the constant representing the 60-watt default value
for each lamp's wattage
so that if we ever decide to change it
we only have to change it in one place.
This is another example of the "restricting access" style rule.
The Whole Program
Here is the entire program,
including the new variables, constants, and methods,
for
statements instead of while
statements,
increment and decrement operators,
and simplified boolean
expressions:
class Lamp
{
/*
Define a lamp that can turn on and off,
report its state, report its state changes,
have a wattage, brighten and darken in stages,
and make sure it's off on creation.
The class itself remembers the maximum possible wattage
of any lamp and the default wattage of each lamp.
*/
//this lamp is either on or off
private boolean lampIsOn;
//this lamp has a certain brightness, if it's on
private int brightness;
//this lamp has a wattage
private int wattage;
//all lamps have a maximum possible wattage
public final static int MAXIMUM_WATTAGE = 500;
//all lamps have a default wattage
public final static int DEFAULT_WATTAGE = 60;
public Lamp()
{
/*
Set this lamp's initial state
as being a Lamp.DEFAULT_WATTAGE-watt lamp
in the off position.
*/
lampIsOn = false;
brightness = 0;
wattage = Lamp.DEFAULT_WATTAGE;
}
public Lamp(int wattage)
{
/*
Set this lamp's initial state
as being in the off position.
Set this lamp's wattage to the given wattage.
Do not let any lamp have a wattage higher than
Lamp.MAXIMUM_WATTAGE.
*/
lampIsOn = false;
brightness = 0;
if (wattage > Lamp.MAXIMUM_WATTAGE)
this.wattage = Lamp.MAXIMUM_WATTAGE;
else
this.wattage = wattage;
}
public void turnOn()
{
/*
Turn this lamp on.
*/
lampIsOn = true;
brightness = 1;
reportState();
}
public void turnOff()
{
/*
Turn this lamp off.
*/
lampIsOn = false;
brightness = 0;
reportState();
}
public boolean isOn()
{
/*
Report whether this lamp is on or off.
*/
return lampIsOn;
}
public void reportState()
{
/*
Report this lamp's status.
*/
if (this.isOn())
{
System.out.println("I am on");
System.out.println("My wattage is: " + wattage);
}
else
System.out.println("I am off.");
}
public void brighten()
{
/*
If this lamp is on, make it brighter.
*/
if (this.isOn())
brightness++;
reportState();
}
public void darken()
{
/*
If this lamp is on, make it darker.
*/
if (this.isOn())
brightness--;
reportState();
}
public void brighten(int brightnessIncrease)
{
/*
Make this lamp brighter in steps.
*/
for (int index = 1; index <= brightnessIncrease; index++)
brighten();
}
public void darken(int brightnessDecrease)
{
/*
Make this lamp darker in steps.
*/
for (int index = 1; index <= brightnessDecrease; index++)
darken();
}
}
import Lamp;
class FiddleWithLamps
{
/*
Create some lamps and fiddle with them.
*/
public static void main(String[] parameters)
{
//create a (normal) Lamp.DEFAULT_WATTAGE-watt lamp
Lamp kandinsky;
kandinsky = new Lamp();
kandinsky.turnOn();
kandinsky.brighten(4);
kandinsky.darken(2);
kandinsky.turnOff();
//create a 100-watt lamp
Lamp bauhaus;
bauhaus = new Lamp(100);
bauhaus.turnOn();
bauhaus.brighten(6);
bauhaus.darken(1);
bauhaus.turnOff();
if (kandinsky.isOn() == bauhaus.isOn())
System.out.println("The two lamp states are equal.");
else
System.out.println("The two lamp states are unequal.");
System.out.println("Maximum wattage of any lamp = " +
Lamp.MAXIMUM_WATTAGE);
}
}
Benefits of Encapsulation
Now that we've added brightness
we could get rid of lampIsOn
if we choose
since lampIsOn
is true
only when brightness > 0
.
The only methods we'd have to change would be
isOn()
, turnOn()
, and turnOff()
.
We would also have to change the two constructors.
Thanks to our attempts to keep class Lamp
encapsulated,
even though we're thinking of removing a variable
(a portion of a lamp's state)
no object that uses lamps would have to change in any way.
Encapsulating lamps as much as possible
protects us from having to change it a lot,
if we ever have to change it.
Further, it protects all other classes from having to change at all
if class Lamp
changes.
Since they can in no way affect a lamp's internal state before the change,
internal changes in class Lamp
cannot affect them.
The less other class know about class Lamp
,
and the less class Lamp
knows about itself,
the easier changes to class Lamp
become.
This makes building large, complex, flexible programs much easier
than if everything depended on everything else.
That is the point of encapsulation.
Breaking encapsulation is a serious style crime.
Adding String
Names
Now let's give each lamp a name. To do so we can add one new variable to hold the lamp's name, if any, and two methods to control access to the variable: one to save a name assigned from outside and the other to report the lamp's current name.
This seems to require us to build a new class of objects to hold strings,
but fortunately the Java interpreter already knows of a class
whose objects can hold and manipulate strings:
class String
.
Here are the additions to class Lamp
:
private String name;
public void setName(String name)
{
/*
Name this lamp.
*/
this.name = name;
}
public String getName()
{
/*
Report this lamp's name.
*/
return name;
}
And here is how we might use that new capability:
Lamp kandinsky;
kandinsky = new Lamp();
kandinsky.setName("kandinsky");
System.out.println(kandinsky.getName() + " here");
kandinsky.turnOn();
kandinsky.turnOff();
In Java we delimit strings with double quotes ("
).
Creating
String
s
Just as class Lamp
defines lamp objects,
class String
defines string objects.
These objects can hold and manipulate strings---that is, sequences of symbols.
Just as with lamps,
we can create them with new
,
ask them to execute their methods,
and ask them to modify their variables (if any were public
).
They also have constructors just as Lamp
objects do.
Here, for example, are two pieces of code.
One creates a new object of class Lamp
and the other creates a new object of class String
both using their default constructors:
|
|