Sponsored Link •
|
Advertisement
|
The process of software design is largely a process of organizing. The previous three chapters explored the object-oriented ways you can organize a Java program. This chapter discusses an additional way to organize Java programs that has nothing to do with object-orientation: packages. In Java, a package is a library of types (classes and interfaces). This chapter describes four ways to think about packages and shows how to make use of packages in your Java programs.
Once you know about packages, you can understand all the access levels (such as
private
, protected
, etc.) available to types and their fields
and methods. This chapter compares all the access levels and gives advice on how to use them.
The first way to think about packages is as a tool to help you reduce the likelyhood of name conflicts in your programs. When you design a Java program, you model the problem domain by identifying and defining types and assigning each a name. Types refer to each other by name, so each type name you assign must be unique. If you design a large program, or incorporate types named and defined by others, you may encounter name conflicts. To address the problem of name conflicts, you use packages.
Packages effectively lengthen type names, making the names more distinctive. In a Java program,
every type belongs to some package. A package is a set of types grouped together under a common
package name. Each type has a simple name, and each package has a package
name. The name of the package containing a type, plus a dot, plus the type's simple name is the
type's
fully qualified name.
For example, if you have a class named
CoffeeCup
in a package named dishes
,
"dishes.CoffeeCup
" is its fully qualified name.
("dishes
" is its package name; "CoffeeCup
"
its simple name.)
The fully qualified name of a
type, which is longer and more distinctive than its simple name, enables like-named types from different
packages to be used in the same program.
If you discard the package name from a type's fully qualified name, you get the type's simple name.
Therefore, the simple name of dishes.CoffeeCup
is, simply,
CoffeeCup
. To use CoffeeCup
, types in the same package can
just use its simple name. Types in other packages, however, must also identify
dishes
, the package containing CoffeeCup
, as well as its
simple name. This ensures that a different CoffeeCup
class defined in a different
package will not conflict with dishes.CoffeeCup
.
To help make type names even more distinctive, you can organize your packages hierarchically. Packages can contain not only types, but other packages as well. The entities contained in a package--its classes, interfaces, and sub-packages--are called the package's members.
The fully qualified name of a class nestled deep down inside several packages is the name of each
package and the class's simple name, all separated by dots. For instance, if you placed
CoffeeCup
inside package dishes
and placed
dishes
inside package vcafe
(for virtual cafe), the fully
qualified name of CoffeeCup
would be
"vcafe.dishes.CoffeeCup
." The greater the number of nested packages in
which you place a class, the more dot-separated names the class will have in its fully qualified name, and
the more distinctive that fully qualified name will be.
Packages help you guard against the potential of name conflicts in your Java programs. Instead of worrying that the simple name of every type you need to use in a program is unique, you need only worry that every fully qualified name is unique.
One other way to deal with name conflicts involves class loaders and the multiple name spaces offered by the JVM. This will be discussed in Chapter 20.
A second way to think about packages is as a tool to help you organize the types you create for your program. With packages, you can organize a program into logically related groups of types, and organize the groups hierarchically.
The package is an organizational tool independent of any object-oriented organization of a program. For example, all the types in a particular family of types could belong to the same package, or be spread out across several packages. A class in one package can subclass a class in another package. The only requirement is that the subclass must specify the name of the package containing its superclass as well as the superclass's simple name. When you organize your types into packages, what you are actually organizing is type names.
Although you can grant special privileges between types that belong to the same package, a topic that will be discussed later in this chapter, you can't grant special privileges between a types in a package and types in a sub-package. To the types defined in a parent package, a sub-package is just like any other package. From the perspective of a Java compiler or the Java Virtual Machine, nested packages are not really seen as a hierarchy. They are just seen as a set of independent packages, each with a unique name. Packages are seen as a conceptual hierarchy only from the perspective of developers, who can use the hierarchy to express conceptual relationships between different groups of types.
Often, Java compilers and Java Virtual Machines expect the source files or class files contained in a hierarchy of packages to be located in a corresponding directory hierarchy, in which each directory takes the name of a package. Here, the compiler or Java Virtual Machine is using the package hierarchy as a way to locate files on a disk. The actual manner in which a particular compiler or Java Virtual Machine finds class files is a detail specific to each individual development environment or Java Platform implementation. The process of using directory hierarchies that map to package hierarchies to locate class files will be discussed further later in this chapter.
A third way to think about packages is simply as libraries. Any Java program you write will make use
of libraries developed by others and made available to your program as packages. Any program will at
least use the run-time libraries of the Java API, some of which are java.lang
,
java.io
, java.util
, java.net
,
java.awt
, and java.applet
. If, rather than developing a
complete program, you wish to develop class library that other developers can use in their programs, your
end-product will be a package.
The fourth way to think about packages is as a tool that can help you separate interface from implementation. You can grant special access privileges between types within the same package, and you can declare entire types to be accessible only to other types within the same package. The full details of how to do this will be given later in this chapter as part of a discussion of Java's access levels.
Because the packages used by a program can come from many sources, it is important that you name
your packages in a way that won't conflict with the names of packages developed by others. Of course, you
don't know what packages might be developed by other programmers, nor how they will name those
packages. This points out that the mechanism of packages doesn't actually solve the name conflict
problem, it only reduces the likelihood of an actual conflict. Just because you go to the trouble of enclosing
your CoffeeCup
class in two nested packages--vcafe
and
dishes
--doesn't mean someone else won't inadvertently do the same.
To combat the potential of name conflicts between types developed by different software vendors, Java comes with a recommended naming convention for packages. If everyone would follow the recommended convention when naming their packages, harmony would cover the Earth. Java does not, however, enforce any naming convention, so name conflicts are still possible. It is up to you to do your part in preventing naming conflicts within Java programs.
The official recommendation on package naming is to use the reversed internet domain name of your
company or organization as the first part of your package names. Because internet domain names are
globally unique, this improves the chances your package names will be globally unique. If your company's
domain name were artima.com
, for instance, you would start any package name with
"com.artima
." The fully qualified name of CoffeeCup
would
become com.artima.vcafe.dishes.CoffeeCup
.
All the packages you create must be given a name that will be unique across the scope in which they will be visible. If they will be visible only locally, you needn't use the recommended naming convention. If you are certain your package names are not going to be visible on a global scale, but will remain inside, say, your division, you can devise and follow a division-wide package naming scheme. For any other package, however, following the recommended naming scheme makes you a good Java citizen.
As you write a Java program, you must place every class you define into a
package, and give each package a unique name. You place a class into a
package by including a package declaration at the top of the source file. A
package declaration
is just the keyword package
followed by the package name and a semicolon. The package declaration must appear in the source file
before any class or interface declaration, and each source file can contain only one package declaration.
For example, you would place CoffeeCup
into the package
com.artima.vcafe.dishes
as follows:
// In Source Packet in file // packages/ex1/com/artima/vcafe/dishes/CoffeeCup.java package com.artima.vcafe.dishes; public class CoffeeCup { public static final int MAX_SHORT_ML = 237; public static final int MAX_TALL_ML = 355; public static final int MAX_GRANDE_ML = 473; public void add(int amountOfCoffee) { System.out.println("Adding " + amountOfCoffee + " ml of coffee."); } //... }
The package name in the example above, com.artima.vcafe.dishes
,
indicates that dishes
is a sub-package of vcafe
, which is a sub-
package of artima
, which is a sub-package of com
. You needn't
have any source file in your program that declares the com
package, the
com.artima
package, or the com.artima.vcafe
package.
The package statement in the example above is enough to establish the existence of all four packages:
com
, artima
, vcafe
, and
dishes
.
On the other hand, if you do have source files that declare classes as members of, say, the
com.artima.vcafe
package, those classes have no special relation to the classes of
com.artima.vcafe.dishes
, as far as the Java language is concerned. To the
Java language, com.artima.vcafe
and
com.artima.vcafe.dishes
are just two different packages with two different
names. To you, the programmer, however, the hierarchical relationship between the two packages would
have meaning: it would express the conceptual relationship between two
different groups of types.
[bv: is this redundant with something that came before?]
Although the location of source and class files for package members at both compile-time and run-
time depends on your particular development and runtime environments,
many environments require that you create a hierarchy of
directories that correspond to the hierarchy of packages. If you were to work on such a system, you
would likely have to put the source and class file for the CoffeeCup
class defined
above in a directory named ".../com/artima/vcafe/dishes
" or
"...\com\artima\vcafe\dishes
", depending on your preferred direction of
slash.
To give one concrete example, imagine you are using Sun's JDK 1.1.1 to run a Java program on
Microsoft Windows95. You would set an environment variable, CLASSPATH
, to
indicate to the Java Virtual Machine where it should look for class files. If your
CLASSPATH
is set to
".;C:\MYLIB;C:\JDK1.1.1\LIB\CLASSES.ZIP
", then the compiler and the
Java Virtual Machine would look in three places for the classes needed by your program:
.
"
C:\MYLIB
"
C:\JDK1.1.1\LIB\CLASSES.ZIP
"
com.artima.vcafe.dishes.CoffeeCup
in the program, the Java
Virtual Machine would first look for a directory, relative to the current directory, named
.\com\artima\vcafe\dishes
. (It would look here first because
".
" is the first directory in the CLASSPATH
.) If it finds a
CoffeeCup.class
in that directory, it would load it. If this directory didn't exist,
or there was no CoffeeCup.class
in that directory, the Java Virtual Machine
would look for a directory named C:\MYLIB\com\artima\vcafe\dishes
. If it
finds a CoffeeCup.class
here, it would load it. Otherwise it would look inside the
zip file for a com\artima\vcafe\dishes\CoffeeCup.class
. It is unlikely
that CoffeeCup.class
it is in the zip file, because this is where all the runtime
libraries of the Java Platform are kept in JDK 1.1.1.
As it searches through the directories and zip files listed in the class path, the Java Virtual Machine
loads the first class file that it encounters with a name that matches the class name,
CoffeeCup.class
, and a relative directory that matches the package name,
com\artima\vcafe\dishes
. Once it has loaded the class file, the virtual
machine checks the binary data to verify that the class is indeed
com.artima.vcafe.dishes.CoffeeCup
.
This Windows95 and JDK 1.1.1 example was just one possible way a Java Platform implementation could locate class files. To find out how your particular Java Platform or Java development environment locates class files, you must consult its documentation.
In every Java program there can be one unnamed package, which is simply a package with no name. In a sense, the unnamed package really does have a name, just a very short one, which distinguishes it from the other packages in your program. To place a class into the unnamed package, just define the class in a source file with no package statement. All types declared in this book prior to this chapter were in the unnamed package.
You should not use the unnamed package for a general-purpose library, because it is probably the most common package name used by Java programmers. (In addition, types declared in the unnamed package are accessible only to each other. In other words, a type in a named package can't access a type in the unnamed package.) In general, you will want to partition large Java programs into named packages to better organize your program and to take advantage of the implementation-hiding capabilities of packages. The unnamed package is convenient and appropriate for the core types that make up an applet or application.
In a Java source file, you have two ways to refer to a class or interface defined in another package. You can either use the fully qualified name of the class everywhere you refer to it, or you can import that class's fully qualified name into your source file and then just use the simple name everywhere. Importing a type into a source file means making the compiler recognize the type in that source file by its simple name.
You can't import packages, just types. Import doesn't include any code,
like #include
of C or C++. It only means that you can use the
simple name of a type instead of the fully qualified names.
As an example, imagine you are writing code in the unnamed package that takes advantage of the
CoffeeCup
class defined in package
com.artima.vcafe.dishes
. One approach is to just use the fully qualified name
of CoffeeCup
everywhere, as in:
// In Source Packet in file packages/ex1/Example1a.java // Deep in the heart of the unnamed package... class Example1a { public static void main(String[] args) { com.artima.vcafe.dishes.CoffeeCup cup = new com.artima.vcafe.dishes.CoffeeCup(); cup.add(com.artima.vcafe.dishes.CoffeeCup.MAX_SHORT_ML); } }This approach is reasonable if the source file has only a few references to a class, but otherwise can make your code tiresome for you to type and others to read. The alternative is to import the class into the source file and then refer to the class by its simple name. Here's an example:
// In Source Packet in file packages/ex1/Example1b.java // At the top of a file in the unnamed package, import the class. import com.artima.vcafe.dishes.CoffeeCup; // Everywhere else in the file, just use the simple name. class Example1b { public static void main(String[] args) { CoffeeCup cup = new CoffeeCup(); cup.add(CoffeeCup.MAX_TALL_ML); } }
If you find yourself using several types from a single package, you can import all their names from a package into your source file with one import statement by using an asterisk in place of the class or interface name. (Actually, the asterisk only imports classes and interfaces declared as public, a feature that will be described in detail later in this chapter.):
// In Source Packet in file packages/ex1/Example1c.java // Import all public types from the com.artima.vcafe.dishes package. import com.artima.vcafe.dishes.*; // Everywhere else in the file, just use the simple names. class Example1c { public static void main(String[] args) { CoffeeCup cup = new CoffeeCup(); cup.add(CoffeeCup.MAX_GRANDE_ML); } }
Import statements such as the ones shown in the examples above reduce the amount of typing required
to use types from other packages, but they also make it possible for names to conflict again. For instance,
if you imported two different CoffeeCup
classes from two different packages, just
referring to "CoffeeCup
" would be ambiguous. The compiler wouldn't know which
CoffeeCup
you were talking about. In this case you would need to explicitly indicate
which CoffeeCup
you meant by prefacing the simple name with the package name.
In other words, even though you imported both CoffeeCup
classes, you'll still have
to use the fully qualified names to resolve the ambiguity.
As an example, imagine you imported all the public types from two packages,
com.artima.vcafe.dishes
and
com.artima.pencilholders
, both of which contained a
CoffeeCup
class. To use either version of CoffeeCup
you
would have to use its fully qualified name, as shown below:
// In Source Packet in file // packages/ex1/com/artima/pencilholders/CoffeeCup.java package com.artima.pencilholders; public class CoffeeCup { public void add(int amountOfPencils) { System.out.println("Adding " + amountOfPencils + " pencils."); } //... } // In Source Packet in file packages/ex1/Example1d.java // All types defined in both packages are // imported, yielding two different classes named "CoffeeCup." import com.artima.pencilholders.*; import com.artima.vcafe.dishes.*; class Example1d { public static void main(String[] args) { // Somewhere later in the code, you wish to instantiate a // new CoffeeCup from the com.artima.vcafe.dishes package: com.artima.vcafe.dishes.CoffeeCup myCoffee = new com.artima.vcafe.dishes.CoffeeCup(); // While you sip your coffee with the cup from the virtual // cafe, you also want a place to store your spare pencils. // So, you create a new CoffeeCup from the // com.artima.pencilholders package. This is a different // class, but one that shares the same simple name as the // previous "CoffeeCup." com.artima.pencilholders.CoffeeCup myPencilHolder = new com.artima.pencilholders.CoffeeCup(); myCoffee.add(com.artima.vcafe.dishes.CoffeeCup.MAX_SHORT_ML); myPencilHolder.add(10); } }
The code as shown above compiles fine, because each time you use a CoffeeCup
you clearly indicate which CoffeeCup
you want. You have indeed accomplished
your goal of using two different CoffeeCup
classes in the same source file, yet you
have once again cluttered the code with long package names.
Fortunately, one other approach exists that
may help you reduce some of the clutter. If you only import one of the packages containing a
CoffeeCup
class, you could use the simple name when referring to that
CoffeeCup
. As before, you'd have to use the fully qualified name when referring to
the other CoffeeCup
. Rewriting the previous example using this approach, yields the
following code:
// In Source Packet in file packages/ex1/Example1e.java // Import all types defined in com.artima.vcafe.dishes, but // don't import anything from com.artima.pencilholders. import com.artima.vcafe.dishes.*; class Example1e { public static void main(String[] args) { // Somewhere later in the code, you wish to instantiate a // new CoffeeCup from the com.artima.vcafe.dishes package. // Here you can just use the simple name: CoffeeCup myCoffee = new CoffeeCup(); // To create a new CoffeeCup from the // com.artima.pencilholders package, you must once again // use the fully qualified name: com.artima.pencilholders.CoffeeCup myPencilHolder = new com.artima.pencilholders.CoffeeCup(); myCoffee.add(CoffeeCup.MAX_TALL_ML); myPencilHolder.add(15); } }
You might be wondering if you can just import all the members of the
com.artima
package and just use
vcafe.dishes.CoffeeCup
and
pencilholders.CoffeeCup
to distinguish between the two classes of coffee cup.
Well, you can't. The import statement only imports types, not packages. The statement "import
com.artima.*;
" imports all the types defined in that package, but doesn't import any sub-
packages defined in that package. The statement "import com.artima;
" doesn't
compile, because you are trying to import a package and not a class or interface. Another statement that
doesn't compile is "import com.artima.*.dishes;
". The
*
must always go at the end, as it only matches type names, not package names.
There is one exception to the rule that you must import types from other packages before you can use
their simple names: java.lang.*
. The public types defined in the standard run-
time library java.lang
are automatically imported into every Java source file. This
package contains classes, such as String
, Thread
, and
Object
, that are essential to the inner workings of Java programs. To make use of the
types contained in the packages from Java's standard run-time library other than
java.lang
, you must either import the packages or use fully qualified names, just
like any other package.
Import statements are provided as a convenience for the programmer. Because of import statements, you don't have to always type long and tedious fully qualified names. The Java compiler can work out the fully qualified names of types given the import statements and the simple names in a source file. When the compiler generates class files, it discards any import statements in the source file. In class files, all types are identified by their fully qualified names. In your programs, you can choose to use import statements or fully qualified names, whatever you think will maximize the readability of your code.
As mentioned earlier in this chapter, an import statement does not
dynamically include code from a different file, as #include
does for C and C++ programs. Import is just about names.
One of the most useful features of Java packages is the ability to grant access to classes, interfaces, methods, or fields exclusively to other members of the same package. This feature gives the package an internal implementation and an external interface. It provides the usual advantages of a hidden internal implementation: robustness and ease of modification. The robustness arises from the inability of types declared in other packages to incorrectly manipulate the internal implementation of the package. Types in other packages must go through the external interface of the package, and the package maintains control of its internal implementation. Ease of modification comes from the ability to change the internal implementation of the package without affecting the code of other packages, which is tied only to the external interface.
The first step you can take to hide the internal implementation of a package is to declare as public only those types that are needed by other packages. When you declare a class or interface, it is by definition contained in a package. If you want a class or interface to be accessible to types declared in other packages, you must declare it public. If you do not declare it public, it will only be accessible to types in the same package. Therefore, you can denote some types (the public ones) as part of the external interface of the package. The other types (the ones that aren't public) are part of the internal implementation of the package. An example of both kinds of class declarations is shown below:
// In Source Packet in file // packages/ex2/com/artima/vcafe/dishes/Cup.java package com.artima.vcafe.dishes; // Class Cup is part of the internal implementation of // package com.artima.vcafe.dishes. class Cup { public void add(int amountOfCoffee) { System.out.println("Adding to a Cup."); } //... } // In Source Packet in file // packages/ex2/com/artima/vcafe/dishes/CoffeeCup.java package com.artima.vcafe.dishes; // Class CoffeeCup is part of the external interface of // package com.artima.vcafe.dishes. public class CoffeeCup extends Cup { public static final int MAX_SHORT_ML = 237; public static final int MAX_TALL_ML = 355; public static final int MAX_GRANDE_ML = 473; //... }
In the code shown above, class CoffeeCup
is declared public, but class
Cup
is not. Consequently, CoffeeCup
is accessible everywhere,
but Cup
is accessible only in the com.artima.vcafe.dishes
package.
Package access is the default for types. Unless you explicitly modify your class declaration with the
keyword public
, you'll get "package access," as this default level of access is called.
You cannot declare a class with the access specifiers protected
or
private
. It must either be declared with the keyword public
or
have no access specifier.
As the example above demonstrates, you can declare a superclass with package access and still give its
subclass public access. Given the code above, a type in another package could not subclass
Cup
, but could subclass CoffeeCup
. If you want types in other
packages to be able to use CoffeeCup
but not subclass it, you must also declare it
final, as shown below:
// In Source Packet in file // packages/ex3/com/artima/vcafe/dishes/Cup.java package com.artima.vcafe.dishes; // Class Cup is part of the internal implementation of // package com.artima.vcafe.dishes. class Cup { public void add(int amountOfCoffee) { System.out.println("Adding to a Cup."); } //... } // In Source Packet in file // packages/ex3/com/artima/vcafe/dishes/CoffeeCup.java package com.artima.vcafe.dishes; // Class CoffeeCup is part of the external interface of // package com.artima.vcafe.dishes. It can be used, but // not subclassed, by classes in other packages. public final class CoffeeCup extends Cup { public static final int MAX_SHORT_ML = 237; public static final int MAX_TALL_ML = 355; public static final int MAX_GRANDE_ML = 473; //... }
When you fill a package with types, you should separate the types that represent the implementation of the package from those that represent the interface. Only those types that are needed by other packages should be declared public. A good rule of thumb is to leave any class or interface with its default package access, unless you're sure it should be public.
Declaring a public class as final will prevent classes in other packages from declaring a subclass, but it will also restrict any other class in its own package from declaring a subclass. This is a severe restriction on the use of a class. Often you will want clients of your package to be able to subclass its public classes. That is one of the fundamental ways to reuse code in object-oriented programming. The rule of thumb here is to make classes final only when you have a good reason.
One possible reason to make a class final is to ensure your package will always behave as expected.
For example, imagine you write a package that depends for correctness on the proper behavior of a certain
class of objects, say the CoffeeCup
class, defined in your package. You make class
CoffeeCup
public so that clients can create instances of it to pass to the methods of
other classes defined in your package. If your package requires that the CoffeeCup
objects passed to it behave in a certain way, your package might break if a client declares their own
subclass of class CoffeeCup
, say LeakyCup
, and overrides the
methods that your package depends upon for correctness. You can avoid this by declaring every method in
CoffeeCup
as final, or by declaring the entire CoffeeCup
class
final.
In the examples in this book, each type is declared in its own source file. The name of the source file
is the name of the type plus the extension .java
. For example, class
CoffeeCup
is declared in file CoffeeCup.java
, and interface
Washable
is declared in file Washable.java
. Although
placing each type in a separate file named after the type is in general a good practice, because it makes the
type's source easier for you and other developers to locate, it is not always required. Java compilers do
require that public types be declared in a file that bears the name of the public types. They do not,
however, require this of non-public types. You can place as many non-public types in the same file as you
wish, and the file can have whatever name you wish. If a file does contain a public type, however, the file
must be given the name of the public type. Because you can only have one package statement in each
source file, all types declared in the same source file are members of the same package.
In general, within any class you design you will want to hide the implementation. Given that packages can (and should) be used to group related types, however, you may want to expose some fields and methods to other classes in the same package while keeping them hidden from classes outside the package. Java provides access control modifiers to support this intermediate level of implementation hiding. By applying proper modifiers on a class's fields and methods, you can hide the class's implementation from classes outside the package while exposing the implementation to classes inside the package.
Java gives you three access control modifiers--private
,
protected
, and public
--to apply to the fields and methods of
public classes, but you can obtain four distinct levels of access from their use. Three of the levels (private,
protected, and public access) are denoted by the use of one of the three access control modifiers. The
fourth level (package access) is the default and is indicated by the lack of any access control modifier.
Here is a description of each of the four access levels available to members of public classes, in order from
least to most accessible:
private
) - a field or method accessible
only to the class which defines it.
protected
) - a field or method
accessible to any type in the same package, and to subclasses in any package.
public
) - a field or method accessible to
any type in any package.
There is no way to grant special access to types in sub-packages. This is why the Java compiler and
the Java Virtual Machine view a package and its sub-packages as independent packages with no special
privileges between them. Thus, the relationship between types in hierarchically related packages, such as
com.artima.vcafe
and com.artima.vcafe.dishes
, is
only conceptual. Package hierarchies help you organize your types, but don't allow any special access
privileges between the two groups of types.
A graphical depiction of the effect of each kind of access control modifier is shown in Figures 7-1
through 7-4. In these figures, the ovals represent classes, the arrows represent inheritance, the rectangles
represent packages. Each figure indicates which classes will be able to access a member of class
Cup
with one of the five access levels. Classes that can access the member in
Cup
are shown in solid gray; classes that can't are shown with a checkerboard pattern.
Figure 5-1. Private access to a member of Cup
.
Figure 5-2. Package access to a member of Cup
.
Figure 5-3. Protected access to a member of Cup
.
Figure 5-4. Public access to a member of Cup
.
An example of each kind of access control modifier is shown in the following version of class
Coffee
:
// In Source Packet in file // packages/ex4/com/artima/vcafe/beverages/Coffee.java package com.artima.vcafe.beverages; public class Coffee { // PRIVATE ACCESS // Accessible to only class Coffee itself. private int temperature; // PACKAGE ACCESS // Accessible to Coffee and to the other classes and // interfaces of package com.artima.vcafe.beverages. void changeTemperature(int delta) { temperature += delta; } // PROTECTED ACCESS // Accessible to Coffee, to its subclasses (no matter what // package the subclasses are defined in), and to the other // types of package com.artima.vcafe.beverages, including // non-subclasses. protected static final int bestTemperature = 50; // PUBLIC ACCESS // Accessible to the entire universe. public void setTemperature(int temperature) { this.temperature = temperature; } public int getTemperature() { return temperature; } }
private
and protected
The private
keyword grants exclusive access not to an object, but to a class. An
object can access its private members, but so can any other object of the same class. For example, if a
CoffeeCup
object has a reference to another CoffeeCup
object,
the first CoffeeCup
can access the second CoffeeCup
's private
members through that reference. This is true of both private variables and private methods, whether they
are static or not.
Inside a package, the true meaning of the protected
keyword is quite simple.
To classes in the same package, protected access looks just like package access. Any class can access
any protected member of another class declared in the same package.
When you have subclasses in other packages, however, the true meaning of
protected
becomes more complex. Take a look at the inheritance hierarchy
shown in Figure 5-5. In this hierarchy, class Cup
, which is declared in the
com.artima.vcafe.dishes
package, declares a protected instance method
named getSize()
. This method is accessible to any subclasses declared anywhere,
including those shown declared in package com.artima.other
. Any objects whose
class descends from Cup
--instances of class CoffeeCup
,
CoffeeMug
, EspressoCup
, or TeaCup
--
can invoke getSize()
on themselves. Whether they can invoke
getSize()
on a reference to another object, however, depends upon where that other
object sits in the inheritance hierarchy.
Figure 5-5. The true meaning of protected
.
If a protected instance variable or instance method is accessible to a class, that class can access the
protected member through a reference only if the reference type is the class or one of its subclasses. For
example, for code in the CoffeeCup
class to invoke getSize()
on a reference to another object, that reference must be of type CoffeeCup
or one of
its subclasses. A CoffeeCup
object could therefore invoke
getSize()
on a CoffeeCup
reference, a
CoffeeMug
reference, or an EspressoCup
reference. A
CoffeeCup
object could not, however, invoke getSize()
on a
Cup
reference or a TeaCup
reference.
If class has a protected variable or method that is static
, the rules are different.
Take as an example the protected static method getCupsInUse()
declared in class
Cup
as shown in Figure 5-5. Any code in a subclass of Cup
can
access a getCupsInUse()
by invoking it on itself or invoking it on a reference of
type Cup
or any of its subclasses. Code in the EspressoCup
class
could invoke getCupsInUse()
on itself or on a reference of type
Cup
, CoffeeCup
, CoffeeMug
,
EspressoCup
, or TeaCup
.
The most important rule of thumb concerning the use of access control modifiers is to keep data private unless you have a good reason not to. Keeping data private is the best way to maximize the robustness and ease of modification of your classes. If you keep data private, other classes can access a class's fields only through its methods. This enables the designer of a class to keep control over the manner in which the class's fields are manipulated. If fields are not private, other classes can change the fields directly, possibly in unpredictable and improper ways. Keeping data private also enables a class designer to more easily change the algorithms and data structures used by a class. Given that other classes can only manipulate a class's private fields indirectly, through the class's methods, other classes will depend only upon the external interface to the private fields provided by the methods. You can change the private fields of a class and modify the code of the methods that manipulate those fields. As long as you don't alter the signature and return type of the methods, the other classes that depended on the previous version of the class will still link properly. Making fields private is the fundamental technique for hiding the implementation of Java classes.
As mentioned in an earlier chapter, one other reason to make data private is because you synchronize access to data by multiple threads through methods. This justification for keeping data private will be discussed in Chapter 17.
As a general rule, the only good non-private field is a final one. Given that final fields cannot be changed after they are initialized, non-private final fields do not run the risk of improper manipulation by other classes. Other classes can use the field, but not change it.
A common use of non-private final fields is to define names to represent a set of valid values that may be passed to (or returned from) a method. As mentioned in Chapter 5, such fields are called constants and are declared static as well as non-private and final. A Java programmer will create constants in this manner in situations where a C++ programmer would have used an enumerated type or declared a "const" member variable.
Rules of thumb such as the ones outlined above are called rules of thumb for a reason: They are not absolute laws. Java allows you to declare fields in classes with any kind of access level, and you may very well encounter situations in which declaring a field private is too restrictive. One potential justification for non-private fields is simple trust. In some situations you may have absolute trust of certain other classes. For example, perhaps you are designing a small set of types that must work together closely to solve a particular problem. It may make sense to put all of these types in their own package, and allow them direct access to some of each other's fields. Although this would create interdependencies between the internal implementations of the classes, you may deem the level of interdependency to be acceptable. If later you change the internal implementation of one of the classes, you'll have to update the other classes that relied on the original implementation. As long as you don't grant access to the fields to classes outside the package, any repercussions of the implementation change will remain inside the package.
Nevertheless, the general rule of thumb in designing packages is to treat the types that share the same package with as much suspicion as types from different packages. If you don't trust classes from other packages to directly manipulate your class's fields, neither should you let classes from the same package directly manipulate them. Keep in mind that you usually can't prevent another programmer from adding new classes to your package, even if you only deliver class files to that programmer. If you leave all your fields with package access, a programmer using your package can easily gain access to those fields by creating a class and declaring it as a member of your package. Therefore, it is best to keep data private, except sometimes when the data is final, so that irrespective of what package classes are defined in, all classes must go through methods to manipulate each other's fields.
The methods you define in public classes should have whatever level of access control matches their role in your program. You should exploit the full range of access levels provided by Java on the methods of your public classes, assigning to each method the most restrictive access level it can reasonably have.
You can use the same rule of thumb to design classes that have package access. You must keep in mind, however, that for package-access classes, fields and methods declared public won't be accessible outside the package. Fields and methods declared protected won't be accessible to subclasses in other packages, because there won't be any subclasses in other packages. Only classes within the same package will be able to subclass the package-access class. Still, you should probably keep the same mindset when designing package-access classes as you do when designing public classes, because at some later time you may turn a package-access class into a public class.
Interfaces have slightly different rules for access levels, because every field and method defined by an
interface is implicitly public. You can't use the keywords private
or
protected
on the fields and methods of interfaces. If you leave off the
public
keyword when declaring interface members, as is officially recommended by
the Java Language Specification, you do not get package access. You still get public access. Therefore, you
can't hide any implementation details of a package inside an interface (You can't hide an interface's
members). On the other hand, you can hide the entire interface. If you don't declare an interface public,
the interface as a whole will only be available to other types in the same package. As with classes, you
should make interfaces public only if they are needed by classes and interfaces defined in other packages.
Here's an example of two interfaces. Interface Soakable
is part of the
internal implementation of a package. Interface Washable
is part of the external
implementation of the package:
// In Source Packet in file // packages/ex5/com/artima/vcafe/dishes/Washable.java package com.artima.vcafe.dishes; public interface Washable { void wash(); } // In Source Packet in file // packages/ex5/com/artima/vcafe/dishes/Soakable.java package com.artima.vcafe.dishes; interface Soakable extends Washable { void soak(); }
In this example, wash()
and breakIt()
are not explicitly
declared public, because they are public by default. Because the Washable
interface
as a whole is not explicitly declared as public, however, it has package access. Interface
Washable
is only be accessible to other types declared in the
com.artima.vcafe.dishes
package. Interface Breakable
,
because it is declared as public, is available to any type declared in any package.
The compiler gives default constructors the same access level as their
class. In the example above, class CoffeeCup
is public, so
the default constructor is public. If CoffeeCup
had been
given package access (which will be defined in , the default constructor would be given package
access as well.
Example: How Singleton pattern can be implemented using private constructors.
Sponsored Links
|