Advanced Java language features

Packages and static imports in Java

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
Thinkstock

Advanced Java language features

Show More

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).

1 2 3 Page 1
Page 1 of 3