A primary consideration in
object-oriented design is “separating the things that change from the
things that stay the same.”
This is particularly important for
libraries. The user (client programmer) of that library must be able to
rely on the part they use, and know that they won’t need to rewrite code
if a new version of the library comes out. On the flip side, the library creator
must have the freedom to make modifications and improvements with the certainty
that the client programmer’s code won’t be affected by those
changes.
This can be achieved through convention.
For example, the library programmer must agree to not remove existing methods
when modifying a class in the library, since that would break the client
programmer’s code. The reverse situation is thornier, however. In the case
of a data member, how can the library creator know which data members have been
accessed by client programmers? This is also true with methods that are only
part of the implementation of a class, and not meant to be used directly by the
client programmer. But what if the library creator wants to rip out an old
implementation and put in a new one? Changing any of those members might break a
client programmer’s code. Thus the library creator is in a strait jacket
and can’t change anything.
To solve this problem, Java provides
access specifiers to allow
the library creator to say what is available to the client programmer and what
is not. The levels of access control from “most
access” to “least access” are
public,
“friendly” (which has no keyword),
protected, and
private. From the previous paragraph you might think
that, as a library designer,
you’ll want to keep everything as “private” as possible, and
expose only the methods that you want the client programmer to use. This is
exactly right, even though it’s often counterintuitive for people who
program in 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.
import java.util.*;
This brings in the entire utility
library that’s part of the standard Java
distribution. Since 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 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 the book. If you’re planning to create a program
that is “Internet friendly,” however, 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). If you don’t do this, the compiler will complain. There can be
only one public
class in each compilation unit (again, 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 the jar utility in Java 1.1). The Java
interpreter is responsible for finding, loading and interpreting these
files.[32]
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, where the
package statement must appear as the first non-comment 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 packages 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();
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.
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 how Java handles
the problem of clutter.
It also 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 (by
InterNIC,[33] who
controls their assignment) 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 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 when Java, or a tool like a Java-enabled browser, is
installed on a 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 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
util, so I’ll end up with a package name:
package com.bruceeckel.util;
Now this package name can be used as an
umbrella name space for the following two files:
//: com:bruceeckel:util:Vector.java // Creating a package package com.bruceeckel.util; public class Vector { public Vector() { System.out.println( "com.bruceeckel.util.Vector"); } } ///:~
When you create your own packages,
you’ll discover that the package statement must be the first
non-comment code in the file. The second file looks much the
same:
//: com:bruceeckel:util:List.java // Creating a package package com.bruceeckel.util; 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\util
If you walk back through this, you can
see the package name com.bruceeckel.util, 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: (See page
141 if you have trouble executing this
program.):
//: c05:LibTest.java // Uses the library import com.bruceeckel.util.*; public class LibTest { 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\util, 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.
The first time you create an object of an
imported class (or you access a static member of a class), the compiler
will hunt for the .class file of the same name (so if you’re
creating an object of class X, it looks for X.class) in the
appropriate directory. If it finds only X.class, that’s what it
must use. However, if it also finds an X.java in the same directory, the
compiler will compare the date stamp on the two files, and if X.java is
more recent than X.class, it will
automatically recompile
X.java to generate an up-to-date
X.class.
If a class is not in a .java file
of the same name as that class, this behavior will not occur for that
class.
What happens if two libraries are
imported via * and they include the same
names? For example, suppose a
program does this:
import com.bruceeckel.util.*; import java.util.*;
Since java.util.* also contains a
Vector class, this causes a potential collision. However, as long as the
collision doesn’t actually occur, 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:
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.
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(Object obj) { System.out.print(obj); } public static void rint(String s) { System.out.print(s); } public static void rint(char[] s) { System.out.print(s); } public static void rint(char c) { System.out.print(c); } public static void rint(int i) { System.out.print(i); } public static void rint(long l) { System.out.print(l); } public static void rint(float f) { System.out.print(f); } public static void rint(double d) { System.out.print(d); } public static void rint(boolean b) { System.out.print(b); } public static void rintln() { System.out.println(); } public static void rintln(Object obj) { System.out.println(obj); } public static void rintln(String s) { System.out.println(s); } public static void rintln(char[] s) { System.out.println(s); } public static void rintln(char c) { System.out.println(c); } public static void rintln(int i) { System.out.println(i); } public static void rintln(long l) { System.out.println(l); } public static void rintln(float f) { System.out.println(f); } public static void rintln(double d) { System.out.println(d); } public static void rintln(boolean b) { System.out.println(b); } } ///:~
All the different data types can now be
printed out 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!"); } } ///:~
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.)
The P.java file brought up an
interesting pitfall. Especially with early implementations of Java, setting the
classpath correctly is generally quite a headache. During the development of
this book, the P.java file was introduced and seemed to work fine, but at
some point it began breaking. For a long time I was certain that this was the
fault of one implementation of Java or another, but finally I discovered that at
one point I had introduced a program (CodePackager.java, shown in Chapter
16) that used a different class P. Because it was used as a tool,
it was sometimes placed in the classpath, and other times it
wasn’t. When it was, the P in CodePackager.java was found
first by Java when executing a program in which it was looking for the class in
com.bruceeckel.tools, and the compiler would say that a particular method
couldn’t be found. This was frustrating because you can see the method in
the above class P and no further diagnostics were reported to give you a
clue that it was finding a completely different class. (That wasn’t even
public.)
At first this could seem like a compiler
bug, but if you look at the import statement it says only
“here’s where you might find P.” However, the
compiler is supposed to look anywhere in its classpath, so if it finds a
P there it will use it, and if it finds the “wrong” one
first during a search then it will stop looking. This is slightly
different from the case described on page Error! Bookmark not defined.
because there the offending classes were both in packages, and here there was a
P that was not in a package, but could still be found during a normal
classpath search.
If you’re having an experience like
this, check to make sure that there’s only one class of each name anywhere
in your
classpath.
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 for a 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; 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 9, 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.
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 { 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 out
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.
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, 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.
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 data member 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.
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 that
you own should have friendly access to other code that 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 addition, you’ll probably want to exclude classes that
shouldn’t have access to the classes being defined in the current
package.
An important question in any relationship
is “Who can access my private implementation?” The class
controls which code has access to its members. There’s no magic way to
“break in;” someone in another package can’t declare a new
class 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:
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: (See page 141 if you have trouble
executing this program.)
//: c05:dessert:Cookie.java // Creates a library package c05.dessert; public class Cookie { public Cookie() { System.out.println("Cookie constructor"); } void foo() { System.out.println("foo"); } } ///:~
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.foo(); // 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
foo( ) member is inaccessible inside Dinner.java since
foo( ) is friendly only within package
dessert.
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) { 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.
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 is often 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. 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 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 you.[34]
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 you from changing or removing the method. Making a method
private guarantees that you retain this option. (However, just because
the handle is private doesn't mean that some other object can't have a
public handle to the same object. See Chapter 12 for issues about
aliasing.)
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
the book up through the inheritance chapter. 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 on page 202, 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() { System.out.println( "ChocolateChip constructor"); } public static void main(String[] args) { ChocolateChip x = new ChocolateChip(); //! x.foo(); // Can't access foo } } ///:~
One of the interesting things about
inheritance is that if a method foo( ) exists in class
Cookie, then it also exists in any class inherited from Cookie.
But since foo( ) 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 foo() { System.out.println("foo"); } }
then foo( ) still has
“friendly” access within package dessert, but it is also
accessible to anyone inheriting from Cookie. However, it is not
public.
Access control is often referred to as
implementation hiding.
Wrapping data and methods within classes (combined with implementation hiding
this is often called encapsulation) produces a
data type with characteristics and behaviors, but access control puts boundaries
within that 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
think it’s 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 users 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 their
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. However, with the comment
documentation supported by javadoc (described in Chapter 2) the issue of code
readability by the client programmer becomes less important.
public class X { public void pub1( ) { /* . . . */ } public void pub2( ) { /* . . . */ } 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. 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, good browsers should be an expected part of any good Java development
tool.
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 {
That is, if the name of your library is
mylib any client programmer can access Widget by
saying
import mylib.Widget;
or
import mylib.*;
However, there’s an extra pair of
constraints:
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
protected.[35]
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 preventing anyone but
you, inside a static member of the class, from creating an object of that
class.[36]
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(); } } ///:~
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 handle to an object, which is what happens here. This
method returns a handle 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 handle 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 will be discussed later in this book. 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.
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.
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.
///: c05:PackagedClass.java package c05; 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.*; 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 package
change anything?
[32]
There’s nothing in Java that forces the use of an interpreter. There exist
native-code Java compilers that generate a single executable
file.
[33]
ftp://ftp.internic.net
[34]
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.)
[35]
Actually, a Java 1.1 inner class can be private or protected, but
that’s a special case. These will be introduced in Chapter
7.
[36]
You can also do it by inheriting (Chapter 6) from that class.