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 Strings,
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 like 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 forth.
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
with the same name.
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 or blocks are transient,
they only exist while some object is executing the method.
Other objects can never see them at all,
since they're not even visible from other methods of the same object.
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.
Fourth, the stage manager lets us name a block and have it accept
parameters and return from anywhere in it---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 forever.
For instance, 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;
they do the heavy lifting---we tell them where to put it.
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;
/*
If this lamp is on, make it brighter.
*/
public void brighten()
{
if (this.isOn() == true)
brightness = brightness + 1;
reportState();
}
/*
If this lamp is on, make it darker.
*/
public void darken()
{
if (this.isOn() == true)
brightness = brightness - 1;
reportState();
}
/*
Make this lamp brighter in steps.
*/
public void brighten(int brightnessIncrease)
{
int index = 1;
while (index <= brightnessIncrease)
{
brighten();
index = index + 1;
}
}
/*
Make this lamp darker in steps.
*/
public void darken(int brightnessDecrease)
{
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 a Lamp actor to figure out
which method we want it 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 a Lamp actor can't figure out which method we want to execute
if we ask it to, for example, brighten(15).
Using
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)
{
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 two special operators
when we want to increase or decrease
an int (or double) variable by one,
since that is also very common.
The statements:
index1++; index2--;
are short for the statements:
index1 = index1 + 1; index2 = index2 - 1;
Both the increment operator (++)
and the decrement operator (--)
only apply to int or double variables.
Now the above darken() method could look like this:
public void darken(int brightnessDecrease)
{
for (int index = 1; index <= brightnessDecrease; index++)
darken();
}
Short and sweet.
Simplifying
boolean Expressions
The second time we saw an if
(which was in SimpleRole)
we used it to compare
estragon's boolean variable
finishedDividing
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 if 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:
/*
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.
*/
class Lamp
{
private boolean isOn;
private int brightness;
private int wattage;
public final static int MAXIMUM_WATTAGE = 500;
public final static int DEFAULT_WATTAGE = 60;
/*
Set this lamp's initial state
as being a Lamp.DEFAULT_WATTAGE-watt lamp
in the off position.
*/
public Lamp()
{
isOn = false;
brightness = 0;
wattage = Lamp.DEFAULT_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.
*/
public Lamp(int wattage)
{
isOn = false;
brightness = 0;
if (wattage > Lamp.MAXIMUM_WATTAGE)
this.wattage = Lamp.MAXIMUM_WATTAGE;
else
this.wattage = wattage;
}
/*
Turn this lamp on.
*/
public void turnOn()
{
isOn = true;
brightness = 1;
reportState();
}
/*
Turn this lamp off.
*/
public void turnOff()
{
isOn = false;
brightness = 0;
reportState();
}
/*
Report whether this lamp is on or off.
*/
public boolean isOn()
{
return isOn;
}
/*
Report this lamp's status.
*/
public void reportState()
{
if (this.isOn())
{
System.out.println("I am on");
System.out.println("My wattage is: " + wattage);
}
else
System.out.println("I am off.");
}
/*
If this lamp is on, make it brighter.
*/
public void brighten()
{
if (this.isOn())
brightness++;
reportState();
}
/*
If this lamp is on, make it darker.
*/
public void darken()
{
if (this.isOn())
brightness--;
reportState();
}
/*
Make this lamp brighter in steps.
*/
public void brighten(int brightnessIncrease)
{
for (int index = 1; index <= brightnessIncrease; index++)
brighten();
}
/*
Make this lamp darker in steps.
*/
public void darken(int brightnessDecrease)
{
for (int index = 1; index <= brightnessDecrease; index++)
darken();
}
}
import Lamp;
class Lobby
{
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 the isOn boolean
variable if we choose,
since isOn 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,
internal changes in class Lamp cannot affect them.
Encapsulation gives objects an inside and an outside,
and that is of immense value when it comes to changing your program.
The less other classes 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;
/*
Name this lamp.
*/
public void setName(String name)
{
this.name = name;
}
/*
Report this lamp's name.
*/
public String getName()
{
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
Strings
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:
|
|