Use Java's packages and static imports to organize top-level types and simplify access to their static members.
In a 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.
What you’ll learn in this Java tutorial
- What are packages in Java?
- Using the package statement
- Using the import statement
- Packages on the Java classpath
- All about static imports
There’s a lot more to discuss and learn about packages, and I’ll have a lot of example for you to explore.
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).
You can run into name conflicts when using the wildcard version of the import statement because any unqualified type name matches the wildcard. For example, you have graphics
and geometry
packages that each contain a Triangle
class, the source code begins with import geometry.*;
and import
graphics.*;
statements, and it also contains an unqualified occurrence of Triangle
. Because the compiler doesn’t know if Triangle
refers to geometry
‘s Triangle
class or to graphics
‘ Triangle
class, it reports an error. You can fix this problem by qualifying Triangle
with the correct package name (graphics.Triangle
or geometry.Triangle
).
To avoid additional problems with the import statement, follow these rules:
- Because Java is case sensitive, package and subpackage names specified in an import statement must be expressed in the same case as that used in the package statement.
- You cannot precede the import statement with anything apart from comments, a package statement, other import statements, and static import statements (which I introduce later in this article).
The compiler automatically imports types from the java.lang
library package. As a result, you don’t have to specify import java.lang.System;
(import java.lang
‘s System
class) or similar import statements in your source code.
Because Java implementations map package and subpackage names to same-named directories, an import statement is equivalent to loading a reference type’s class file from the directory sequence corresponding to the package sequence.
Importing types
Continuing the packaged audio library example, I’ve created a UseAudio
application that shows how to import and work with this library’s Audio
type. Listing 3 presents this application’s source code.
Listing 3. Importing types (UseAudio.java)
import ca.javajeff.audio.Audio;
public final class UseAudio
{
public static void main(String[] args)
{
if (args.length != 1)
{
System.err.println("usage: java UseAudio filename");
return;
}
Audio audio = Audio.newAudio(args[0]);
if (audio == null)
{
System.err.println("unsupported audio format");
return;
}
System.out.println("Samples");
for (int i = 0; i < audio.getSamples().length; i++)
System.out.print(audio.getSamples()[i] + " ");
System.out.println();
System.out.println("Sample Rate: " + audio.getSampleRate());
}
}
Listing 3 doesn’t begin with a package statement because simple applications are typically not stored in packages. Instead, it begins with an import statement for importing the audio library’s Audio
class.
The main()
method first verifies that a single command-line argument has been specified. If the verification succeeds, it passes this argument to Audio.newAudio()
and assigns the returned Audio
object’s reference to a local variable named audio
. main()
then proceeds to verify that audio
isn’t null and (in this case) interrogate the Audio
object, outputting the audio clip’s sample values along with its sample rate.
Copy Listing 3 to a file named UseAudio.java
and place this file in the same directory as the ca
directory that you previously created. Then, execute the following command to compile UseAudio.java
:
javac UseAudio.java
If all goes well, you should observe UseAudio.class
in the current directory.
Execute the following command to run UseAudio
against a fictitious WAV file named audio.wav
:
java UseAudio audio.wav
You should observe the following output:
Samples
Sample Rate: 0
Suppose that UseAudio.java
wasn’t located in the same directory as ca
. How would you compile this source file and run the resulting application? The answer is to use the classpath.
The Java classpath
The Java classpath is a sequence of packages that the Java virtual machine (JVM) searches for reference types. It’s specified via the -classpath
(or -cp
) option used to start the JVM or, when not present, the CLASSPATH
environment variable.
Suppose (on a Windows platform) that the audio library is stored in C:audio
and that UseAudio.java
is stored in C:UseAudio
, which is current. Specify the following commands to compile the source code and run the application:
javac -cp ../audio UseAudio.java
java -cp ../audio;. UseAudio audio.wav
The period character in the java
-prefixed command line represents the current directory. It must be specified so that the JVM can locate UseAudio.class
.
Additional package topics
The Java language includes a protected
keyword, which is useful in a package context. Also, packages can be distributed in JAR files. Furthermore, the JVM follows a specific search order when searching packages for reference types (regardless of whether or not these packages are stored in JAR files). We’ll explore these topics next.
Protected access
The protected
keyword assigns the protected access level to a class member, such as a field or method (as an example, protected void clear()
). Declaring a class member protected
makes the member accessible to all code in any class located in the same package and to subclasses regardless of their packages.
Joshua Bloch explains the rationale for giving class members protected access in his book, Effective Java Second Edition (“Item 17: Design and document for inheritance or else prohibit it”). They are hooks into a class’s internal workings to let programmers “write efficient subclasses without undue pain.” Check out the book for more information.
JAR files
Distributing a package by specifying instructions for creating the necessary directory structure along with the package’s class files (and instructions on which class files to store in which directories) would be a tedious and error-prone task. Fortunately, JAR files offer a much better alternative.
A JAR (Java archive) file is a ZIP archive with a .jar
extension (instead of the .zip
extension). It includes a special META-INF
directory containing manifest.mf
(a special file that stores information about the contents of the JAR file) and a hierarchical directory structure that organizes class files.
You use the JDK’s jar
tool to create and maintain a JAR file. You can also view the JAR file’s table of contents. To show you how easy it is to use this tool, we’ll create an audio.jar
file that stores the contents of the ca.javajeff.audio
package. We’ll then access this JAR file when running UseAudio.class
. Create audio.jar
as follows:
First, make sure that the current directory contains the previously created ca / javajeff / audio
directory hierarchy, and that audio
contains audio.class
and WavReader.class
.
Second, execute the following command:
jar cf audio.jar cajavajeffaudio*.class
The c
option stands for “create new archive” and the f
option stands for “specify archive filename”.
You should now find an audio.jar
file in the current directory. Prove to yourself that this file contains the two class files by executing the following command, where the t
option stands for “list table of contents”:
jar tf audio.jar
You can run UseAudio.class
by adding audio.jar
to its classpath. For example, assuming that audio.jar
is located in the same directory as UseAudio.class
, you can run UseAudio
under Windows via the following command:
java -classpath audio.jar;. UseAudio
For convenience, you could specify the shorter -cp
instead of the longer -classpath
.
Searching packages for reference types
Newcomers to Java packages often become frustrated by “no class definition found” and other errors. This frustration can be partly avoided by understanding how the JVM looks for reference types. To understand this process, you must realize that the compiler is a special Java application that runs under the control of the JVM. Also, there are two forms of search: compile-time search and runtime search.
Compile-time search
When the compiler encounters a type expression (such as a method call) in source code, it must locate that type’s declaration to verify that the expression is legal. As an example, it might check to see that a method exists in the type’s class, whose parameter types match the types of the arguments passed in the method call.
The compiler first searches the Java platform packages (in rt.jar
and other JAR files), which contain Java’s standard class library types (such as java.lang
‘s System
class). It then searches extension packages for extension types. If the -sourcepath
option is specified when starting javac
, the compiler searches the indicated path’s source files.
Otherwise, the compiler searches the classpath (in left-to-right order) for the first class file or source file containing the type. If no classpath is present, the current directory is searched. If no package matches or the type still cannot be found, the compiler reports an error. Otherwise, it records the package information in the class file.
Runtime search
When the compiler or any other Java application runs, the JVM will encounter types and must load their associated class files via special code known as a classloader. The JVM will use the previously stored package information that’s associated with the encountered type in a search for that type’s class file.
The JVM searches the Java platform packages, followed by extension packages, followed by the classpath or current directory (when there is no classpath) for the first class file that contains the type. If no package matches or the type cannot be found, a “no class definition found” error is reported. Otherwise, the class file is loaded into memory.
Statically importing static members
In Effective Java Second Edition, Item 19, Joshua Bloch mentions that Java developers should only use interfaces to declare types. We should not use interfaces to declare constant interfaces, which are interfaces that only exist to export constants. Listing 4’s Switchable
constant interface provides an example.
Listing 4. A constant interface (Switchable.java)
public interface Switchable
{
boolean OFF = false;
boolean ON = true;
}
Developers resort to constant interfaces to avoid having to prefix the constant’s name with the name of its reference type (e.g., Math.PI
). For example, consider Listing 5’s Light
class, which implements the Switchable
interface so that the developer is free to specify constants OFF
and ON
without having to include class prefixes (if they were declared in a class).
Listing 5. Light implements Switchable (Light.java, version 1)
public class Light implements Switchable
{
private boolean state = OFF;
public void printState()
{
System.out.printf("state = %s%n", (state == OFF) ? "OFF" : "ON");
}
public void toggle()
{
state = (state == OFF) ? ON : OFF;
}
}
A constant interface provides constants that are intended to be used in a class’s implementation. As an implementation detail, you shouldn’t leak constants into the class’s exported API because they could confuse others using your class. Furthermore, to preserve binary compatibility, you’re committed to supporting them, even when the class is no longer using them.
Static imports
To satisfy the need for constant interfaces while avoiding the problems imposed by using them, Java 5 introduced static imports. This language feature can be used to import a reference type’s static members. It’s implemented via the import static
statement whose syntax appears below:
import static packagespec . typename . ( staticmembername | * );
Placing static
after import
distinguishes this statement from a regular import statement. The syntax is similar to the regular import
statement in terms of the standard period-separated list of package and subpackage names. You can import either a single static member name or all static member names (thanks to the asterisk). Consider the following examples:
import static java.lang.Math.*; // Import all static members from Math.
import static java.lang.Math.PI; // Import the PI static constant only.
import static java.lang.Math.cos; // Import the cos() static method only.
Once you’ve imported them, you can specify static members without having to prefix them with their type names. For example, after specifying either the first or third static import, you could specify cos
directly, as in [>
double
cosine = cos(angle);
To fix Listing 5 so that it no longer relies on implements Switchable
, we can insert a static import, as demonstrated in Listing 6.
Listing 6. A static import improves the implementation of Switchable (Light.java, version 2)
package foo;
import static foo.Switchable.*;
public class Light
{
private boolean state = OFF;
public void printState()
{
System.out.printf("state = %s%n", (state == OFF) ? "OFF" : "ON");
}
public void toggle()
{
state = (state == OFF) ? ON : OFF;
}
}
Listing 6 begins with a package foo;
statement because you cannot import static members from a type located in the unnamed package. This package name appears as part of the subsequent static import:
import static
foo.Switchable.*;
What to watch out for when using static imports
There are two additional cautions concerning static imports.
First, when two static imports import the same-named member, the compiler reports an error. For example, suppose package physics
contains a Math
class that’s identical to java.lang
‘s Math
class in that it implements the same PI
constant and trigonometric methods. When confronted by the following code fragment, the compiler reports errors because it cannot determine whether java.lang.Math
‘s or physics.Math
‘s PI
constant is being accessed and cos()
method is being called:
import static java.lang.Math.cos;
import static physics.Math.cos;
double angle = PI;
System.out.println(cos(angle));
Second, overusing static imports pollutes the code’s namespace with all of the static members you import, which can make your code unreadable and unmaintainable. Also, anyone reading your code could have a hard time finding out which type a static member comes from, especially when importing all static member names from a type.
Conclusion
Packages help you create reusable libraries of reference types with their methods. If you should call a method (whether packaged into a library or not) with an illegal argument (such as a negative index for an array), you’ll probably run into a Java exception.