Packages and static imports in Java

how-to
12 May 202023 mins
Core JavaJavaProgramming Languages

Use packages and static imports to organize top-level types and simplify access to their static members

Parcels and stacked packages being protected by black umbrella
Credit: Thinkstock

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.

download
Download the source code for example applications in this Java tutorial. Created by Jeff Friesen for JavaWorld.

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:

  1. You can declare only one package statement in a source file.
  2. 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 the Audio class. This listing begins with a package statement that identifies ca.javajeff.audio as the class’s package.
  • Audio is declared public so that it can be referenced from outside of its package. Also, it’s declared final so that it cannot be extended (meaning, subclassed).
  • Audio declares private samples and sampleRate fields to store audio data. These fields are initialized to the values passed to Audio‘s constructor.
  • Audio‘s constructor is declared package-private (meaning, the constructor isn’t declared public, private, or protected) so that this class cannot be instantiated from outside of its package.
  • Audio presents getSamples() and getSampleRate() methods for returning an audio clip’s samples and sample rate. Each method is declared public so that it can be called from outside of Audio‘s package.
  • Audio concludes with a public and static newAudio() factory method for returning an Audio object corresponding to the filename argument. If the audio clip cannot be obtained, null is returned.
  • newAudio() compares filename‘s extension with .wav (this example only supports WAV audio). If they match, it executes return WavReader.read(filename) to return an Audio 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:

  1. Select a suitable location in your file system as the current directory.
  2. Create a ca/javajeff/audio subdirectory hierarchy within the current directory.
  3. Copy Listings 1 and 2 to files Audio.java and WavReader.java, respectively; and store these files in the audio subdirectory.
  4. Assuming that the current directory contains the ca subdirectory, execute javac ca/javajeff/audio/*.java to compile the two source files in ca/javajeff/audio. If all goes well, you should discover Audio.class and WavReader.class files in the audio subdirectory. (Alternatively, for this example, you could switch to the audio subdirectory and execute javac *.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 graphicsTriangle 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:

  1. 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.
  2. 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.

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.

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 an exception. My next Java 101 tutorial introduces Java exceptions. Jump to that article when you are ready to explore Java’s language features for writing code that works, even when exceptions occur.

Exit mobile version