|
|||||
only
produces critical information
for the next analysis,
design, and
implementation
pass, it also creates a code
foundation.
That
said, if you're looking at a
methodology that contains
tremendous
detail
and suggests many steps
and documents, it's still
difficult to know
when to
stop. Keep in mind what
you're trying to
discover:
1.
What
are the objects? (How do
you partition your project
into its
component
parts?)
2.
What
are their interfaces? (What
messages do you need to send
to
each
object?)
If
you come up with nothing
more than the objects
and their interfaces,
then
you can write a program.
For various reasons you
might need more
descriptions
and documents than this,
but you can't get
away with any
less.
The
process can be undertaken in
five phases, and a Phase 0
that is just
the
initial commitment to using
some kind of
structure.
Phase
0: Make a plan
You
must first decide what
steps you're going to have
in your process. It
sounds
simple (in fact, all of
this sounds simple), and
yet people often
don't
make this decision before
they start coding. If your
plan is "let's
jump
in and start coding," fine.
(Sometimes that's appropriate
when you
have
a well-understood problem.) At least
agree that this is the
plan.
You
might also decide at this
phase that some additional
process structure
is
necessary, but not the
whole nine yards.
Understandably, some
programmers
like to work in "vacation
mode," in which no structure
is
imposed
on the process of developing
their work; "It will be
done when
it's
done." This can be appealing
for a while, but I've
found that having a
few
milestones along the way
helps to focus and galvanize
your efforts
around
those milestones instead of
being stuck with the
single goal of
"finish
the project." In addition, it
divides the project into
more bite-sized
pieces
and makes it seem less
threatening (plus the
milestones offer more
opportunities
for celebration).
74
Thinking
in Java
When
I began to study story
structure (so that I will
someday write a
novel)
I was initially resistant to
the idea of structure,
feeling that I wrote
best
when I simply let it flow
onto the page. But I
later realized that
when
I
write about computers the
structure is clear enough to me
that I don't
have
to think about it very much.
But I still structure my
work, albeit only
semi-consciously
in my head. Even if you
think that your plan is to
just
start
coding, you still somehow go
through the subsequent
phases while
asking
and answering certain
questions.
The
mission statement
Any
system you build, no matter
how complicated, has a
fundamental
purpose;
the business that it's
in, the basic need
that it satisfies. If you
can
look
past the user interface,
the hardware- or system-specific
details, the
coding
algorithms and the
efficiency problems, you
will eventually find
the
core of its being--simple
and straightforward. Like
the so-called high
concept
from
a Hollywood movie, you can
describe it in one or
two
sentences.
This pure description is the
starting point.
The
high concept is quite
important because it sets
the tone for
your
project;
it's a mission statement.
You won't necessarily get it
right the first
time
(you may be in a later phase
of the project before it
becomes
completely
clear), but keep trying
until it feels right. For
example, in an
air-traffic
control system you may
start out with a high
concept focused on
the
system that you're building:
"The tower program keeps
track of the
aircraft."
But consider what happens
when you shrink the
system to a very
small
airfield; perhaps there's
only a human controller, or
none at all. A
more
useful model won't concern
the solution you're creating
as much as
it
describes the problem:
"Aircraft arrive, unload,
service and reload,
then
depart."
Phase
1: What are we making?
In
the previous generation of
program design (called
procedural
design),
this
is called "creating the
requirements
analysis and
system
specification."
These, of course, were
places to get lost;
intimidatingly
named
documents that could become
big projects in their own
right. Their
intention
was good, however. The
requirements analysis says
"Make a list
of
the guidelines we will use
to know when the job is
done and the
Chapter
1: Introduction to Objects
75
customer
is satisfied." The system
specification says "Here's a
description
of
what
the
program will do (not
how)
to satisfy the requirements."
The
requirements
analysis is really a contract
between you and the
customer
(even
if the customer works within
your company, or is some
other object
or
system). The system
specification is a top-level exploration
into the
problem
and in some sense a
discovery of whether it can be
done and how
long
it will take. Since both of
these will require consensus
among people
(and
because they will usually
change over time), I think
it's best to keep
them
as bare as possible--ideally, to lists
and basic diagrams--to
save
time.
You might have other
constraints that require you
to expand them
into
bigger documents, but by
keeping the initial document
small and
concise,
it can be created in a few
sessions of group brainstorming
with a
leader
who dynamically creates the
description. This not only
solicits
input
from everyone, it also
fosters initial buy-in and
agreement by
everyone
on the team. Perhaps most
importantly, it can kick off
a project
with
a lot of enthusiasm.
It's
necessary to stay focused on
the heart of what you're
trying to
accomplish
in this phase: determine
what the system is supposed
to do.
The
most valuable tool for
this is a collection of what
are called "use
cases."
Use cases identify key
features in the system that
will reveal some
of
the fundamental classes
you'll be using. These are
essentially
descriptive
answers to questions
like9:
·
"Who
will use this
system?"
·
"What
can those actors do with
the system?"
·
"How
does this
actor
do that
with
this system?"
·
"How
else might this work if
someone else were doing
this, or if
the
same actor had a different
objective?" (to reveal
variations)
·
"What
problems might happen while
doing this with the
system?"
(to
reveal exceptions)
If
you are designing an
auto-teller, for example,
the use case for
a
particular
aspect of the functionality of
the system is able to
describe what
the
auto-teller does in every
possible situation. Each of
these "situations"
9
Thanks for
help from James H
Jarrett.
76
Thinking
in Java
is
referred to as a scenario,
and a use case can be
considered a collection
of
scenarios. You can think of
a scenario as a question that
starts with:
"What
does the system do if...?"
For example, "What does
the auto-teller
do
if a customer has just
deposited a check within the
last 24 hours, and
there's
not enough in the account
without the check having
cleared to
provide
a desired withdrawal?"
Use
case diagrams are
intentionally simple to prevent
you from getting
bogged
down in system implementation
details prematurely:
Bank
Make
Deposit
Uses
Make
Teller
Withdrawal
Get
Account
Customer
Balance
Transfer
Between
Accounts
ATM
Each
stick person represents an
"actor," which is typically a
human or
some
other kind of free agent.
(These can even be other
computer
systems,
as is the case with "ATM.")
The box represents the
boundary of
your
system. The ellipses
represent the use cases,
which are
descriptions
of
valuable work that can be
performed with the system.
The lines
between
the actors and the
use cases represent the
interactions.
It
doesn't matter how the
system is actually implemented, as
long as it
looks
like this to the
user.
A
use case does not
need to be terribly complex,
even if the
underlying
system
is complex. It is only intended to
show the system as it
appears to
the
user. For example:
Chapter
1: Introduction to Objects
77
Greenhouse
Maintain
Growing
Temperature
Gardener
The
use cases produce the
requirements specifications by
determining all
the
interactions that the user
may have with the
system. You try to
discover
a full set of use cases
for your system, and
once you've done
that
you
have the core of what
the system is supposed to
do. The nice
thing
about
focusing on use cases is
that they always bring
you back to the
essentials
and keep you from
drifting off into issues
that aren't critical
for
getting
the job done. That
is, if you have a full
set of use cases, you
can
describe
your system and move
onto the next phase.
You probably won't
get
it all figured out perfectly
on the first try, but
that's OK. Everything
will
reveal itself in time, and
if you demand a perfect
system specification
at
this point you'll get
stuck.
If
you do get stuck, you
can kick-start this phase by
using a rough
approximation
tool: describe the system in
a few paragraphs and
then
look
for nouns and verbs.
The nouns can suggest
actors, context of the
use
case
(e.g., "lobby"), or artifacts
manipulated in the use case.
Verbs can
suggest
interactions between actors
and use cases, and
specify steps
within
the use case. You'll
also discover that nouns
and verbs produce
objects
and messages during the
design phase (and note
that use cases
describe
interactions between subsystems, so
the "noun and
verb"
technique
can be used only as a
brainstorming tool as it does
not generate
use
cases) 10.
The
boundary between a use case
and an actor can point
out the existence
of
a user interface, but it
does not define such a
user interface. For a
process
of defining and creating
user interfaces, see
Software
for Use by
10
More
information on use cases can be
found in Applying
Use Cases by Schneider
&
Winters
(Addison-Wesley 1998) and
Use
Case Driven Object Modeling
with UML by
Rosenberg
(Addison-Wesley 1999).
78
Thinking
in Java
Larry
Constantine and Lucy
Lockwood, (Addison-Wesley Longman,
1999)
or
go to www.ForUse.com.
Although
it's a black art, at this
point some kind of basic
scheduling is
important.
You now have an overview of
what you're building, so
you'll
probably
be able to get some idea of
how long it will take. A
lot of factors
come
into play here. If you
estimate a long schedule
then the company
might
decide not to build it (and
thus use their resources on
something
more
reasonable--that's a good
thing).
Or a manager might have
already
decided
how long the project
should take and will
try to influence your
estimate.
But it's best to have an
honest schedule from the
beginning and
deal
with the tough decisions
early. There have been a
lot of attempts to
come
up with accurate scheduling
techniques (much like
techniques to
predict
the stock market), but
probably the best approach
is to rely on
your
experience and intuition.
Get a gut feeling for
how long it will
really
take,
then double that and
add 10 percent. Your gut
feeling is probably
correct;
you can
get
something working in that
time. The "doubling"
will
turn
that into something decent,
and the 10 percent will
deal with the
final
polishing and details11.
However you want to explain
it, and
regardless
of the moans and
manipulations that happen
when you reveal
such
a schedule, it just seems to
work out that
way.
Phase
2: How will we build
it?
In
this phase you must
come up with a design that
describes what the
classes
look like and how
they will interact. An
excellent technique in
determining
classes and interactions is
the Class-Responsibility-
Collaboration
(CRC)
card. Part of the value of
this tool is that it's so
low-
tech:
you start out with a
set of blank 3 x 5 cards,
and you write on
them.
Each
card represents a single
class, and on the card
you write:
1.
The
name of the class. It's
important that this name
capture the
essence
of what the class does, so
that it makes sense at a
glance.
11
My personal take on
this has changed lately.
Doubling and adding 10
percent will give
you
a reasonably accurate estimate (assuming there are
not too many wild-card
factors),
but
you still have to work quite
diligently to finish in that
time. If you want time to
really
make
it elegant and to enjoy yourself in the
process, the correct multiplier is more
like
three
or four times, I believe.
Chapter
1: Introduction to Objects
79
2.
The
"responsibilities" of the class:
what it should do. This
can
typically
be summarized by just stating
the names of the
member
functions
(since those names should be
descriptive in a good
design),
but it does not preclude
other notes. If you need to
seed
the
process, look at the problem
from a lazy
programmer's
standpoint:
What objects would you
like to magically appear
to
solve
your problem?
3.
The
"collaborations" of the class:
what other classes does it
interact
with?
"Interact" is an intentionally broad
term; it could mean
aggregation
or simply that some other
object exists that
will
perform
services for an object of
the class. Collaborations
should
also
consider the audience for
this class. For example, if
you create
a
class Firecracker,
who is going to observe it,
a Chemist
or
a
Spectator?
The former will want to
know what chemicals go
into
the
construction, and the latter
will respond to the colors
and
shapes
released when it
explodes.
You
may feel like the
cards should be bigger
because of all the
information
you'd
like to get on them, but
they are intentionally
small, not only to
keep
your
classes small but also to
keep you from getting
into too much
detail
too
early. If you can't fit
all you need to know
about a class on a
small
card,
the class is too complex
(either you're getting too
detailed, or you
should
create more than one
class). The ideal class
should be understood
at
a glance. The idea of CRC
cards is to assist you in
coming up with a
first
cut
of the design so that you
can get the big
picture and then refine
your
design.
One
of the great benefits of CRC
cards is in communication. It's
best done
real
time, in a group, without
computers. Each person takes
responsibility
for
several classes (which at
first have no names or other
information).
You
run a live simulation by
solving one scenario at a
time, deciding
which
messages are sent to the
various objects to satisfy
each scenario. As
you
go through this process, you
discover the classes that
you need along
with
their responsibilities and
collaborations, and you fill
out the cards as
you
do this. When you've moved
through all the use
cases, you should
have
a fairly complete first cut
of your design.
80
Thinking
in Java
Before
I began using CRC cards,
the most successful
consulting
experiences
I had when coming up with an
initial design
involved
standing
in front of a team--who hadn't
built an OOP project
before--and
drawing
objects on a whiteboard. We talked
about how the objects
should
communicate
with each other, and
erased some of them and
replaced
them
with other objects.
Effectively, I was managing
all the "CRC
cards"
on
the whiteboard. The team
(who knew what the
project was supposed
to
do)
actually created the design;
they "owned" the design
rather than
having
it given to them. All I was
doing was guiding the
process by asking
the
right questions, trying out
the assumptions, and taking
the feedback
from
the team to modify those
assumptions. The true beauty
of the
process
was that the team
learned how to do object-oriented
design not by
reviewing
abstract examples, but by
working on the one design
that was
most
interesting to them at that
moment: theirs.
Once
you've come up with a set of
CRC cards, you may
want to create a
more
formal description of your
design using UML12.
You don't need to
use
UML, but it can be helpful,
especially if you want to
put up a diagram
on
the wall for everyone to
ponder, which is a good
idea. An alternative to
UML
is a textual description of the
objects and their
interfaces, or,
depending
on your programming language,
the code itself13.
UML
also provides an additional
diagramming notation for
describing the
dynamic
model of your system. This
is helpful in situations in which
the
state
transitions of a system or subsystem
are dominant enough that
they
need
their own diagrams (such as
in a control system). You
may also need
to
describe the data
structures, for systems or
subsystems in which data
is
a
dominant factor (such as a
database).
You'll
know you're done with
Phase 2 when you have
described the objects
and
their interfaces. Well, most
of them--there are usually a
few that slip
through
the cracks and don't
make themselves known until
Phase 3. But
that's
OK. All you are
concerned with is that you
eventually discover all
of
your
objects. It's nice to
discover them early in the
process, but OOP
12
For starters, I
recommend the aforementioned
UML
Distilled, 2nd edition.
13
Python (www.Python.org) is
often used as "executable
pseudocode."
Chapter
1: Introduction to Objects
81
provides
enough structure so that
it's not so bad if you
discover them
later.
In fact, the design of an
object tends to happen in
five stages,
throughout
the process of program
development.
Five
stages of object
design
The
design life of an object is
not limited to the time
when you're writing
the
program. Instead, the design
of an object appears over a
sequence of
stages.
It's helpful to have this
perspective because you stop
expecting
perfection
right away; instead, you
realize that the
understanding of what
an
object does and what it
should look like happens
over time. This
view
also
applies to the design of
various types of programs;
the pattern for a
particular
type of program emerges
through struggling again and
again
with
that problem (This is
chronicled in the book
Thinking
in Patterns
with
Java, downloadable
at ).
Objects, too, have
their
patterns that emerge through
understanding, use, and
reuse.
1.
Object discovery. This
stage occurs during the
initial analysis of a
program.
Objects may be discovered by
looking for external factors
and
boundaries,
duplication of elements in the
system, and the
smallest
conceptual
units. Some objects are
obvious if you already have
a set of
class
libraries. Commonality between
classes suggesting base
classes and
inheritance
may appear right away, or
later in the design
process.
2.
Object assembly. As you're
building an object you'll
discover the
need
for new members that
didn't appear during
discovery. The
internal
needs
of the object may require
other classes to support
it.
3.
System construction. Once
again, more requirements for
an
object
may appear at this later
stage. As you learn, you
evolve your
objects.
The need for communication
and interconnection with
other
objects
in the system may change
the needs of your classes or
require new
classes.
For example, you may
discover the need for
facilitator or helper
classes,
such as a linked list, that
contain little or no state
information and
simply
help other classes
function.
4.
System extension. As you
add new features to a system
you may
discover
that your previous design
doesn't support easy system
extension.
With
this new information, you
can restructure parts of the
system,
possibly
adding new classes or class
hierarchies.
82
Thinking
in Java
5.
Object reuse. This is
the real stress test
for a class. If someone
tries
to
reuse it in an entirely new
situation, they'll probably
discover some
shortcomings.
As you change a class to
adapt to more new programs,
the
general
principles of the class will
become clearer, until you
have a truly
reusable
type. However, don't expect
most objects from a system
design to
be
reusable--it is perfectly acceptable
for the bulk of your
objects to be
system-specific.
Reusable types tend to be
less common, and they
must
solve
more general problems in
order to be reusable.
Guidelines
for object
development
These
stages suggest some
guidelines when thinking
about developing
your
classes:
1.
Let
a specific problem generate a
class, then let the
class grow and
mature
during the solution of other
problems.
2.
Remember,
discovering the classes you
need (and their
interfaces)
is
the majority of the system
design. If you already had
those
classes,
this would be an easy
project.
3.
Don't
force yourself to know
everything at the beginning;
learn as
you
go. This will happen
anyway.
4.
Start
programming; get something
working so you can prove
or
disprove
your design. Don't fear
that you'll end up with
procedural-
style
spaghetti code--classes partition
the problem and help
control
anarchy
and entropy. Bad classes do
not break good
classes.
5.
Always
keep it simple. Little clean
objects with obvious utility
are
better
than big complicated
interfaces. When decision
points come
up,
use an Occam's Razor
approach: Consider the
choices and
select
the one that is simplest,
because simple classes are
almost
always
best. Start small and
simple, and you can
expand the class
interface
when you understand it
better. As time goes on,
it's
difficult
to remove elements from a
class.
Phase
3: Build the core
This
is the initial conversion
from the rough design
into a compiling and
executing
body of code that can be
tested, and especially that
will prove or
Chapter
1: Introduction to Objects
83
disprove
your architecture. This is
not a one-pass process, but
rather the
beginning
of a series of steps that
will iteratively build the
system, as
you'll
see in Phase 4.
Your
goal is to find the core of
your system architecture
that needs to be
implemented
in order to generate a running
system, no matter how
incomplete
that system is in this
initial pass. You're
creating a framework
that
you can build on with
further iterations. You're
also performing the
first
of many system integrations
and tests, and giving
the stakeholders
feedback
about what their system
will look like and
how it is progressing.
Ideally,
you are also exposing
some of the critical risks.
You'll probably
also
discover changes and
improvements that can be
made to your
original
architecture--things you would
not have learned
without
implementing
the system.
Part
of building the system is
the reality check that
you get from
testing
against
your requirements analysis
and system specification (in
whatever
form
they exist). Make sure
that your tests verify
the requirements and
use
cases. When the core of
the system is stable, you're
ready to move on
and
add more
functionality.
Phase
4: Iterate the use cases
Once
the core framework is
running, each feature set
you add is a small
project
in itself. You add a feature
set during an iteration,
a reasonably
short
period of development.
How
big is an iteration? Ideally,
each iteration lasts one to
three weeks
(this
can vary based on the
implementation language). At the
end of that
period,
you have an integrated,
tested system with more
functionality
than
it had before. But what's
particularly interesting is the
basis for the
iteration:
a single use case. Each
use case is a package of
related
functionality
that you build into
the system all at once,
during one
iteration.
Not only does this
give you a better idea of
what the scope of a
use
case should be, but it
also gives more validation
to the idea of a use
case,
since the concept isn't
discarded after analysis and
design, but
instead
it is a fundamental unit of development
throughout the
software-
building
process.
84
Thinking
in Java
You
stop iterating when you
achieve target functionality or an
external
deadline
arrives and the customer
can be satisfied with the
current
version.
(Remember, software is a subscription
business.) Because
the
process
is iterative, you have many
opportunities to ship a product
rather
than
a single endpoint; open-source
projects work exclusively in
an
iterative,
high-feedback environment, which is
precisely what makes
them
successful.
An
iterative development process is
valuable for many reasons.
You can
reveal
and resolve critical risks
early, the customers have
ample
opportunity
to change their minds,
programmer satisfaction is
higher,
and
the project can be steered
with more precision. But an
additional
important
benefit is the feedback to
the stakeholders, who can
see by the
current
state of the product exactly
where everything lies. This
may
reduce
or eliminate the need for
mind-numbing status meetings
and
increase
the confidence and support
from the
stakeholders.
Phase
5: Evolution
This
is the point in the
development cycle that has
traditionally been
called
"maintenance," a catch-all term
that can mean everything
from
"getting
it to work the way it was
really supposed to in the
first place" to
"adding
features that the customer
forgot to mention" to the
more
traditional
"fixing the bugs that
show up" and "adding
new features as the
need
arises." So many misconceptions
have been applied to the
term
"maintenance"
that it has taken on a
slightly deceiving quality,
partly
because
it suggests that you've
actually built a pristine
program and all
you
need to do is change parts,
oil it, and keep it
from rusting. Perhaps
there's
a better term to describe
what's going on.
I'll
use the term evolution14. That is, "You
won't get it right the
first time,
so
give yourself the latitude
to learn and to go back and
make changes."
You
might need to make a lot of
changes as you learn and
understand the
problem
more deeply. The elegance
you'll produce if you evolve
until you
get
it right will pay off,
both in the short and
the long term. Evolution
is
14
At least one
aspect of evolution is covered in Martin
Fowler's book Refactoring:
improving
the design of existing code
(Addison-Wesley
1999), which uses Java
examples
exclusively.
Chapter
1: Introduction to Objects
85
where
your program goes from
good to great, and where
those issues that
you
didn't really understand in
the first pass become
clear. It's also
where
your
classes can evolve from
single-project usage to reusable
resources.
What
it means to "get it right"
isn't just that the
program works
according
to
the requirements and the
use cases. It also means
that the internal
structure
of the code makes sense to
you, and feels like it
fits together
well,
with no awkward syntax,
oversized objects, or ungainly
exposed bits
of
code. In addition, you must
have some sense that
the program
structure
will survive the changes
that it will inevitably go
through during
its
lifetime, and that those
changes can be made easily
and cleanly. This is
no
small feat. You must
not only understand what
you're building, but
also
how the program will
evolve (what I call the
vector
of change).
Fortunately,
object-oriented programming languages
are particularly
adept
at supporting this kind of
continuing modification--the
boundaries
created
by the objects are what
tend to keep the structure
from breaking
down.
They also allow you to
make changes--ones that
would seem
drastic
in a procedural program--without causing
earthquakes
throughout
your code. In fact, support
for evolution might be the
most
important
benefit of OOP.
With
evolution, you create
something that at least
approximates what you
think
you're building, and then
you kick the tires,
compare it to your
requirements,
and see where it falls
short. Then you can go
back and fix it
by
redesigning and reimplementing
the portions of the program
that
didn't
work right15. You might actually
need to solve the problem,
or an
aspect
of the problem, several
times before you hit on
the right solution.
(A
study of Design
Patterns is usually
helpful here. You can
find
information
in Thinking
in Patterns with Java, downloadable
at
.)
15
This is
something like "rapid
prototyping," where you were
supposed to build a
quick-
and-dirty
version so that you could learn
about the system, and then throw away
your
prototype
and build it right. The
trouble with rapid
prototyping is that people
didn't throw
away
the prototype, but instead built upon
it. Combined with the lack of structure
in
procedural
programming, this often leads to
messy systems that are
expensive to
maintain.
86
Thinking
in Java
Evolution
also occurs when you
build a system, see that it
matches your
requirements,
and then discover it wasn't
actually what you
wanted.
When
you see the system in
operation, you find that
you really wanted to
solve
a different problem. If you
think this kind of evolution
is going to
happen,
then you owe it to yourself
to build your first version
as quickly as
possible
so you can find out if it is
indeed what you
want.
Perhaps
the most important thing to
remember is that by
default--by
definition,
really--if you modify a
class, its super- and
subclasses will still
function.
You need not fear
modification (especially if you
have a built-in
set
of unit tests to verify the
correctness of your
modifications).
Modification
won't necessarily break the
program, and any change in
the
outcome
will be limited to subclasses
and/or specific collaborators of
the
class
you change.
Plans
pay off
Of
course you wouldn't build a
house without a lot of
carefully drawn
plans.
If you build a deck or a dog
house your plans won't be so
elaborate,
but
you'll probably still start
with some kind of sketches
to guide you on
your
way. Software development
has gone to extremes. For a
long time,
people
didn't have much structure
in their development, but
then big
projects
began failing. In reaction, we
ended up with methodologies
that
had
an intimidating amount of structure
and detail, primarily
intended
for
those big projects. These
methodologies were too scary
to use--it
looked
like you'd spend all
your time writing documents
and no time
programming.
(This was often the
case.) I hope that what
I've shown you
here
suggests a middle path--a
sliding scale. Use an
approach that fits
your
needs (and your
personality). No matter how
minimal you choose to
make
it, some
kind
of plan will make a big
improvement in your project
as
opposed
to no plan at all. Remember
that, by most estimates,
over 50
percent
of projects fail (some
estimates go up to 70 percent!).
By
following a plan--preferably one
that is simple and
brief--and coming
up
with design structure before
coding, you'll discover that
things fall
together
far more easily than if
you dive in and start
hacking. You'll also
realize
a great deal of satisfaction.
It's my experience that
coming up with
an
elegant solution is deeply
satisfying at an entirely different
level; it
feels
closer to art than
technology. And elegance
always pays off; it's
not a
Chapter
1: Introduction to Objects
87
frivolous
pursuit. Not only does it
give you a program that's
easier to build
and
debug, but it's also
easier to understand and
maintain, and that's
where
the financial value
lies.
Extreme
programming
I
have studied analysis and
design techniques, on and
off, since I was in
graduate
school. The concept of
Extreme
Programming (XP) is
the most
radical,
and delightful, that I've
seen. You can find it
chronicled in
Extreme
Programming Explained by Kent
Beck (Addison-Wesley,
2000)
and
on the Web at www.xprogramming.com.
XP
is both a philosophy about
programming work and a set
of guidelines
to
do it. Some of these
guidelines are reflected in
other recent
methodologies,
but the two most
important and distinct
contributions, in
my
opinion, are "write tests
first" and "pair
programming." Although he
argues
strongly for the whole
process, Beck points out
that if you adopt
only
these two practices you'll
greatly improve your
productivity and
reliability.
Write
tests first
Testing
has traditionally been
relegated to the last part
of a project, after
you've
"gotten everything working,
but just to be sure." It's
implicitly had
a
low priority, and people
who specialize in it have
not been given a lot
of
status
and have often even
been cordoned off in a
basement, away from
the
"real programmers." Test
teams have responded in
kind, going so far
as
to wear black clothing and
cackling with glee whenever
they break
something
(to be honest, I've had
this feeling myself when
breaking
compilers).
XP
completely revolutionizes the
concept of testing by giving it
equal (or
even
greater) priority than the
code. In fact, you write
the tests before
you
write
the code that will be
tested, and the tests
stay with the code
forever.
The
tests must be executed
successfully every time you
do an integration
of
the project (which is often,
sometimes more than once a
day).
Writing
tests first has two
extremely important
effects.
88
Thinking
in Java
First,
it forces a clear definition of
the interface of a class.
I've often
suggested
that people "imagine the
perfect class to solve a
particular
problem"
as a tool when trying to
design the system. The XP
testing
strategy
goes further than that--it
specifies exactly what the
class must
look
like, to the consumer of
that class, and exactly
how the class
must
behave.
In no uncertain terms. You
can write all the
prose, or create all
the
diagrams you want,
describing how a class
should behave and what
it
looks
like, but nothing is as real
as a set of tests. The
former is a wish
list,
but
the tests are a contract
that is enforced by the
compiler and the
running
program. It's hard to
imagine a more concrete
description of a
class
than the tests.
While
creating the tests, you
are forced to completely
think out the
class
and
will often discover needed
functionality that might be
missed during
the
thought experiments of UML
diagrams, CRC cards, use
cases, etc.
The
second important effect of
writing the tests first
comes from running
the
tests every time you do a
build of your software. This
activity gives you
the
other half of the testing
that's performed by the
compiler. If you look
at
the evolution of programming
languages from this
perspective, you'll
see
that the real improvements
in the technology have
actually revolved
around
testing. Assembly language
checked only for syntax,
but C
imposed
some semantic restrictions,
and these prevented you
from
making
certain types of mistakes.
OOP languages impose even
more
semantic
restrictions, which if you
think about it are actually
forms of
testing.
"Is this data type
being used properly?" and
"Is this function
being
called
properly?" are the kinds of
tests that are being
performed by the
compiler
or run-time system. We've
seen the results of having
these tests
built
into the language: people
have been able to write
more complex
systems,
and get them to work,
with much less time
and effort. I've
puzzled
over why this is,
but now I realize it's
the tests: you do
something
wrong,
and the safety net of
the built-in tests tells
you there's a problem
and
points you to where it
is.
But
the built-in testing
afforded by the design of
the language can only
go
so
far. At some point,
you
must
step in and add the
rest of the tests
that
produce
a full suite (in cooperation
with the compiler and
run-time
system)
that verifies all of your
program. And, just like
having a compiler
watching
over your shoulder, wouldn't
you want these tests
helping you
Chapter
1: Introduction to Objects
89
right
from the beginning? That's
why you write them
first, and run
them
automatically
with every build of your
system. Your tests become
an
extension
of the safety net provided
by the language.
One
of the things that I've
discovered about the use of
more and more
powerful
programming languages is that I am
emboldened to try
more
brazen
experiments, because I know
that the language will
keep me from
wasting
my time chasing bugs. The XP
test scheme does the
same thing
for
your entire project. Because
you know your tests
will always catch
any
problems
that you introduce (and
you regularly add any
new tests as you
think
of them), you can make
big changes when you
need to without
worrying
that you'll throw the
whole project into complete
disarray. This
is
incredibly powerful.
Pair
programming
Pair
programming goes against the
rugged individualism that
we've been
indoctrinated
into from the beginning,
through school (where we
succeed
or
fail on our own, and
working with our neighbors
is considered
"cheating"),
and media, especially
Hollywood movies in which
the hero is
usually
fighting against mindless
conformity16. Programmers, too,
are
considered
paragons of individuality--"cowboy
coders" as Larry
Constantine
likes to say. And yet
XP, which is itself battling
against
conventional
thinking, says that code
should be written with two
people
per
workstation. And that this
should be done in an area
with a group of
workstations,
without the barriers that
the facilities-design people
are so
fond
of. In fact, Beck says
that the first task of
converting to XP is to arrive
with
screwdrivers and Allen
wrenches and take apart
everything that gets
in
the way.17 (This will require a
manager who can deflect
the ire of the
facilities
department.)
16
Although this
may be a more American perspective, the
stories of Hollywood reach
everywhere.
17
Including (especially) the
PA system. I once worked in a company
that insisted on
broadcasting
every phone call that
arrived for every executive,
and it constantly
interrupted
our productivity (but the managers
couldn't begin to conceive of stifling
such
an
important service as the PA). Finally, when no
one was looking I started
snipping
speaker
wires.
90
Thinking
in Java
The
value of pair programming is
that one person is actually
doing the
coding
while the other is thinking
about it. The thinker
keeps the big
picture
in mind--not only the
picture of the problem at
hand, but the
guidelines
of XP. If two people are
working, it's less likely
that one of
them
will get away with
saying, "I don't want to
write the tests first,"
for
example.
And if the coder gets
stuck, they can swap
places. If both of
them
get
stuck, their musings may be
overheard by someone else in
the work
area
who can contribute. Working
in pairs keeps things
flowing and on
track.
Probably more important, it
makes programming a lot more
social
and
fun.
I've
begun using pair programming
during the exercise periods
in some of
my
seminars and it seems to
significantly improve everyone's
experience.
Why
Java succeeds
The
reason Java has been so
successful is that the goal
was to solve many
of
the problems facing
developers today. The goal
of Java is improved
productivity.
This productivity comes in
many ways, but the
language is
designed
to aid you as much as
possible, while hindering
you as little as
possible
with arbitrary rules or any
requirement that you use a
particular
set
of features. Java is designed to be
practical; Java language
design
decisions
were based on providing the
maximum benefits to
the
programmer.
Systems
are easier
to
express and understand
Classes
designed to fit the problem
tend to express it better.
This means
that
when you write the
code, you're describing your
solution in the terms
of
the problem space ("Put
the grommet in the bin")
rather than the
terms
of
the computer, which is the
solution space ("Set the
bit in the chip
that
means
that the relay will
close"). You deal with
higher-level concepts
and
can
do much more with a single
line of code.
The
other benefit of this ease
of expression is maintenance, which
(if
reports
can be believed) takes a
huge portion of the cost
over a program's
lifetime.
If a program is easier to understand,
then it's easier to
maintain.
Chapter
1: Introduction to Objects
91
This
can also reduce the
cost of creating and
maintaining the
documentation.
Maximal
leverage with libraries
The
fastest way to create a
program is to use code
that's already written:
a
library.
A major goal in Java is to
make library use easier.
This is
accomplished
by casting libraries into
new data types (classes), so
that
bringing
in a library means adding
new types to the language.
Because the
Java
compiler takes care of how
the library is used--guaranteeing
proper
initialization
and cleanup, and ensuring
that functions are
called
properly--you
can focus on what you
want the library to do,
not how you
have
to do it.
Error
handling
Error
handling in C is a notorious problem,
and one that is
often
ignored--finger-crossing
is usually involved. If you're
building a large,
complex
program, there's nothing
worse than having an error
buried
somewhere
with no clue as to where it
came from. Java exception
handling
is
a way to guarantee that an
error is noticed, and
that
something
happens as a result.
Programming
in the large
Many
traditional languages have
built-in limitations to program
size and
complexity.
BASIC, for example, can be
great for pulling together
quick
solutions
for certain classes of
problems, but if the program
gets more
than
a few pages long, or
ventures out of the normal
problem domain of
that
language, it's like trying
to swim through an ever-more
viscous fluid.
There's
no clear line that tells
you when your language is
failing you, and
even
if there were, you'd ignore
it. You don't say,
"My BASIC program
just
got
too big; I'll have to
rewrite it in C!" Instead,
you try to shoehorn a
few
more
lines in to add that one
new feature. So the extra
costs come
creeping
up on you.
Java
is designed to aid programming
in the large--that
is, to erase those
creeping-complexity
boundaries between a small
program and a large
one.
You certainly don't need to
use OOP when you're
writing a "hello
world"
style utility program, but
the features are there
when you need
92
Thinking
in Java
them.
And the compiler is
aggressive about ferreting
out bug-producing
errors
for small and large
programs alike.
Strategies
for transition
If
you buy into OOP,
your next question is
probably, "How can I get
my
manager/colleagues/department/peers
to start using objects?"
Think
about
how you--one independent
programmer--would go about
learning
to
use a new language and a
new programming paradigm.
You've done it
before.
First comes education and
examples; then comes a trial
project to
give
you a feel for the
basics without doing
anything too confusing.
Then
comes
a "real world" project that
actually does something
useful.
Throughout
your first projects you
continue your education by
reading,
asking
questions of experts, and
trading hints with friends.
This is the
approach
many experienced programmers
suggest for the switch to
Java.
Switching
an entire company will of
course introduce certain
group
dynamics,
but it will help at each
step to remember how one
person would
do
it.
Guidelines
Here
are some guidelines to
consider when making the
transition to OOP
and
Java:
1.
Training
The
first step is some form of
education. Remember the
company's
investment
in code, and try not to
throw everything into
disarray for six to
nine
months while everyone
puzzles over how interfaces
work. Pick a
small
group for indoctrination,
preferably one composed of
people who
are
curious, work well together,
and can function as their
own support
network
while they're learning
Java.
An
alternative approach that is
sometimes suggested is the
education of
all
company levels at once,
including overview courses
for strategic
managers
as well as design and
programming courses for
project builders.
This
is especially good for
smaller companies making
fundamental shifts
in
the way they do things, or
at the division level of
larger companies.
Because
the cost is higher, however,
some may choose to start
with
Chapter
1: Introduction to Objects
93
project-level
training, do a pilot project
(possibly with an outside
mentor),
and
let the project team
become the teachers for
the rest of the
company.
2.
Low-risk project
Try
a low-risk project first and
allow for mistakes. Once
you've gained
some
experience, you can either
seed other projects from
members of this
first
team or use the team
members as an OOP technical
support staff.
This
first project may not
work right the first
time, so it should not
be
mission-critical
for the company. It should
be simple, self-contained,
and
instructive;
this means that it should
involve creating classes
that will be
meaningful
to the other programmers in
the company when they
get their
turn
to learn Java.
3.
Model from success
Seek
out examples of good
object-oriented design before
starting from
scratch.
There's a good probability
that someone has solved
your problem
already,
and if they haven't solved
it exactly you can probably
apply what
you've
learned about abstraction to
modify an existing design to
fit your
needs.
This is the general concept
of design
patterns, covered in
Thinking
in
Patterns with Java, downloadable
at .
4.
Use existing class
libraries
The
primary economic motivation
for switching to OOP is the
easy use of
existing
code in the form of class
libraries (in particular,
the Standard
Java
libraries, which are covered
throughout this book). The
shortest
application
development cycle will
result when you can
create and use
objects
from off-the-shelf libraries.
However, some new
programmers
don't
understand this, are unaware
of existing class libraries,
or, through
fascination
with the language, desire to
write classes that may
already
exist.
Your success with OOP
and Java will be optimized
if you make an
effort
to seek out and reuse
other people's code early in
the transition
process.
5.
Don't rewrite existing code in
Java
It
is not usually the best
use of your time to take
existing, functional
code
and
rewrite it in Java. (If you
must turn it into objects,
you can interface
to
the C or C++ code using
the Java Native Interface,
described in
94
Thinking
in Java
Appendix
B.) There are incremental
benefits, especially if the
code is
slated
for reuse. But chances
are you aren't going to
see the dramatic
increases
in productivity that you
hope for in your first
few projects unless
that
project is a new one. Java
and OOP shine best
when taking a project
from
concept to reality.
Management
obstacles
If
you're a manager, your job
is to acquire resources for
your team, to
overcome
barriers to your team's
success, and in general to
try to provide
the
most productive and
enjoyable environment so your
team is most
likely
to perform those miracles
that are always being
asked of you.
Moving
to Java falls in all three
of these categories, and it
would be
wonderful
if it didn't cost you
anything as well. Although
moving to Java
may
be cheaper--depending on your
constraints--than the
OOP
alternatives
for a team of C programmers
(and probably for
programmers
in
other procedural languages), it
isn't free, and there
are obstacles you
should
be aware of before trying to
sell the move to Java
within your
company
and embarking on the move
itself.
Startup
costs
The
cost of moving to Java is
more than just the
acquisition of Java
compilers
(the Sun Java compiler is
free, so this is hardly an
obstacle).
Your
medium- and long-term costs
will be minimized if you
invest in
training
(and possibly mentoring for
your first project) and
also if you
identify
and purchase class libraries
that solve your problem
rather than
trying
to build those libraries
yourself. These are
hard-money costs that
must
be factored into a realistic
proposal. In addition, there
are the
hidden
costs in loss of productivity
while learning a new
language and
possibly
a new programming environment.
Training and mentoring
can
certainly
minimize these, but team
members must overcome their
own
struggles
to understand the new
technology. During this
process they will
make
more mistakes (this is a
feature, because acknowledged
mistakes
are
the fastest path to
learning) and be less
productive. Even then,
with
some
types of programming problems,
the right classes, and
the right
development
environment, it's possible to be
more productive while
you're
learning Java (even
considering that you're
making more mistakes
and
writing fewer lines of code
per day) than if you'd
stayed with C.
Chapter
1: Introduction to Objects
95
Performance
issues
A
common question is, "Doesn't
OOP automatically make my
programs a
lot
bigger and slower?" The
answer is, "It depends."
The extra safety
features
in Java have traditionally
extracted a performance penalty
over a
language
like C++. Technologies such
as "hotspot" and
compilation
technologies
have improved the speed
significantly in most cases,
and
efforts
continue toward higher
performance.
When
your focus is on rapid
prototyping, you can throw
together
components
as fast as possible while
ignoring efficiency issues. If
you're
using
any third-party libraries,
these are usually already
optimized by
their
vendors; in any case it's
not an issue while you're in
rapid-
development
mode. When you have a
system that you like, if
it's small and
fast
enough, then you're done. If
not, you begin tuning
with a profiling
tool,
looking first for speedups
that can be done by
rewriting small
portions
of code. If that doesn't
help, you look for
modifications that
can
be
made in the underlying
implementation so no code that
uses a
particular
class needs to be changed.
Only if nothing else solves
the
problem
do you need to change the
design. The fact that
performance is so
critical
in that portion of the
design is an indicator that it
must be part of
the
primary design criteria. You
have the benefit of finding
this out early
using
rapid development.
If
you find a function that is
a particular bottleneck, you
can rewrite it in
C/C++
using Java's native
methods, the
subject of Appendix B.
Common
design errors
When
starting your team into
OOP and Java, programmers
will typically
go
through a series of common
design errors. This often
happens due to
insufficient
feedback from experts during
the design and
implementation
of
early projects, because no
experts have been developed
within the
company,
and because there may be
resistance to retaining
consultants.
It's
easy to feel that you
understand OOP too early in
the cycle and go
off
on
a bad tangent. Something
that's obvious to someone
experienced with
the
language may be a subject of
great internal debate for a
novice. Much
of
this trauma can be skipped
by using an experienced outside
expert for
training
and mentoring.
96
Thinking
in Java
Java
vs. C++?
Java
looks a lot like C++,
and so naturally it would
seem that C++ will
be
replaced
by Java. But I'm starting to
question this logic. For
one thing,
C++
still has some features
that Java doesn't, and
although there have
been
a lot of promises about Java
someday being as fast or
faster than
C++,
we've seen steady
improvements but no dramatic
breakthroughs.
Also,
there seems to be a continuing
interest in C++, so I don't
think that
language
is going away any time
soon. (Languages seem to
hang around.
Speaking
at one of my "Intermediate/Advanced Java
Seminars," Allen
Holub
asserted that the two
most commonly used languages
are Rexx and
COBOL,
in that order.)
I'm
beginning to think that the
strength of Java lies in a
slightly different
arena
than that of C++. C++ is a
language that doesn't try to
fit a mold.
Certainly
it has been adapted in a
number of ways to solve
particular
problems.
Some C++ tools combine
libraries, component models,
and
code-generation
tools to solve the problem
of developing windowed
end-
user
applications (for Microsoft
Windows). And yet, what do
the vast
majority
of Windows developers use?
Microsoft's Visual Basic
(VB). This
despite
the fact that VB produces
the kind of code that
becomes
unmanageable
when the program is only a
few pages long (and
syntax
that
can be positively mystifying). As
successful and popular as VB
is, it's
not
a very good example of
language design. It would be
nice to have the
ease
and power of VB without the
resulting unmanageable code.
And
that's
where I think Java will
shine: as the "next VB."
You may or may
not
shudder
to hear this, but think
about it: so much of Java is
intended to
make
it easy for the programmer
to solve application-level problems
like
networking
and cross-platform UI, and
yet it has a language design
that
allows
the creation of very large
and flexible bodies of code.
Add to this
the
fact that Java has
the most robust type
checking and error
handling
systems
I've ever seen in a language
and you have the
makings of a
significant
leap forward in programming
productivity.
Should
you use Java instead of
C++ for your project?
Other than Web
applets,
there are two issues to
consider. First, if you want
to use a lot of
existing
C++ libraries (and you'll
certainly get a lot of
productivity gains
Chapter
1: Introduction to Objects
97
there),
or if you have an existing C or
C++ code base, Java
might slow
your
development down rather than
speeding it up.
If
you're developing all your
code primarily from scratch,
then the
simplicity
of Java over C++ will
significantly shorten your
development
time--the
anecdotal evidence (stories
from C++ teams that
I've talked to
who
have switched to Java)
suggests a doubling of development
speed
over
C++. If Java performance
doesn't matter or you can
somehow
compensate
for it, sheer time-to-market
issues make it difficult to
choose
C++
over Java.
The
biggest issue is performance.
Interpreted Java has been
slow, even 20
to
50 times slower than C in
the original Java
interpreters. This
has
improved
greatly over time, but it
will still remain an
important number.
Computers
are about speed; if it
wasn't significantly faster to
do
something
on a computer then you'd do it by
hand. (I've even heard
it
suggested
that you start with
Java, to gain the short
development time,
then
use a tool and support
libraries to translate your
code to C++, if you
need
faster execution
speed.)
The
key to making Java feasible
for most development
projects is the
appearance
of speed improvements like
so-called "just-in time"
(JIT)
compilers,
Sun's own "hotspot"
technology, and even native
code
compilers.
Of course, native code
compilers will eliminate the
touted
cross-platform
execution of the compiled
programs, but they will
also
bring
the speed of the executable
closer to that of C and C++.
And cross-
compiling
a program in Java should be a
lot easier than doing so in
C or
C++.
(In theory, you just
recompile, but that promise
has been made
before
for other languages.)
You
can find comparisons of Java
and C++ and observations
about Java
realities
in the appendices of the
first edition of this book
(Available on
this
book's accompanying CD ROM, as
well as at ).
Summary
This
chapter attempts to give you
a feel for the broad
issues of object-
oriented
programming and Java,
including why OOP is
different, and why
Java
in particular is different, concepts of
OOP methodologies, and
finally
98
Thinking
in Java
the
kinds of issues you will
encounter when moving your
own company to
OOP
and Java.
OOP
and Java may not be
for everyone. It's important
to evaluate your
own
needs and decide whether
Java will optimally satisfy
those needs, or
if
you might be better off
with another programming
system (including
the
one you're currently using).
If you know that your
needs will be very
specialized
for the foreseeable future
and if you have specific
constraints
that
may not be satisfied by
Java, then you owe it to
yourself to investigate
the
alternatives18. Even if you eventually
choose Java as your
language,
you'll
at least understand what the
options were and have a
clear vision of
why
you took that
direction.
You
know what a procedural
program looks like: data
definitions and
function
calls. To find the meaning
of such a program you have
to work a
little,
looking through the function
calls and low-level concepts
to create a
model
in your mind. This is the
reason we need
intermediate
representations
when designing procedural
programs--by themselves,
these
programs tend to be confusing
because the terms of
expression are
oriented
more toward the computer
than to the problem you're
solving.
Because
Java adds many new
concepts on top of what you
find in a
procedural
language, your natural
assumption may be that the
main( )
in
a
Java program will be far
more complicated than for
the equivalent C
program.
Here, you'll be pleasantly
surprised: A well-written
Java
program
is generally far simpler and
much easier to understand
than the
equivalent
C program. What you'll see
are the definitions of the
objects
that
represent concepts in your
problem space (rather than
the issues of
the
computer representation) and
messages sent to those
objects to
represent
the activities in that
space. One of the delights
of object-
oriented
programming is that, with a
well-designed program, it's
easy to
understand
the code by reading it.
Usually there's a lot less
code as well,
because
many of your problems will
be solved by reusing existing
library
code.
18
In particular, I
recommend looking at Python
(http://www.Python.org).
Chapter
1: Introduction to Objects
99
2:
Everything
is
an Object
Although
it is based on C++, Java is
more of a "pure"
object-oriented
language.
Both
C++ and Java are
hybrid languages, but in
Java the designers
felt
that
the hybridization was not as
important as it was in C++. A
hybrid
language
allows multiple programming
styles; the reason C++ is
hybrid is
to
support backward compatibility
with the C language. Because
C++ is a
superset
of the C language, it includes
many of that
language's
undesirable
features, which can make
some aspects of C++
overly
complicated.
The
Java language assumes that
you want to do only
object-oriented
programming.
This means that before
you can begin you
must shift your
mindset
into an object-oriented world
(unless it's already
there).
The
benefit
of this initial effort is
the ability to program in a
language that is
simpler
to learn and to use than
many other OOP languages. In
this
chapter
we'll see the basic
components of a Java program
and we'll learn
that
everything in Java is an object,
even a Java program.
You
manipulate objects
with
references
Each
programming language has its
own means of manipulating
data.
Sometimes
the programmer must be
constantly aware of what
type of
manipulation
is going on. Are you
manipulating the object
directly, or are
you
dealing with some kind of
indirect representation (a pointer in C
or
C++)
that must be treated with a
special syntax?
101
All
this is simplified in Java.
You treat everything as an
object, so there is
a
single consistent syntax
that you use everywhere.
Although you treat
everything
as an object, the identifier
you manipulate is actually
a
"reference"
to an object1.
You might imagine this
scene as a television
(the
object)
with your remote control
(the reference). As long as
you're holding
this
reference, you have a
connection to the television,
but when someone
says
"change the channel" or
"lower the volume," what
you're
manipulating
is the reference, which in
turn modifies the object. If
you
want
to move around the room
and still control the
television, you take
the
remote/reference with you,
not the television.
Also,
the remote control can
stand on its own, with no
television. That is,
just
because you have a reference
doesn't mean there's
necessarily an
object
connected to it. So if you
want to hold a word or
sentence, you
create
a String
reference:
String
s;
But
here you've created
only
the
reference, not an object. If
you decided to
send
a message to s
at
this point, you'll get an
error (at run-time)
because
s
isn't
actually attached to anything
(there's no television). A
safer
practice,
then, is always to initialize a
reference when you create
it:
String
s = "asdf";
1
This can be a
flashpoint. There are those
who say "clearly, it's a
pointer," but this
presumes
an underlying implementation. Also, Java
references are much more akin
to
C++
references than pointers in
their syntax. In the first edition of
this book, I chose to
invent
a new term, "handle,"
because C++ references and
Java references have
some
important
differences. I was coming
out of C++ and did not
want to confuse the C++
programmers
whom I assumed would be the largest
audience for Java. In the
2nd edition, I
decided
that "reference" was the
more commonly used term,
and that anyone changing
from
C++ would have a lot more to
cope with than the terminology of
references, so they
might
as well jump in with both feet.
However, there are people who
disagree even with
the
term "reference." I read in
one book where it was
"completely wrong to say
that Java
supports
pass by reference," because
Java object identifiers
(according to that author) are
actually
"object
references." And (he goes
on) everything is actually
pass
by value. So
you're
not passing by reference,
you're "passing an object
reference by value." One
could
argue
for the precision of such
convoluted explanations, but I think my
approach
simplifies
the understanding of the concept without hurting
anything (well, the language
lawyers
may claim that I'm
lying to you, but I'll say
that I'm providing an
appropriate
abstraction.)
102
Thinking
in Java
However,
this uses a special Java
feature: strings can be
initialized with
quoted
text. Normally, you must
use a more general type of
initialization
for
objects.
You
must create
all
the objects
When
you create a reference, you
want to connect it with a
new object.
You
do so, in general, with the
new keyword.
new says,
"Make me a new
one
of these objects." So in the
above example, you can
say:
String
s = new String("asdf");
Not
only does this mean
"Make me a new String,"
but it also gives
information
about how
to
make the String
by
supplying an initial
character
string.
Of
course, String
is
not the only type
that exists. Java comes
with a
plethora
of ready-made types. What's
more important is that you
can
create
your own types. In fact,
that's the fundamental
activity in Java
programming,
and it's what you'll be
learning about in the rest
of this
book.
Where
storage lives
It's
useful to visualize some
aspects of how things are
laid out while
the
program
is running, in particular how
memory is arranged. There
are six
different
places to store data:
1.
Registers.
This is the fastest storage
because it exists in a
place
different
from that of other storage:
inside the processor.
However,
the
number of registers is severely
limited, so registers
are
allocated
by the compiler according to
its needs. You don't
have
direct
control, nor do you see
any evidence in your
programs that
registers
even exist.
2.
The
stack. This
lives in the general RAM
(random-access
memory)
area, but has direct
support from the processor
via its
stack
pointer. The
stack pointer is moved down
to create new
Chapter
2: Everything is an Object
103
memory
and moved up to release that
memory. This is an
extremely
fast and efficient way to
allocate storage, second
only to
registers.
The Java compiler must
know, while it is creating
the
program,
the exact size and
lifetime of all the data
that is stored on
the
stack, because it must
generate the code to move
the stack
pointer
up and down. This constraint
places limits on the
flexibility
of
your programs, so while some
Java storage exists on the
stack--
in
particular, object references--Java
objects themselves are
not
placed
on the stack.
3.
The
heap.
This is a general-purpose pool of
memory (also in the
RAM
area) where all Java
objects live. The nice
thing about the
heap
is that, unlike the stack,
the compiler doesn't need to
know
how
much storage it needs to
allocate from the heap or
how long
that
storage must stay on the
heap. Thus, there's a great
deal of
flexibility
in using storage on the
heap. Whenever you need
to
create
an object, you simply write
the code to create it using
new,
and
the storage is allocated on
the heap when that
code is executed.
Of
course there's a price you
pay for this flexibility: it
takes more
time
to allocate heap storage
than it does to allocate
stack storage
(that
is, if you even could
create
objects on the stack in
Java, as you
can
in C++).
4.
Static
storage. "Static" is
used here in the sense of
"in a fixed
location"
(although it's also in RAM).
Static storage contains
data
that
is available for the entire
time a program is running.
You can
use
the static
keyword
to specify that a particular
element of an
object
is static, but Java objects
themselves are never placed
in
static
storage.
5.
Constant
storage. Constant
values are often placed
directly in
the
program code, which is safe
since they can never
change.
Sometimes
constants are cordoned off
by themselves so that
they
can
be optionally placed in read-only
memory (ROM).
6.
Non-RAM
storage. If data
lives completely outside a
program it
can
exist while the program is
not running, outside the
control of
the
program. The two primary
examples of this are
streamed
objects,
in
which objects are turned
into streams of bytes,
generally
104
Thinking
in Java
to
be sent to another machine,
and persistent
objects, in which
the
objects
are placed on disk so they
will hold their state
even when
the
program is terminated. The
trick with these types of
storage is
turning
the objects into something
that can exist on the
other
medium,
and yet can be resurrected
into a regular
RAM-based
object
when necessary. Java
provides support for
lightweight
persistence,
and future versions of Java
might provide more
complete
solutions for
persistence.
Special
case: primitive types
There
is a group of types that
gets special treatment; you
can think of
these
as "primitive" types that
you use quite often in
your programming.
The
reason for the special
treatment is that to create an
object with new--
especially
a small, simple variable--isn't
very efficient because
new places
objects
on the heap. For these
types Java falls back on
the approach taken
by
C and C++. That is,
instead of creating the
variable using new,
an
"automatic"
variable is created that
is
not a reference. The
variable holds
the
value, and it's placed on
the stack so it's much
more efficient.
Java
determines the size of each
primitive type. These sizes
don't change
from
one machine architecture to
another as they do in most
languages.
This
size invariance is one
reason Java programs are so
portable.
Primitive
Size
Minimum
Maximum
Wrapper
type
type
boolean
--
--
--
Boolean
char
16-bit
Unicode
0
Unicode
216- 1
Character
byte
8-bit
-128
+127
Byte
short
16-bit
-215
+215--1
Short
int
32-bit
-231
+231--1
Integer
long
64-bit
-263
+263--1
Long
float
32-bit
IEEE754
IEEE754
Float
double
64-bit
IEEE754
IEEE754
Double
void
--
--
--
Void
All
numeric types are signed, so
don't go looking for
unsigned types.
Chapter
2: Everything is an Object
105
Table of Contents:
|
|||||