In my previous Java 101 tutorial, you learned how to better organize your code by declaring reference types (also known as classes and interfaces) as members of other reference types and blocks. I also showed you how to use nesting to avoid name conflicts between nested reference types and top-level reference types that share the same name.
Along with nesting, Java uses packages to resolve same-name issues in top-level reference types. Using static imports also simplifies access to the static members in packaged top-level reference types. Static imports will save you keystrokes when accessing these members in your code, but there are a few things to watch out for when you use them. In this tutorial, I will introduce you to using packages and static imports in your Java programs.
Packaging reference types
Java developers group related classes and interfaces into packages. Using packages makes it easier to locate and use reference types, avoid name conflicts between same-named types, and control access to types.
In this section, you'll learn about packages. You'll find out what packages are, learn about the package
and import
statements, and explore the additional topics of protected access, JAR files, and type searches.
What are packages in Java?
In software development, we commonly organize items according to their hierarchical relationships. For example, in the previous tutorial, I showed you how to declare classes as members of other classes. We can also use file systems to nest directories in other directories.
Using these hierarchical structures will help you avoid name conflicts. For example, in a non-hierarchical file system (a single directory), it's not possible to assign the same name to multiple files. In contrast, a hierarchical file system lets same-named files exist in different directories. Similarly, two enclosing classes can contain same-named nested classes. Name conflicts don't exist because items are partitioned into different namespaces.
Java also allows us to partition top-level (non-nested) reference types into multiple namespaces so that we can better organize these types and to prevent name conflicts. In Java, we use the package language feature to partition top-level reference types into multiple namespaces. In this case, a package is a unique namespace for storing reference types. Packages can store classes and interfaces, as well as subpackages, which are packages nested within other packages.
A package has a name, which must be a non-reserved identifier; for example, java
. The member access operator (.
) separates a package name from a subpackage name and separates a package or subpackage name from a type name. For example, the two-member access operators in java.lang.System
separate package name java
from the lang
subpackage name and separate subpackage name lang
from the System
type name.
Reference types must be declared public
to be accessible from outside their packages. The same applies to any constants, constructors, methods, or nested types that must be accessible. You'll see examples of these later in the tutorial.
The package statement
In Java, we use the package statement to create a package. This statement appears at the top of a source file and identifies the package to which the source file types belong. It must conform to the following syntax:
package identifier[.identifier]*;
A package statement starts with the reserved word package
and continues with an identifier, which is optionally followed by a period-separated sequence of identifiers. A semicolon (;
) terminates this statement.
The first (left-most) identifier names the package, and each subsequent identifier names a subpackage. For example, in package a.b;
, all types declared in the source file belong to the b
subpackage of the a
package.
A sequence of package names must be unique to avoid compilation problems. For example, suppose you create two different graphics
packages, and assume that each graphics
package contains a Triangle
class with a different interface. When the Java compiler encounters something like what's below, it needs to verify that the Triangle(int, int, int, int)
constructor exists:
Triangle
t = new Triangle(1, 20, 30, 40);
The compiler will search all accessible packages until it finds a graphics
package that contains a Triangle
class. If the found package includes the appropriate Triangle
class with a Triangle(int, int, int, int)
constructor, everything is fine. Otherwise, if the found Triangle
class doesn't have a Triangle(int, int, int, int)
constructor, the compiler reports an error. (I'll say more about the search algorithm later in this tutorial.)
This scenario illustrates the importance of choosing unique package name sequences. The convention in selecting a unique name sequence is to reverse your Internet domain name and use it as a prefix for the sequence. For example, I would choose ca.javajeff
as my prefix because javajeff.ca
is my domain name. I would then specify ca.javajeff.graphics.Triangle
to access Triangle
.
You need to follow a couple of rules to avoid additional problems with the package statement:
- You can declare only one package statement in a source file.
- You cannot precede the package statement with anything apart from comments.
The first rule, which is a special case of the second rule, exists because it doesn't make sense to store a reference type in multiple packages. Although a package can store multiple types, a type can belong to only one package.
When a source file doesn't declare a package statement, the source file's types are said to belong to the unnamed package. Non-trivial reference types are typically stored in their own packages and avoid the unnamed package.
Java implementations map package and subpackage names to same-named directories. For example, an implementation would map graphics
to a directory named graphics
. In the case of the package a.b
, the first letter, a would map to a directory named a
and b would map to a b
subdirectory of a
. The compiler stores the class files that implement the package's types in the corresponding directory. Note that the unnamed package corresponds to the current directory.
Example: Packaging an audio library in Java
A practical example is helpful for fully grasping the package
statement. In this section I demonstrate packages in the context of an audio library that lets you read audio files and obtain audio data. For brevity, I'll only present a skeletal version of the library.
The audio library currently consists of only two classes: Audio
and WavReader
. Audio
describes an audio clip and is the library's main class. Listing 1 presents its source code.
Listing 1. Package statement example (Audio.java)
package ca.javajeff.audio;
public final class Audio
{
private int[] samples;
private int sampleRate;
Audio(int[] samples, int sampleRate)
{
this.samples = samples;
this.sampleRate = sampleRate;
}
public int[] getSamples()
{
return samples;
}
public int getSampleRate()
{
return sampleRate;
}
public static Audio newAudio(String filename)
{
if (filename.toLowerCase().endsWith(".wav"))
return WavReader.read(filename);
else
return null; // unsupported format
}
}
Let's go through Listing 1 step by step.
- The
Audio.java
file in Listing 1 stores theAudio
class. This listing begins with a package statement that identifiesca.javajeff.audio
as the class's package. Audio
is declaredpublic
so that it can be referenced from outside of its package. Also, it's declaredfinal
so that it cannot be extended (meaning, subclassed).Audio
declaresprivate
samples
andsampleRate
fields to store audio data. These fields are initialized to the values passed toAudio
's constructor.Audio
's constructor is declared package-private (meaning, the constructor isn't declaredpublic
,private
, orprotected
) so that this class cannot be instantiated from outside of its package.Audio
presentsgetSamples()
andgetSampleRate()
methods for returning an audio clip's samples and sample rate. Each method is declaredpublic
so that it can be called from outside ofAudio
's package.Audio
concludes with apublic
andstatic
newAudio()
factory method for returning anAudio
object corresponding to thefilename
argument. If the audio clip cannot be obtained,null
is returned.newAudio()
comparesfilename
's extension with.wav
(this example only supports WAV audio). If they match, it executesreturn WavReader.read(filename)
to return anAudio
object with WAV-based audio data.
Listing 2 describes WavReader
.
Listing 2. The WavReader helper class (WavReader.java)
package ca.javajeff.audio;
final class WavReader
{
static Audio read(String filename)
{
// Read the contents of filename's file and process it
// into an array of sample values and a sample rate
// value. If the file cannot be read, return null. For
// brevity (and because I've yet to discuss Java's
// file I/O APIs), I present only skeletal code that
// always returns an Audio object with default values.
return new Audio(new int[0], 0);
}
}
WavReader
is intended to read a WAV file's contents into an Audio
object. (The class will eventually be larger with additional private
fields and methods.) Notice that this class isn't declared public
, which makes WavReader
accessible to Audio
but not to code outside of the ca.javajeff.audio
package. Think of WavReader
as a helper class whose only reason for existence is to serve Audio
.
Complete the following steps to build this library:
- Select a suitable location in your file system as the current directory.
- Create a
ca/javajeff/audio
subdirectory hierarchy within the current directory. - Copy Listings 1 and 2 to files
Audio.java
andWavReader.java
, respectively; and store these files in theaudio
subdirectory. - Assuming that the current directory contains the
ca
subdirectory, executejavac ca/javajeff/audio/*.java
to compile the two source files inca/javajeff/audio
. If all goes well, you should discoverAudio.class
andWavReader.class
files in theaudio
subdirectory. (Alternatively, for this example, you could switch to theaudio
subdirectory and executejavac *.java
.)
Now that you've created the audio library, you'll want to use it. Soon, we'll look at a small Java application that demonstrates this library. First, you need to learn about the import statement.
Java's import statement
Imagine having to specify ca.javajeff.graphics.Triangle
for each occurrence of Triangle
in source code, repeatedly. Java provides the import statement as a convenient alternative for omitting lengthy package details.
The import statement imports types from a package by telling the compiler where to look for unqualified (no package prefix) type names during compilation. It appears near the top of a source file and must conform to the following syntax:
import identifier[.identifier]*.(typeName | *);
An import statement starts with reserved word import
and continues with an identifier, which is optionally followed by a period-separated sequence of identifiers. A type name or asterisk (*
) follows, and a semicolon terminates this statement.
The syntax reveals two forms of the import statement. First, you can import a single type name, which is identified via typeName
. Second, you can import all types, which is identified via the asterisk.
The *
symbol is a wildcard that represents all unqualified type names. It tells the compiler to look for such names in the right-most package of the import statement's package sequence unless the type name is found in a previously searched package. Note that using the wildcard doesn't have a performance penalty or lead to code bloat. However, it can lead to name conflicts, which you will see.
For example, import ca.javajeff.graphics.Triangle;
tells the compiler that an unqualified Triangle
class exists in the ca.javajeff.graphics
package. Similarly, something like
import
ca.javajeff.graphics.*;
tells the compiler to look in this package when it encounters a Triangle
name, a Circle
name, or even an Account
name (if Account
has not already been found).