|
|||||
other
languages (especially C) and
are used to accessing
everything
without
restriction. By the end of
this chapter you should be
convinced of
the
value of access control in
Java.
The
concept of a library of components
and the control over
who can
access
the components of that
library is not complete,
however. There's
still
the question of how the
components are bundled
together into a
cohesive
library unit. This is
controlled with the
package
keyword
in
Java,
and the access specifiers
are affected by whether a
class is in the
same
package or in a separate package. So to
begin this chapter,
you'll
learn
how library components are
placed into packages. Then
you'll be
able
to understand the complete
meaning of the access
specifiers.
package:
the library unit
A
package is what you get
when you use the
import
keyword
to bring in
an
entire library, such
as
import
java.util.*;
This
brings in the entire utility
library that's part of the
standard Java
distribution.
Since, for example, the
class ArrayList
is
in java.util,
you
can
now either specify the
full name java.util.ArrayList
(which
you can
do
without the import
statement),
or you can simply say
ArrayList
(because
of the import).
If
you want to bring in a
single class, you can
name that class in
the
import
statement
import
java.util.ArrayList;
Now
you can use ArrayList
with
no qualification. However, none of
the
other
classes in java.util
are
available.
The
reason for all this
importing is to provide a mechanism to
manage
"name
spaces." The names of all
your class members are
insulated from
each
other. A method f(
) inside
a class A
will
not clash with an f( ) that
has
the same signature (argument
list) in class B.
But what about the
class
names?
Suppose you create a
stack
class
that is installed on a
machine
that
already has a stack
class
that's written by someone
else? With Java
on
the Internet, this can
happen without the user
knowing it, since
classes
244
Thinking
in Java
can
get downloaded automatically in
the process of running a
Java
program.
This
potential clashing of names is
why it's important to have
complete
control
over the name spaces in
Java, and to be able to
create a completely
unique
name regardless of the
constraints of the
Internet.
So
far, most of the examples in
this book have existed in a
single file and
have
been designed for local
use, and haven't bothered
with package
names.
(In this case the
class name is placed in the
"default package.")
This
is certainly an option, and
for simplicity's sake this
approach will be
used
whenever possible throughout
the rest of this book.
However, if
you're
planning to create libraries or
programs that are friendly
to other
Java
programs on the same
machine, you must think
about preventing
class
name clashes.
When
you create a source-code
file for Java, it's
commonly called a
compilation
unit (sometimes a
translation
unit).
Each compilation unit
must
have a name ending in
.java,
and inside the compilation
unit there
can
be a public
class
that must have the
same name as the file
(including
capitalization,
but excluding the .java
filename
extension). There can
be
only
one
public
class
in each compilation unit,
otherwise the
compiler
will
complain. The rest of the
classes in that compilation
unit, if there are
any,
are hidden from the
world outside that package
because they're not
public,
and they comprise "support"
classes for the main
public
class.
When
you compile a .java
file
you get an output file
with exactly the
same
name
but an extension of .class
for
each class in the .java
file.
Thus you
can
end up with quite a few
.class
files
from a small number of
.java
files.
If you've programmed with a
compiled language, you might
be used
to
the compiler spitting out an
intermediate form (usually an
"obj" file)
that
is then packaged together
with others of its kind
using a linker (to
create
an executable file) or a librarian
(to create a library).
That's not
how
Java works. A working
program is a bunch of .class
files,
which can
be
packaged and compressed into
a JAR file (using Java's
jar archiver).
Chapter
5: Hiding the Implementation
245
The
Java interpreter is responsible
for finding, loading, and
interpreting
these
files1.
A
library is also a bunch of
these class files. Each
file has one class
that is
public
(you're
not forced to have a
public
class,
but it's typical), so
there's
one component for each
file. If you want to say
that all these
components
(that are in their own
separate .java
and
.class
files)
belong
together,
that's where the package
keyword
comes in.
When
you say:
package
mypackage;
at
the beginning of a file (if
you use a package
statement,
it must
appear
as
the first noncomment in the
file), you're stating that
this compilation
unit
is part of a library named
mypackage.
Or, put another way,
you're
saying
that the public
class
name within this compilation
unit is under
the
umbrella of the name
mypackage,
and if anyone wants to use
the
name
they must either fully
specify the name or use
the import
keyword
in
combination with mypackage
(using
the choices given
previously).
Note
that the convention for
Java package names is to use
all lowercase
letters,
even for intermediate
words.
For
example, suppose the name of
the file is MyClass.java.
This means
there
can be one and only
one public
class
in that file, and the
name of
that
class must be MyClass
(including
the capitalization):
package
mypackage;
public
class MyClass {
//
. . .
Now,
if someone wants to use
MyClass
or,
for that matter, any of
the
other
public
classes
in mypackage,
they must use the
import
keyword
to
make the name or names in
mypackage
available.
The alternative is
to
give the fully qualified
name:
mypackage.MyClass
m = new mypackage.MyClass();
1
There's nothing in
Java that forces the use of
an interpreter. There exist native-code
Java
compilers
that generate a single executable
file.
246
Thinking
in Java
The
import
keyword
can make this much
cleaner:
import
mypackage.*;
//
. . .
MyClass
m = new MyClass();
It's
worth keeping in mind that
what the package
and
import
keywords
allow
you to do, as a library
designer, is to divide up the
single global
name
space so you won't have
clashing names, no matter
how many
people
get on the Internet and
start writing classes in
Java.
Creating
unique package names
You
might observe that, since a
package never really gets
"packaged" into
a
single file, a package could
be made up of many .class
files,
and things
could
get a bit cluttered. To
prevent this, a logical
thing to do is to place
all
the
.class
files
for a particular package
into a single directory;
that is, use
the
hierarchical file structure of
the operating system to your
advantage.
This
is one way that Java
references the problem of
clutter; you'll see
the
other
way later when the
jar utility
is introduced.
Collecting
the package files into a
single subdirectory solves
two other
problems:
creating unique package
names, and finding those
classes that
might
be buried in a directory structure
someplace. This is
accomplished,
as
was introduced in Chapter 2, by
encoding the path of the
location of the
.class
file
into the name of the
package.
The compiler enforces this,
but
by
convention, the first part
of the package
name
is the Internet
domain
name
of the creator of the class,
reversed. Since Internet
domain names
are
guaranteed to be unique, if
you
follow this convention it's
guaranteed
that
your package
name
will be unique and thus
you'll never have a
name
clash. (That is, until
you lose the domain
name to someone else
who
starts
writing Java code with
the same path names as
you did.) Of course,
if
you don't have your
own domain name then
you must fabricate an
unlikely
combination (such as your
first and last name) to
create unique
package
names. If you've decided to
start publishing Java code
it's worth
the
relatively small effort to
get a domain name.
The
second part of this trick is
resolving the package
name
into a
directory
on your machine, so when the
Java program runs and it
needs to
load
the .class
file
(which it does dynamically, at
the point in the
program
Chapter
5: Hiding the Implementation
247
where
it needs to create an object of
that particular class, or
the first time
you
access a static
member
of the class), it can locate
the directory where
the
.class
file
resides.
The
Java interpreter proceeds as
follows. First, it finds the
environment
variable
CLASSPATH (set via the
operating system, sometimes by
the
installation
program that installs Java
or a Java-based tool on
your
machine).
CLASSPATH contains one or
more directories that are
used as
roots
for a search for .class
files.
Starting at that root, the
interpreter will
take
the package name and
replace each dot with a
slash to generate a
path
name from the CLASSPATH
root (so package
foo.bar.baz
becomes
foo\bar\baz
or
foo/bar/baz
or
possibly something
else,
depending
on your operating system).
This is then concatenated to
the
various
entries in the CLASSPATH.
That's where it looks for
the .class
file
with the name corresponding
to the class you're trying
to create. (It
also
searches some standard
directories relative to where
the Java
interpreter
resides).
To
understand this, consider my
domain name, which is
bruceeckel.com.
By reversing this, com.bruceeckel
establishes
my
unique
global name for my classes.
(The com, edu, org,
etc., extension was
formerly
capitalized in Java packages,
but this was changed in
Java 2 so
the
entire package name is
lowercase.) I can further
subdivide this by
deciding
that I want to create a
library named simple,
so I'll end up with
a
package name:
package
com.bruceeckel.simple;
Now
this package name can be
used as an umbrella name
space for the
following
two files:
//:
com:bruceeckel:simple:Vector.java
//
Creating a package.
package
com.bruceeckel.simple;
public
class Vector {
public
Vector() {
System.out.println(
"com.bruceeckel.util.Vector");
}
}
///:~
248
Thinking
in Java
When
you create your own
packages, you'll discover
that the package
statement
must be the first noncomment
code in the file. The
second file
looks
much the same:
//:
com:bruceeckel:simple:List.java
//
Creating a package.
package
com.bruceeckel.simple;
public
class List {
public
List() {
System.out.println(
"com.bruceeckel.util.List");
}
}
///:~
Both
of these files are placed in
the subdirectory on my
system:
C:\DOC\JavaT\com\bruceeckel\simple
If
you walk back through
this, you can see
the package name
com.bruceeckel.simple,
but what about the
first portion of the
path?
That's
taken care of in the
CLASSPATH environment variable,
which is,
on
my machine:
CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT
You
can see that the
CLASSPATH can contain a
number of alternative
search
paths.
There's
a variation when using JAR
files, however. You must
put the name
of
the JAR file in the
classpath, not just the
path where it's located. So
for
a
JAR named grape.jar
your
classpath would
include:
CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar
Once
the classpath is set up
properly, the following file
can be placed in
any
directory:
//:
c05:LibTest.java
//
Uses the library.
import
com.bruceeckel.simple.*;
public
class LibTest {
Chapter
5: Hiding the Implementation
249
public
static void main(String[] args) {
Vector
v = new Vector();
List
l = new List();
}
}
///:~
When
the compiler encounters the
import
statement,
it begins searching
at
the directories specified by
CLASSPATH, looking for
subdirectory
com\bruceeckel\simple,
then seeking the compiled
files of the
appropriate
names (Vector.class
for
Vector
and
List.class
for
List).
Note
that both the classes
and the desired methods in
Vector
and
List
must
be public.
Setting
the CLASSPATH has been
such a trial for beginning
Java users (it
was
for me, when I started)
that Sun made the
JDK in Java 2 a bit
smarter.
You'll find that, when
you install it, even if
you don't set a
CLASSPATH
you'll be able to compile
and run basic Java
programs. To
compile
and run the source-code
package for this book
(available on the
CD
ROM packaged with this
book, or at ),
however,
you
will need to make some
modifications to your CLASSPATH
(these are
explained
in the source-code
package).
Collisions
What
happens if two libraries are
imported via * and they
include the
same
names? For example, suppose
a program does this:
import
com.bruceeckel.simple.*;
import
java.util.*;
Since
java.util.*
also
contains a Vector
class,
this causes a
potential
collision.
However, as long as you
don't write the code
that actually causes
the
collision, everything is OK--this is
good because otherwise you
might
end
up doing a lot of typing to
prevent collisions that
would never happen.
The
collision does
occur
if you now try to make a
Vector:
Vector
v = new Vector();
Which
Vector
class
does this refer to?
The compiler can't know,
and the
reader
can't know either. So the
compiler complains and
forces you to be
explicit.
If I want the standard Java
Vector,
for example, I must
say:
250
Thinking
in Java
java.util.Vector
v = new java.util.Vector();
Since
this (along with the
CLASSPATH) completely specifies
the location
of
that Vector,
there's no need for the
import
java.util.* statement
unless
I'm using something else
from java.util.
A
custom tool library
With
this knowledge, you can
now create your own
libraries of tools to
reduce
or eliminate duplicate code.
Consider, for example,
creating an
alias
for System.out.println(
) to
reduce typing. This can be
part of a
package
called tools:
//:
com:bruceeckel:tools:P.java
//
The P.rint & P.rintln shorthand.
package
com.bruceeckel.tools;
public
class P {
public
static void rint(String s) {
System.out.print(s);
}
public
static void rintln(String s) {
System.out.println(s);
}
}
///:~
You
can use this shorthand to
print a String
either
with a newline
(P.rintln(
))
or without a newline (P.rint(
)).
You
can guess that the
location of this file must
be in a directory that
starts
at one of the CLASSPATH
locations, then
continues
com/bruceeckel/tools.
After compiling, the
P.class
file
can be used
anywhere
on your system with an
import
statement:
//:
c05:ToolTest.java
//
Uses the tools library.
import
com.bruceeckel.tools.*;
public
class ToolTest {
public
static void main(String[] args) {
P.rintln("Available
from now on!");
P.rintln(""
+ 100); // Force it to be a String
Chapter
5: Hiding the Implementation
251
P.rintln(""
+ 100L);
P.rintln(""
+ 3.14159);
}
}
///:~
Notice
that all objects can
easily be forced into
String
representations
by
putting
them in a String
expression;
in the above case, starting
the
expression
with an empty String
does
the trick. But this
brings up an
interesting
observation. If you call
System.out.println(100),
it works
without
casting it to a String.
With some extra overloading,
you can get
the
P class
to do this as well (this is an
exercise at the end of this
chapter).
So
from now on, whenever
you come up with a useful
new utility, you
can
add
it to the tools
directory.
(Or to your own personal
util or
tools
directory.)
Using
imports to change behavior
A
feature that is missing from
Java is C's conditional
compilation,
which
allows
you to change a switch and
get different behavior
without changing
any
other code. The reason
such a feature was left
out of Java is
probably
because
it is most often used in C to
solve cross-platform issues:
different
portions
of the code are compiled
depending on the platform
that the
code
is being compiled for. Since
Java is intended to be
automatically
cross-platform,
such a feature should not be
necessary.
However,
there are other valuable
uses for conditional
compilation. A very
common
use is for debugging code.
The debugging features are
enabled
during
development, and disabled in
the shipping product. Allen
Holub
(www.holub.com)
came up with the idea of
using packages to
mimic
conditional
compilation. He used this to
create a Java version of C's
very
useful
assertion
mechanism, whereby
you can say "this
should be true" or
"this
should be false" and if the
statement doesn't agree with
your
assertion
you'll find out about
it. Such a tool is quite
helpful during
debugging.
Here
is the class that you'll
use for debugging:
//:
com:bruceeckel:tools:debug:Assert.java
//
Assertion tool for debugging.
package
com.bruceeckel.tools.debug;
252
Thinking
in Java
public
class Assert {
private
static void perr(String msg) {
System.err.println(msg);
}
public
final static void is_true(boolean exp) {
if(!exp)
perr("Assertion failed");
}
public
final static void is_false(boolean exp){
if(exp)
perr("Assertion failed");
}
public
final static void
is_true(boolean
exp, String msg) {
if(!exp)
perr("Assertion failed: " + msg);
}
public
final static void
is_false(boolean
exp, String msg) {
if(exp)
perr("Assertion failed: " + msg);
}
}
///:~
This
class simply encapsulates
Boolean tests, which print
error messages
if
they fail. In Chapter 10,
you'll learn about a more
sophisticated tool
for
dealing
with errors called exception
handling, but
the perr(
) method
will
work fine in the
meantime.
The
output is printed to the
console standard
error stream by
writing to
System.err.
When
you want to use this
class, you add a line in
your program:
import
com.bruceeckel.tools.debug.*;
To
remove the assertions so you
can ship the code, a
second Assert
class
is
created, but in a different
package:
//:
com:bruceeckel:tools:Assert.java
//
Turning off the assertion output
//
so you can ship the program.
package
com.bruceeckel.tools;
public
class Assert {
Chapter
5: Hiding the Implementation
253
public
final static void is_true(boolean exp){}
public
final static void is_false(boolean exp){}
public
final static void
is_true(boolean
exp, String msg) {}
public
final static void
is_false(boolean
exp, String msg) {}
}
///:~
Now
if you change the previous
import
statement
to:
import
com.bruceeckel.tools.*;
The
program will no longer print
assertions. Here's an
example:
//:
c05:TestAssert.java
//
Demonstrating the assertion tool.
//
Comment the following, and uncomment the
//
subsequent line to change assertion behavior:
import
com.bruceeckel.tools.debug.*;
//
import com.bruceeckel.tools.*;
public
class TestAssert {
public
static void main(String[] args) {
Assert.is_true((2
+ 2) == 5);
Assert.is_false((1
+ 1) == 2);
Assert.is_true((2
+ 2) == 5, "2 + 2 == 5");
Assert.is_false((1
+ 1) == 2, "1 +1 != 2");
}
}
///:~
By
changing the package
that's
imported, you change your
code from the
debug
version to the production
version. This technique can
be used for
any
kind of conditional
code.
Package
caveat
It's
worth remembering that
anytime you create a
package, you
implicitly
specify
a directory structure when
you give the package a
name. The
package
must
live
in the directory indicated by
its name, which must be
a
directory
that is searchable starting
from the CLASSPATH.
Experimenting
with the package
keyword
can be a bit frustrating at
first,
because
unless you adhere to the
package-name to directory-path
rule,
254
Thinking
in Java
you'll
get a lot of mysterious
run-time messages about not
being able to
find
a particular class, even if
that class is sitting there
in the same
directory.
If you get a message like
this, try commenting out
the package
statement,
and if it runs you'll know
where the problem
lies.
Java
access specifiers
When
used, the Java access
specifiers public,
protected,
and
private
are
placed in front of each
definition for each member
in your class,
whether
it's a field or a method.
Each access specifier
controls the access
for
only that particular
definition. This is a distinct
contrast to C++, in
which
the access specifier
controls all the definitions
following it until
another
access specifier comes
along.
One
way or another, everything
has some kind of access
specified for it. In
the
following sections, you'll
learn all about the
various types of
access,
starting
with the default
access.
"Friendly"
What
if you give no access
specifier at all, as in all
the examples before
this
chapter? The default access
has no keyword, but it is
commonly
referred
to as "friendly." It means that
all the other classes in
the current
package
have access to the friendly
member, but to all the
classes outside
of
this package the member
appears to be private.
Since a compilation
unit--a
file--can belong only to a
single package, all the
classes within a
single
compilation unit are
automatically friendly with
each other. Thus,
friendly
elements are also said to
have package
access.
Friendly
access allows you to group
related classes together in a
package
so
that they can easily
interact with each other.
When you put
classes
together
in a package (thus granting
mutual access to their
friendly
members;
e.g., making them "friends")
you "own" the code in
that
package.
It makes sense that only
code you own should
have friendly
access
to other code you own.
You could say that
friendly access gives
a
meaning
or a reason for grouping
classes together in a package. In
many
languages
the way you organize
your definitions in files
can be willy-nilly,
but
in Java you're compelled to
organize them in a sensible
fashion. In
Chapter
5: Hiding the Implementation
255
addition,
you'll probably want to
exclude classes that
shouldn't have
access
to the classes being defined
in the current
package.
The
class controls which code
has access to its members.
There's no magic
way
to "break in." Code from
another package can't show
up and say, "Hi,
I'm
a friend of Bob's!"
and expect to see the
protected,
friendly, and
private
members
of Bob.
The only way to grant
access to a member is
to:
1.
Make
the member public.
Then everybody, everywhere,
can access
it.
2.
Make
the member friendly by
leaving off any access
specifier, and
put
the other classes in the
same package. Then the
other classes
can
access the member.
3.
As
you'll see in Chapter 6,
when inheritance is introduced,
an
inherited
class can access a protected
member
as well as a public
member
(but not private
members).
It can access
friendly
members
only if the two classes
are in the same package.
But don't
worry
about that now.
4.
Provide
"accessor/mutator" methods (also
known as "get/set"
methods)
that read and change
the value. This is the
most civilized
approach
in terms of OOP, and it is
fundamental to JavaBeans, as
you'll
see in Chapter 13.
public:
interface access
When
you use the public
keyword,
it means that the member
declaration
that
immediately follows public
is
available to everyone, in particular
to
the
client programmer who uses
the library. Suppose you
define a package
dessert
containing
the following compilation
unit:
//:
c05:dessert:Cookie.java
//
Creates a library.
package
c05.dessert;
public
class Cookie {
public
Cookie() {
System.out.println("Cookie
constructor");
}
256
Thinking
in Java
void
bite() { System.out.println("bite"); }
}
///:~
Remember,
Cookie.java
must
reside in a subdirectory called
dessert,
in
a
directory under c05
(indicating
Chapter 5 of this book) that
must be
under
one of the CLASSPATH
directories. Don't make the
mistake of
thinking
that Java will always
look at the current
directory as one of
the
starting
points for searching. If you
don't have a `.'
as one of the paths
in
your
CLASSPATH, Java won't look
there.
Now
if you create a program that
uses Cookie:
//:
c05:Dinner.java
//
Uses the library.
import
c05.dessert.*;
public
class Dinner {
public
Dinner() {
System.out.println("Dinner
constructor");
}
public
static void main(String[] args) {
Cookie
x = new Cookie();
//!
x.bite(); // Can't access
}
}
///:~
you
can create a Cookie
object,
since its constructor is
public
and
the
class
is public.
(We'll look more at the
concept of a public
class
later.)
However,
the bite(
) member
is inaccessible inside Dinner.java
since
bite(
) is
friendly only within package
dessert.
The
default package
You
might be surprised to discover
that the following code
compiles, even
though
it would appear that it
breaks the rules:
//:
c05:Cake.java
//
Accesses a class in a
//
separate compilation unit.
class
Cake {
public
static void main(String[] args) {
Chapter
5: Hiding the Implementation
257
Pie
x = new Pie();
x.f();
}
}
///:~
In
a second file, in the same
directory:
//:
c05:Pie.java
//
The other class.
class
Pie {
void
f() { System.out.println("Pie.f()"); }
}
///:~
You
might initially view these
as completely foreign files,
and yet Cake
is
able
to create a Pie
object
and call its f(
) method!
(Note that you
must
have
`.' in your CLASSPATH in
order for the files to
compile.) You'd
typically
think that Pie
and
f( ) are
friendly and therefore not
available to
Cake.
They are
friendly--that
part is correct. The reason
that they are
available
in Cake.java
is
because they are in the
same directory and
have
no
explicit package name. Java
treats files like this as
implicitly part of
the
"default
package" for that directory,
and therefore friendly to
all the other
files
in that directory.
private:
you can't touch that!
The
private
keyword
means that no one can
access that member
except
that
particular class, inside
methods of that class. Other
classes in the
same
package cannot access
private
members,
so it's as if you're
even
insulating
the class against yourself.
On the other hand, it's
not unlikely
that
a package might be created by
several people collaborating
together,
so
private
allows
you to freely change that
member without concern
that
it
will affect another class in
the same package.
The
default "friendly" package
access often provides an
adequate amount
of
hiding; remember, a "friendly"
member is inaccessible to the
user of the
package.
This is nice, since the
default access is the one
that you normally
use
(and the one that
you'll get if you forget to
add any access
control).
Thus,
you'll typically think about
access for the members
that you
explicitly
want to make public
for
the client programmer, and
as a result,
you
might not initially think
you'll use the private
keyword
often since
258
Thinking
in Java
it's
tolerable to get away
without it. (This is a
distinct contrast with
C++.)
However,
it turns out that the
consistent use of private
is
very important,
especially
where multithreading is concerned.
(As you'll see in
Chapter
14.)
Here's
an example of the use of
private:
//:
c05:IceCream.java
//
Demonstrates "private" keyword.
class
Sundae {
private
Sundae() {}
static
Sundae makeASundae() {
return
new Sundae();
}
}
public
class IceCream {
public
static void main(String[] args) {
//!
Sundae x = new Sundae();
Sundae
x = Sundae.makeASundae();
}
}
///:~
This
shows an example in which
private
comes
in handy: you might
want
to
control how an object is
created and prevent someone
from directly
accessing
a particular constructor (or
all of them). In the example
above,
you
cannot create a Sundae
object
via its constructor; instead
you must
call
the makeASundae(
) method
to do it for you2.
Any
method that you're certain
is only a "helper" method
for that class
can
be made private,
to
ensure that you don't
accidentally use it
elsewhere
in the package and thus
prohibit yourself from
changing or
removing
the method. Making a method
private
guarantees
that you
retain
this option.
2
There's another
effect in this case: Since the
default constructor is the only
one defined,
and
it's private,
it will prevent inheritance of this
class. (A subject that will
be introduced
in
Chapter 6.)
Chapter
5: Hiding the Implementation
259
The
same is true for a private
field
inside a class. Unless you
must expose
the
underlying implementation (which is a
much rarer situation than
you
might
think), you should make
all fields private.
However, just because
a
reference
to an object is private
inside
a class doesn't mean that
some
other
object can't have a
public
reference
to the same object.
(See
Appendix
A for issues about
aliasing.)
protected:
"sort of friendly"
The
protected
access
specifier requires a jump
ahead to understand.
First,
you should be aware that
you don't need to understand
this section
to
continue through this book
up through inheritance (Chapter
6). But for
completeness,
here is a brief description
and example using protected.
The
protected
keyword
deals with a concept called
inheritance,
which
takes
an existing class and adds
new members to that class
without
touching
the existing class, which we
refer to as the base
class. You
can
also
change the behavior of
existing members of the
class. To inherit
from
an
existing class, you say
that your new class
extends
an
existing class,
like
this:
class
Foo extends Bar {
The
rest of the class definition
looks the same.
If
you create a new package
and you inherit from a
class in another
package,
the only members you
have access to are the
public
members
of
the
original package. (Of
course, if you perform the
inheritance in the
same
package,
you have the normal
package access to all the
"friendly"
members.)
Sometimes the creator of the
base class would like to
take a
particular
member and grant access to
derived classes but not
the world
in
general. That's what
protected
does.
If you refer back to the
file
Cookie.java,
the following class
cannot
access
the "friendly"
member:
//:
c05:ChocolateChip.java
//
Can't access friendly member
//
in another class.
import
c05.dessert.*;
public
class ChocolateChip extends Cookie {
public
ChocolateChip() {
260
Thinking
in Java
System.out.println(
"ChocolateChip
constructor");
}
public
static void main(String[] args) {
ChocolateChip
x = new ChocolateChip();
//!
x.bite(); // Can't access bite
}
}
///:~
One
of the interesting things
about inheritance is that if a
method bite(
)
exists
in class Cookie,
then it also exists in any
class inherited from
Cookie.
But since bite(
) is
"friendly" in a foreign package,
it's
unavailable
to us in this one. Of course,
you could make it public,
but
then
everyone would have access
and maybe that's not
what you want. If
we
change the class Cookie
as
follows:
public
class Cookie {
public
Cookie() {
System.out.println("Cookie
constructor");
}
protected
void bite() {
System.out.println("bite");
}
}
then
bite( )
still
has "friendly" access within
package dessert,
but it is
also
accessible to anyone inheriting
from Cookie.
However, it is not
public.
Interface
and
implementation
Access
control is often referred to as
implementation
hiding.
Wrapping
data
and methods within classes
in combination with
implementation
hiding
is often called encapsulation3. The result is a data
type with
characteristics
and behaviors.
3
However,
people often refer to implementation
hiding alone as encapsulation.
Chapter
5: Hiding the Implementation
261
Access
control puts boundaries
within a data type for
two important
reasons.
The first is to establish
what the client programmers
can and
can't
use. You can build
your internal mechanisms
into the structure
without
worrying that the client
programmers will accidentally
treat the
internals
as part of the interface
that they should be
using.
This
feeds directly into the
second reason, which is to
separate the
interface
from the implementation. If
the structure is used in a
set of
programs,
but client programmers can't
do anything but send
messages to
the
public
interface,
then you can change
anything that's not
public
(e.g.,
"friendly," protected,
or private)
without requiring
modifications
to
client code.
We're
now in the world of
object-oriented programming, where a
class
is
actually
describing "a class of objects," as
you would describe a class
of
fishes
or a class of birds. Any
object belonging to this
class will share
these
characteristics
and behaviors. The class is
a description of the way
all
objects
of this type will look
and act.
In
the original OOP language,
Simula-67, the keyword
class
was
used to
describe
a new data type. The
same keyword has been
used for most
object-oriented
languages. This is the focal
point of the whole
language:
the
creation of new data types
that are more than
just boxes containing
data
and methods.
The
class is the fundamental OOP
concept in Java. It is one of
the
keywords
that will not
be
set in bold in this book--it
becomes annoying
with
a word repeated as often as
"class."
For
clarity, you might prefer a
style of creating classes
that puts the
public
members
at the beginning, followed by
the protected,
friendly,
and
private
members.
The advantage is that the
user of the class
can
then
read down from the
top and see first
what's important to them
(the
public
members,
because they can be accessed
outside the file), and
stop
reading
when they encounter the
non-public
members,
which are part of
the
internal implementation:
public
class X {
public
void pub1( ) { /* . . . */ }
public
void pub2( ) { /* . . . */ }
262
Thinking
in Java
public
void pub3( )
{
/*
.
.
.
*/
}
private
void priv1(
)
{
/*
.
.
.
*/ }
private
void priv2(
)
{
/*
.
.
.
*/ }
private
void priv3(
)
{
/*
.
.
.
*/ }
private
int i;
//
. . .
}
This
will make it only partially
easier to read because the
interface and
implementation
are still mixed together.
That is, you still
see the source
code--the
implementation--because it's right
there in the class.
In
addition,
the comment documentation
supported by javadoc (described
in
Chapter
2) lessens the importance of
code readability by the
client
programmer.
Displaying the interface to
the consumer of a class is
really
the
job of the class
browser, a tool
whose job is to look at all
the available
classes
and show you what
you can do with them
(i.e., what members
are
available)
in a useful fashion. By the
time you read this,
browsers should
be
an expected part of any good
Java development
tool.
Class
access
In
Java, the access specifiers
can also be used to
determine which
classes
within
a
library will be available to
the users of that library.
If you want a
class
to be available to a client programmer,
you place the public
keyword
somewhere before the opening
brace of the class body.
This
controls
whether the client
programmer can even create
an object of the
class.
To
control the access of a
class, the specifier must
appear before the
keyword
class.
Thus you can
say:
public
class Widget {
Now
if the name of your library
is mylib
any
client programmer can
access
Widget
by
saying
import
mylib.Widget;
or
import
mylib.*;
Chapter
5: Hiding the Implementation
263
However,
there's an extra set of
constraints:
1.
There
can be only one public
class
per compilation unit (file).
The
idea
is that each compilation
unit has a single public
interface
represented
by that public
class.
It can have as many
supporting
"friendly"
classes as you want. If you
have more than one
public
class
inside a compilation unit,
the compiler will give
you an error
message.
2.
The
name of the public
class
must exactly match the
name of the
file
containing the compilation
unit, including capitalization. So
for
Widget,
the name of the file
must be Widget.java,
not
widget.java
or
WIDGET.java.
Again, you'll get a
compile-time
error
if they don't agree.
3.
It
is possible, though not
typical, to have a compilation
unit with no
public
class
at all. In this case, you
can name the file
whatever you
like.
What
if you've got a class inside
mylib that
you're just using to
accomplish
the tasks performed by
Widget
or
some other public
class
in
mylib?
You don't want to go to the
bother of creating documentation
for
the
client programmer, and you
think that sometime later
you might want
to
completely change things and
rip out your class
altogether, substituting
a
different one. To give you
this flexibility, you need
to ensure that no
client
programmers become dependent on
your particular
implementation
details hidden inside
mylib.
To accomplish this, you
just
leave
the public
keyword
off the class, in which
case it becomes
friendly.
(That
class can be used only
within that package.)
Note
that a class cannot be
private
(that
would make it accessible to
no
one
but the class), or protected4. So you have only
two choices for
class
access:
"friendly" or public.
If you don't want anyone
else to have access
to
that class, you can
make all the constructors
private,
thereby
4
Actually, an
inner
class can be private
or protected, but that's a special
case. These will
be
introduced in Chapter 7.
264
Thinking
in Java
preventing
anyone but you, inside a
static
member
of the class, from
creating
an object of that
class5.
Here's an example:
//:
c05:Lunch.java
//
Demonstrates class access
specifiers.
//
Make a class effectively private
//
with private constructors:
class
Soup {
private
Soup() {}
//
(1) Allow creation via static method:
public
static Soup makeSoup() {
return
new Soup();
}
//
(2) Create a static object and
//
return a reference upon request.
//
(The "Singleton" pattern):
private
static Soup ps1 = new Soup();
public
static Soup access() {
return
ps1;
}
public
void f() {}
}
class
Sandwich { // Uses Lunch
void
f() { new Lunch(); }
}
//
Only one public class allowed per file:
public
class Lunch {
void
test() {
//
Can't do this! Private constructor:
//!
Soup priv1 = new Soup();
Soup
priv2 = Soup.makeSoup();
Sandwich
f1 = new Sandwich();
Soup.access().f();
}
}
///:~
5
You can also
do it by inheriting (Chapter 6) from that
class.
Chapter
5: Hiding the Implementation
265
Up
to now, most of the methods
have been returning either
void
or
a
primitive
type, so the
definition:
public
static Soup access() {
return
ps1;
}
might
look a little confusing at
first. The word before
the method name
(access)
tells what the method
returns. So far this has
most often been
void,
which
means it returns nothing.
But you can also
return a reference
to
an object, which is what
happens here. This method
returns a reference
to
an object of class Soup.
The
class
Soup shows
how to prevent direct
creation of a class by
making
all the constructors
private.
Remember that if you
don't
explicitly
create at least one
constructor, the default
constructor (a
constructor
with no arguments) will be
created for you. By writing
the
default
constructor, it won't be created
automatically. By making it
private,
no one can create an object
of that class. But now
how does
anyone
use this class? The
above example shows two
options. First, a
static
method
is created that creates a
new Soup
and
returns a reference
to
it. This could be useful if
you want to do some extra
operations on the
Soup
before
returning it, or if you want
to keep count of how many
Soup
objects
to create (perhaps to restrict
their population).
The
second option uses what's
called a design
pattern, which is
covered in
Thinking
in Patterns with Java, downloadable
at .
This
particular pattern is called a
"singleton" because it allows
only a
single
object to ever be created.
The object of class
Soup is
created as a
static
private member of
Soup,
so there's one and only
one, and you
can't
get at it except through the
public
method
access(
).
As
previously mentioned, if you
don't put an access
specifier for class
access
it defaults to "friendly." This
means that an object of that
class can
be
created by any other class
in the package, but not
outside the package.
(Remember,
all the files within
the same directory that
don't have explicit
package
declarations
are implicitly part of the
default package for
that
directory.)
However, if a static
member
of that class is public,
the client
programmer
can still access that
static
member
even though they
cannot
create
an object of that
class.
266
Thinking
in Java
Summary
In
any relationship it's
important to have boundaries
that are respected by
all
parties involved. When you
create a library, you
establish a
relationship
with the user of that
library--the client programmer--who
is
another
programmer, but one putting
together an application or
using
your
library to build a bigger
library.
Without
rules, client programmers
can do anything they want
with all the
members
of a class, even if you
might prefer they don't
directly
manipulate
some of the members.
Everything's naked to the
world.
This
chapter looked at how
classes are built to form
libraries; first, the
way
a group of classes is packaged
within a library, and
second, the way
the
class controls access to its
members.
It
is estimated that a C programming
project begins to break
down
somewhere
between 50K and 100K
lines of code because C has
a single
"name
space" so names begin to
collide, causing an extra
management
overhead.
In Java, the package
keyword,
the package naming
scheme,
and
the import
keyword
give you complete control
over names, so the
issue
of name collision is easily
avoided.
There
are two reasons for
controlling access to members.
The first is to
keep
users' hands off tools
that they shouldn't touch;
tools that are
necessary
for the internal
machinations of the data
type, but not part
of
the
interface that users need to
solve their particular
problems. So making
methods
and fields private
is
a service to users because
they can easily
see
what's important to them and
what they can ignore. It
simplifies their
understanding
of the class.
The
second and most important
reason for access control is
to allow the
library
designer to change the
internal workings of the
class without
worrying
about how it will affect
the client programmer. You
might build
a
class one way at first,
and then discover that
restructuring your code
will
provide
much greater speed. If the
interface and implementation
are
clearly
separated and protected, you
can accomplish this without
forcing
the
user to rewrite their
code.
Chapter
5: Hiding the Implementation
267
Access
specifiers in Java give
valuable control to the
creator of a class.
The
users
of the class can clearly
see exactly what they
can use and what
to
ignore.
More important, though, is
the ability to ensure that
no user
becomes
dependent on any part of the
underlying implementation of a
class.
If you know this as the
creator of the class, you
can change the
underlying
implementation with the
knowledge that no
client
programmer
will be affected by the
changes because they can't
access that
part
of the class.
When
you have the ability to
change the underlying
implementation, you
can
not only improve your
design later, but you
also have the freedom
to
make
mistakes. No matter how
carefully you plan and
design you'll make
mistakes.
Knowing that it's relatively
safe to make these mistakes
means
you'll
be more experimental, you'll
learn faster, and you'll
finish your
project
sooner.
The
public interface to a class is
what the user does
see,
so that is the most
important
part of the class to get
"right" during analysis and
design. Even
that
allows you some leeway
for change. If you don't
get the interface
right
the
first time, you can
add
more
methods, as long as you
don't remove any
that
client programmers have
already used in their
code.
Exercises
Solutions
to selected exercises can be
found in the electronic
document The
Thinking in Java
Annotated
Solution Guide, available
for a small fee from
.
1.
Write
a program that creates an
ArrayList
object
without
explicitly
importing java.util.*.
2.
In
the section labeled
"package: the library unit,"
turn the code
fragments
concerning mypackage
into
a compiling and
running
set
of Java files.
3.
In
the section labeled
"Collisions," take the code
fragments and
turn
them into a program, and
verify that collisions do in
fact
occur.
4.
Generalize
the class P
defined
in this chapter by adding
all the
overloaded
versions of rint(
) and
rintln( )
necessary
to handle
all
the different basic Java
types.
268
Thinking
in Java
5.
Change
the import statement in
TestAssert.java
to
enable and
disable
the assertion
mechanism.
6.
Create
a class with public,
private,
protected,
and
"friendly"
data
members and method members.
Create an object of this
class
and
see what kind of compiler
messages you get when
you try to
access
all the class members. Be
aware that classes in the
same
directory
are part of the "default"
package.
7.
Create
a class with protected
data.
Create a second class in
the
same
file with a method that
manipulates the protected
data
in
the
first class.
8.
Change
the class Cookie
as
specified in the section
labeled
"protected:
`sort of friendly.'" Verify
that bite(
) is
not public.
9.
In
the section titled "Class
access" you'll find code
fragments
describing
mylib and
Widget.
Create this library, then
create a
Widget
in
a class that is not part of
the mylib
package.
10.
Create
a new directory and edit
your CLASSPATH to include
that
new
directory. Copy the
P.class
file
(produced by compiling
com.bruceeckel.tools.P.java)
to your new directory and
then
change
the names of the file,
the P
class
inside and the
method
names.
(You might also want to
add additional output to
watch
how
it works.) Create another
program in a different directory
that
uses
your new class.
11.
Following
the form of the example
Lunch.java,
create a class
called
ConnectionManager
that
manages a fixed array
of
Connection
objects.
The client programmer must
not be able to
explicitly
create Connection
objects,
but can only get
them via a
static
method
in ConnectionManager.
When the
ConnectionManager
runs
out of objects, it returns a
null
reference.
Test the classes in
main(
).
12.
Create
the following file in the
c05/local directory (presumably
in
your
CLASSPATH):
///:
c05:local:PackagedClass.java
package
c05.local;
Chapter
5: Hiding the Implementation
269
class
PackagedClass {
public
PackagedClass() {
System.out.println(
"Creating
a packaged class");
}
}
///:~
Then
create the following file in
a directory other than
c05:
///:
c05:foreign:Foreign.java
package
c05.foreign;
import
c05.local.*;
public
class Foreign {
public
static void main (String[] args) {
PackagedClass
pc = new PackagedClass();
}
}
///:~
Explain
why the compiler generates
an error. Would making
the
Foreign
class
part of the c05.local
package
change anything?
270
Thinking
in Java
6:
Reusing Classes
One
of the most compelling
features about Java is
code
reuse.
But to be revolutionary, you've
got to be able to do
a
lot more than copy
code and change
it.
That's
the approach used in
procedural languages like C,
and it hasn't
worked
very well. Like everything
in Java, the solution
revolves around
the
class. You reuse code by
creating new classes, but
instead of creating
them
from scratch, you use
existing classes that
someone has already
built
and
debugged.
The
trick is to use the classes
without soiling the existing
code. In this
chapter
you'll see two ways to
accomplish this. The first
is quite
straightforward:
You simply create objects of
your existing class inside
the
new
class. This is called
composition,
because
the new class is
composed
of
objects of existing classes.
You're simply reusing the
functionality of the
code,
not its form.
The
second approach is more
subtle. It creates a new
class as a type
of an
existing
class. You literally take
the form of the existing
class and add
code
to
it without modifying the
existing class. This magical
act is called
inheritance,
and the compiler does
most of the work.
Inheritance is one of
the
cornerstones of object-oriented
programming, and has
additional
implications
that will be explored in
Chapter 7.
It
turns out that much of
the syntax and behavior
are similar for
both
composition
and inheritance (which makes
sense because they are
both
ways
of making new types from
existing types). In this
chapter, you'll
learn
about these code reuse
mechanisms.
Composition
syntax
Until
now, composition has been
used quite frequently. You
simply place
object
references inside new
classes. For example,
suppose you'd like an
object
that holds several String
objects,
a couple of primitives, and
an
271
object
of another class. For the
nonprimitive objects, you
put references
inside
your new class, but
you define the primitives
directly:
//:
c06:SprinklerSystem.java
//
Composition for code reuse.
class
WaterSource {
private
String s;
WaterSource()
{
System.out.println("WaterSource()");
s
= new String("Constructed");
}
public
String toString() { return s; }
}
public
class SprinklerSystem {
private
String valve1, valve2, valve3, valve4;
WaterSource
source;
int
i;
float
f;
void
print() {
System.out.println("valve1
= " + valve1);
System.out.println("valve2
= " + valve2);
System.out.println("valve3
= " + valve3);
System.out.println("valve4
= " + valve4);
System.out.println("i
= " + i);
System.out.println("f
= " + f);
System.out.println("source
= " + source);
}
public
static void main(String[] args) {
SprinklerSystem
x = new SprinklerSystem();
x.print();
}
}
///:~
One
of the methods defined in
WaterSource
is
special: toString(
).
You
will learn later that
every nonprimitive object
has a toString(
)
method,
and it's called in special
situations when the compiler
wants a
String
but
it's got one of these
objects. So in the
expression:
System.out.println("source
= " + source);
272
Thinking
in Java
the
compiler sees you trying to
add a String
object
("source
= ")
to a
WaterSource.
This doesn't make sense to
it, because you can
only "add"
a
String
to
another String,
so it says "I'll turn
source
into
a String
by
calling
toString(
)!"
After doing this it can
combine the two Strings
and
pass
the resulting String
to
System.out.println(
).
Any time you
want
to
allow this behavior with a
class you create you
need only write a
toString(
) method.
At
first glance, you might
assume--Java being as safe
and careful as it
is--
that
the compiler would
automatically construct objects
for each of the
references
in the above code; for
example, calling the default
constructor
for
WaterSource
to
initialize source.
The output of the print
statement
is
in fact:
valve1
=
null
valve2
=
null
valve3
=
null
valve4
=
null
i=0
f
= 0.0
source
=
null
Primitives
that are fields in a class
are automatically initialized to
zero, as
noted
in Chapter 2. But the object
references are initialized to
null,
and if
you
try to call methods for
any of them you'll get an
exception. It's
actually
pretty
good (and useful) that
you can still print
them out without
throwing
an exception.
It
makes sense that the
compiler doesn't just create
a default object for
every
reference because that would
incur unnecessary overhead in
many
cases.
If you want the references
initialized, you can do
it:
1.
At
the point the objects
are defined. This means
that they'll always
be
initialized before the
constructor is called.
2.
In
the constructor for that
class.
3.
Right
before you actually need to
use the object. This is
often called
lazy
initialization. It can
reduce overhead in situations
where the
object
doesn't need to be created
every time.
Chapter
6: Reusing Classes
273
All
three approaches are shown
here:
//:
c06:Bath.java
//
Constructor initialization with
composition.
class
Soap {
private
String s;
Soap()
{
System.out.println("Soap()");
s
= new String("Constructed");
}
public
String toString() { return s; }
}
public
class Bath {
private
String
//
Initializing at point of
definition:
s1
= new String("Happy"),
s2
= "Happy",
s3,
s4;
Soap
castille;
int
i;
float
toy;
Bath()
{
System.out.println("Inside
Bath()");
s3
= new String("Joy");
i
= 47;
toy
= 3.14f;
castille
= new Soap();
}
void
print() {
//
Delayed initialization:
if(s4
== null)
s4
= new String("Joy");
System.out.println("s1
= " + s1);
System.out.println("s2
= " + s2);
System.out.println("s3
= " + s3);
System.out.println("s4
= " + s4);
System.out.println("i
= " + i);
System.out.println("toy
= " + toy);
System.out.println("castille
= " + castille);
274
Thinking
in Java
}
public
static void main(String[] args) {
Bath
b = new Bath();
b.print();
}
}
///:~
Note
that in the Bath
constructor
a statement is executed before
any of
the
initializations take place.
When you don't initialize at
the point of
definition,
there's still no guarantee
that you'll perform any
initialization
before
you send a message to an
object reference--except for
the
inevitable
run-time exception.
Here's
the output for the
program:
Inside
Bath()
Soap()
s1
= Happy
s2
= Happy
s3
= Joy
s4
= Joy
i
= 47
toy
= 3.14
castille
= Constructed
When
print( )
is
called it fills in s4
so
that all the fields
are properly
initialized
by the time they are
used.
Inheritance
syntax
Inheritance
is an integral part of Java
(and OOP languages in
general). It
turns
out that you're always
doing inheritance when you
create a class,
because
unless you explicitly
inherit from some other
class, you implicitly
inherit
from Java's standard root
class Object.
The
syntax for composition is
obvious, but to perform
inheritance there's
a
distinctly different form.
When you inherit, you
say "This new class
is
like
that old class." You
state this in code by giving
the name of the
class
as
usual, but before the
opening brace of the class
body, put the
keyword
extends
followed
by the name of the base
class. When
you do this, you
Chapter
6: Reusing Classes
275
automatically
get all the data
members and methods in the
base class.
Here's
an example:
//:
c06:Detergent.java
//
Inheritance syntax &
properties.
class
Cleanser {
private
String s = new String("Cleanser");
public
void append(String a) { s += a; }
public
void dilute() { append(" dilute()"); }
public
void apply() { append(" apply()"); }
public
void scrub() { append(" scrub()"); }
public
void print() { System.out.println(s); }
public
static void main(String[] args) {
Cleanser
x = new Cleanser();
x.dilute();
x.apply(); x.scrub();
x.print();
}
}
public
class Detergent extends Cleanser {
//
Change a method:
public
void scrub() {
append("
Detergent.scrub()");
super.scrub();
// Call base-class version
}
//
Add methods to the interface:
public
void foam() { append(" foam()"); }
//
Test the new class:
public
static void main(String[] args) {
Detergent
x = new Detergent();
x.dilute();
x.apply();
x.scrub();
x.foam();
x.print();
System.out.println("Testing
base class:");
Cleanser.main(args);
}
}
///:~
276
Thinking
in Java
This
demonstrates a number of features.
First, in the Cleanser
append(
) method,
Strings
are concatenated to s
using
the +=
operator,
which
is one of the operators
(along with `+')
that the Java
designers
"overloaded"
to work with Strings.
Second,
both Cleanser
and
Detergent
contain
a main(
) method.
You
can
create a main(
) for
each one of your classes,
and it's often
recommended
to code this way so that
your test code is wrapped in
with
the
class. Even if you have a
lot of classes in a program,
only the main(
)
for
the class invoked on the
command line will be called.
(As long as
main(
) is
public,
it doesn't matter whether
the class that it's
part of is
public.)
So in this case, when you
say java
Detergent,
Detergent.main(
) will
be called. But you can
also say java
Cleanser
to
invoke Cleanser.main(
),
even though Cleanser
is
not a public
class.
This technique of putting a
main( )
in
each class allows easy
unit
testing
for each class. And
you don't need to remove
the main(
) when
you're
finished testing; you can
leave it in for later
testing.
Here,
you can see that
Detergent.main(
) calls
Cleanser.main(
)
explicitly,
passing it the same
arguments from the command
line
(however,
you could pass it any
String
array).
It's
important that all of the
methods in Cleanser
are
public.
Remember
that
if you leave off any
access specifier the member
defaults to "friendly,"
which
allows access only to
package members. Thus,
within
this package,
anyone
could use those methods if
there were no access
specifier.
Detergent
would
have no trouble, for
example. However, if a class
from
some
other package were to
inherit from Cleanser
it
could access only
public
members.
So to plan for inheritance, as a
general rule make
all
fields
private
and
all methods public.
(protected
members
also allow
access
by derived classes; you'll
learn about this later.) Of
course, in
particular
cases you must make
adjustments, but this is a
useful guideline.
Note
that Cleanser
has
a set of methods in its
interface: append(
),
dilute(
),
apply(
),
scrub(
),
and print(
).
Because Detergent
is
derived
from Cleanser
(via
the extends
keyword)
it automatically gets
all
these methods in its
interface, even though you
don't see them
all
explicitly
defined in Detergent.
You can think of
inheritance, then, as
Chapter
6: Reusing Classes
277
reusing
the interface. (The
implementation also comes
with it, but
that
part
isn't the primary
point.)
As
seen in scrub(
),
it's possible to take a
method that's been defined
in
the
base class and modify
it. In this case, you
might want to call
the
method
from the base class
inside the new version.
But inside scrub(
)
you
cannot simply call scrub(
),
since that would produce a
recursive
call,
which isn't what you
want. To solve this problem
Java has the
keyword
super
that
refers to the "superclass"
that the current class
has
been
inherited from. Thus the
expression super.scrub(
) calls
the base-
class
version of the method
scrub(
).
When
inheriting you're not
restricted to using the
methods of the base
class.
You can also add
new methods to the derived
class exactly the
way
you
put any method in a class:
just define it. The
method foam(
) is
an
example
of this.
In
Detergent.main(
) you
can see that for a
Detergent
object
you can
call
all the methods that
are available in Cleanser
as
well as in
Detergent
(i.e.,
foam(
)).
Initializing
the base class
Since
there are now two
classes involved--the base
class and the
derived
class--instead
of just one, it can be a bit
confusing to try to imagine
the
resulting
object produced by a derived
class. From the outside, it
looks
like
the new class has
the same interface as the
base class and
maybe
some
additional methods and
fields. But inheritance
doesn't just copy
the
interface
of the base class. When
you create an object of the
derived class,
it
contains within it a subobject
of
the base class. This
subobject is the
same
as if you had created an
object of the base class by
itself. It's just
that,
from the outside, the
subobject of the base class
is wrapped within
the
derived-class object.
Of
course, it's essential that
the base-class subobject be
initialized
correctly
and there's only one
way to guarantee that:
perform the
initialization
in the constructor, by calling
the base-class
constructor,
which
has all the appropriate
knowledge and privileges to
perform the
base-class
initialization. Java automatically
inserts calls to the
base-class
278
Thinking
in Java
constructor
in the derived-class constructor.
The following example
shows
this
working with three levels of
inheritance:
//:
c06:Cartoon.java
//
Constructor calls during
inheritance.
class
Art {
Art()
{
System.out.println("Art
constructor");
}
}
class
Drawing extends Art {
Drawing()
{
System.out.println("Drawing
constructor");
}
}
public
class Cartoon extends Drawing {
Cartoon()
{
System.out.println("Cartoon
constructor");
}
public
static void main(String[] args) {
Cartoon
x = new Cartoon();
}
}
///:~
The
output for this program
shows the automatic
calls:
Art
constructor
Drawing
constructor
Cartoon
constructor
You
can see that the
construction happens from
the base "outward,"
so
the
base class is initialized
before the derived-class
constructors can
access
it.
Even
if you don't create a
constructor for Cartoon(
),
the compiler will
synthesize
a default constructor for
you that calls the
base class
constructor.
Chapter
6: Reusing Classes
279
Constructors
with arguments
The
above example has default
constructors; that is, they
don't have any
arguments.
It's easy for the
compiler to call these
because there's no
question
about what arguments to
pass. If your class doesn't
have default
arguments,
or if you want to call a
base-class constructor that
has an
argument,
you must explicitly write
the calls to the base-class
constructor
using
the super
keyword
and the appropriate argument
list:
//:
c06:Chess.java
//
Inheritance, constructors and
arguments.
class
Game {
Game(int
i) {
System.out.println("Game
constructor");
}
}
class
BoardGame extends Game {
BoardGame(int
i) {
super(i);
System.out.println("BoardGame
constructor");
}
}
public
class Chess extends BoardGame {
Chess()
{
super(11);
System.out.println("Chess
constructor");
}
public
static void main(String[] args) {
Chess
x = new Chess();
}
}
///:~
If
you don't call the
base-class constructor in BoardGame(
),
the
compiler
will complain that it can't
find a constructor of the
form
Game(
).
In addition, the call to the
base-class constructor must
be
the
first
thing you do in the
derived-class constructor. (The
compiler will
remind
you if you get it
wrong.)
280
Thinking
in Java
Catching
base constructor exceptions
As
just noted, the compiler
forces you to place the
base-class constructor
call
first in the body of the
derived-class constructor. This
means nothing
else
can appear before it. As
you'll see in Chapter 10,
this also prevents a
derived-class
constructor from catching
any exceptions that come
from a
base
class. This can be
inconvenient at times.
Combining
composition
and
inheritance
It
is very common to use
composition and inheritance
together. The
following
example shows the creation
of a more complex class,
using both
inheritance
and composition, along with
the necessary
constructor
initialization:
//:
c06:PlaceSetting.java
//
Combining composition &
inheritance.
class
Plate {
Plate(int
i) {
System.out.println("Plate
constructor");
}
}
class
DinnerPlate extends Plate {
DinnerPlate(int
i) {
super(i);
System.out.println(
"DinnerPlate
constructor");
}
}
class
Utensil {
Utensil(int
i) {
System.out.println("Utensil
constructor");
}
}
Chapter
6: Reusing Classes
281
class
Spoon extends Utensil {
Spoon(int
i) {
super(i);
System.out.println("Spoon
constructor");
}
}
class
Fork extends Utensil {
Fork(int
i) {
super(i);
System.out.println("Fork
constructor");
}
}
class
Knife extends Utensil {
Knife(int
i) {
super(i);
System.out.println("Knife
constructor");
}
}
//
A cultural way of doing something:
class
Custom {
Custom(int
i) {
System.out.println("Custom
constructor");
}
}
public
class PlaceSetting extends Custom {
Spoon
sp;
Fork
frk;
Knife
kn;
DinnerPlate
pl;
PlaceSetting(int
i) {
super(i
+ 1);
sp
= new Spoon(i + 2);
frk
= new Fork(i + 3);
kn
= new Knife(i + 4);
pl
= new DinnerPlate(i + 5);
System.out.println(
"PlaceSetting
constructor");
282
Thinking
in Java
}
public
static void main(String[] args) {
PlaceSetting
x = new PlaceSetting(9);
}
}
///:~
While
the compiler forces you to
initialize the base classes,
and requires
that
you do it right at the
beginning of the constructor, it
doesn't watch
over
you to make sure that
you initialize the member
objects, so you must
remember
to pay attention to
that.
Guaranteeing
proper cleanup
Java
doesn't have the C++
concept of a destructor,
a method that is
automatically
called when an object is
destroyed. The reason is
probably
that
in Java the practice is
simply to forget about
objects rather than
to
destroy
them, allowing the garbage
collector to reclaim the
memory as
necessary.
Often
this is fine, but there
are times when your
class might perform
some
activities
during its lifetime that
require cleanup. As mentioned
in
Chapter
4, you can't know when
the garbage collector will
be called, or if it
will
be called. So if you want
something cleaned up for a
class, you must
explicitly
write a special method to do
it, and make sure
that the client
programmer
knows that they must
call this method. On top of
this--as
described
in Chapter 10 ("Error Handling
with Exceptions")--you
must
guard
against an exception by putting
such cleanup in a finally
clause.
Consider
an example of a computer-aided design
system that draws
pictures
on the screen:
//:
c06:CADSystem.java
//
Ensuring proper cleanup.
import
java.util.*;
class
Shape {
Shape(int
i) {
System.out.println("Shape
constructor");
}
void
cleanup() {
System.out.println("Shape
cleanup");
Chapter
6: Reusing Classes
283
Table of Contents:
|
|||||