Java 101: Packages organize classes and interfaces

Create packages, import packaged classes and interfaces into your programs, move packages, and encapsulate them in jar files

1 2 3 Page 2
Page 2 of 3

The compiler regards the code fragment as an attempt to introduce two reference types with the same Date name:

  1. The imported Date reference type name from java's util subpackage via the single-type import directive
  2. The Date class declaration

If the compiler permitted the above code fragment to compile, which Date reference type would the compiler refer to upon encountering Date d = new Date ();?

Caution
A single-type import directive that imports a reference type name also declared in a source file causes the compiler to report an error.

In contrast to a single-type import directive, a type-on-demand import directive imports public reference type names from a package on an as-needed basis, as the following code fragment demonstrates:

import java.net.*;

According to the code fragment above, if Socket or another java.net reference type name appears in a source file, that name can appear without a java.net prefix. During compilation, when the compiler detects Socket, it searches java.net to verify that Socket is a member of java's net subpackage. Once verified, the compiler stores java.net with Socket in the classfile.

As with the single-type import directive, the type-on-demand import directive has problems. Consider the following code fragment:

import a.*;
import b.*;
X x = new X ();

Suppose packages a and b each contain an X class. When the compiler determines that X is present in a and b, the compiler reports an error because each package might have a different X implementation, and the compiler has no way of knowing which X to import. To solve the problem, the developer must prefix each occurrence of X with the fully qualified package name. For the code fragment above, this leads to either a.X x = new a.X (); or b.X x = new b.X ();.

Caution
The compiler reports an error upon encountering a reference type name and two or more type-on-demand import directives whose packages include that type name.

You can use a type-on-demand import directive to save yourself the trouble of specifying testpkg., wherever A appears in Usetestpkg1, as shown in Listing 3:

Listing 3. Usetestpkg2.java

// Usetestpkg2.java
import testpkg.*;
class Usetestpkg2 implements A.StartStop
{
   public static void main (String [] args)
   {
      A a = new A ();
      // Following code does not compile because x is not public outside
      // testpkg.A
      // System.out.println (a.x);
      // Following code does not compile because returnx() is not public
      // outside testpkg.A
      // System.out.println (a.returnx ());
      System.out.println (a.y);
      System.out.println (a.returny ());
      // Following code does not compile because z has protected access
      // in testpkg.A
      // System.out.println (a.z);
      // Following code does not compile because returnz() has protected
      // access in testpkg.A
      // System.out.println (a.returnz ());
      SubA sa = new SubA ();
      System.out.println (sa.returnZ ());
      // Following code does not compile because testpkg.B is not public
      // in testpkg and hello() is not defined in a public class
      // B.hello ();
      Usetestpkg2 utp = new Usetestpkg2 ();
      utp.start ();
      utp.stop ();
   }
   public void start ()
   {
      System.out.println ("Start");
   }
   public void stop ()
   {
      System.out.println ("Stop");
   }
}
class SubA extends A
{
   int returnZ ()
   {
      // It is legal (regardless of package) for a subclass method to
      // call a superclass's protected method
      return super.returnz ();
   }
}

Usetestpkg2's type-on-demand import directive imports reference type names A and A.StartStop from testpkg. What about class B? We can't import that class name because B is not a public class within testpkg. As a result, only code within testpkg's class A (and any other class you subsequently add to that package) can access B.

Caution
Any attempt to import nonpublic classes or interfaces from a package causes the compiler to report an error.

Usetestpkg2 demonstrates that you can subclass a package's public class (A) and access that class's protected members (such as int returnz()). To prove that, compile Usetestpkg2 (which you place in the same directory as Usetestpkg1) and run the program. If successful, you see the following output:

2
2
3
Start
Stop

3 is the value of testpkg.A's protected z field, which testpkg.A's protected returnz() method returns (via SubA's int returnZ() method).

Unlike returnz(), you cannot call int returnx() because the absence of an access modifier keyword (private, public, or protected) implies package access—and a field or method with package access is only accessible or callable from code within its class's (or interface's) package.

Note
Some reference types, such as Object and String, are located in java.lang. Because programs frequently access java.lang's reference types, the compiler implicitly imports all reference type names on an as-needed basis from java's lang subpackage. As a result, you don't need to specify import java.lang.*; (although you can do so, if desired).

Move packages on the hard drive

For organizational purposes, you occasionally move package directories from place to place on the hard drive. Accomplishing that task requires an understanding of how the classloader locates package directories and classfiles at runtime. To allow the classloader to find package directories and classfiles, the compiler places into a classfile fully qualified package name information for all reference type names appearing in the source file that have declarations existing in a package other than the source file's package. But that information helps with only part of the search. The rest of the search depends on the presence or absence of the classpath environment variable.

Operating systems like Windows, Linux, and Solaris maintain environment variables—named entities that let you configure different aspects of an operating system. Java recognizes one environment variable that helps the classloader locate package directories and classfiles at runtime: classpath. If classpath exists, the classloader focuses on classpath's list of directory paths. The search begins with the leftmost directory path and continues to the rightmost directory path until either a directory path is found that contains the searched-for classfile name (or package directory name) or the classloader throws a NoClassDefFoundError object. For example, in Usetestpkg1 and Usetestpkg2, suppose you move the testpkg directory from the root directory to a packages directory just below the root directory. Assuming the Usetestpkg1.class, Usetestpkg2.class, and SubA.class classfiles exist in the root directory, and assuming classpath is not set, attempts to execute java Usetestpkg1 or java Usetestpkg2 from the root directory result in NoClassDefFoundErrors—because the classloader cannot find testpkg in the root directory. To solve that problem, set classpath to both the current directory (the root directory, in this example) and the packages directory as follows: set classpath=.;\packages (under Windows) or setenv classpath .:/packages (under Solaris via the csh shell program).

Note
You should include the current directory, via a period character, in the classpath environment variable so the classloader will locate a program's starting classfile (such as Usetestpkg2.class) and other classfiles (such as SubA.class) that exist in the unnamed package.

When classpath is absent, the classloader limits its search to the current directory. As the classloader encounters a reference type name in a loaded classfile, the classloader searches the current directory for either a classfile name (if no fully qualified package name appears in the reference type name) that matches the reference type name or a directory whose name matches the first identifier in a fully qualified package name. For example, in Usetestpkg1, assume no classpath environment variable exists and that the root directory is the current directory when you execute java Usetestpkg1. Upon encountering testpkg, the classloader searches the root directory for a testpkg directory. If found, the classloader searches testpkg for classfiles A$StartStop.class and A.class. If the classloader cannot find either testpkg or one of the classfiles, the classloader throws a NoClassDefFoundError object and the java.exe (or equivalent) program terminates. Otherwise, the classloader loads the classfiles.

Note
If a fully qualified package name includes a subpackage name, the classloader searches the first package identifier's matching directory for a directory whose name matches the second package identifier. For example, in a.b, the classloader searches directory a for directory b. This process continues until all directories corresponding to package identifiers have been found. After that, the classloader searches the final directory for a classfile.

Jar files and packages

In addition to directory paths, classpath optionally identifies Java Archive files (jar files, or JARs) that house packages. A jar file is a zip file with a .jar file extension, instead of a .zip file extension.

Jar files conveniently distribute packages; a developer needs to only distribute a single jar file instead of a package's multiple files. Furthermore, a developer does not need to decipher how to recreate a package's directory hierarchy because a jar file internally contains that hierarchy. Finally, compressed jar files reduce less storage space and speeds transmission over a network compared to equivalent non-JAR-based packages. Sun recognizes these advantages and distributes its set of standard packages in a single rt.jar (runtime) jar file.

Let's convert the earlier testpkg package into a jar file. The following steps assume a testpkg directory in the root directory, and A$StartStop.class, A.class, and B.class classfiles in testpkg:

  1. Open a Windows command window and make sure you are in the c: drive's root directory.
  2. Ensure the classpath environment variable does not exist by executing set classpath=.
  3. Copy the following text into a file named manifest and store this file in the root directory:

    Specification-Title: testPkg specification title
    Specification-Vendor: me
    Specification-Version: 1.0
    Implementation-Title: testPkg implementation title
    Implementation-Vendor: you
    Implementation-Version: 1.0.1
    
  4. Execute jar cfm testpkg.jar manifest testpkg. If successful, you should see testpkg.jar appear in the root directory.
  5. Once testpkg.jar appears, execute deltree testpkg to delete the testpkg directory.

The previous steps introduce the following concepts:

  1. Sun's SDK includes a jar program that creates and manages jar files. A list of options follows jar on the command line. The cfm options are interpreted as follows: c creates a jar file, f identifies the jar file as testpkg.jar, and m identifies manifest as the file containing manifest information. Following cfm are the names of the jar file, the manifest file, and the package directory—in the same order as the options specify.
  2. A manifest file stores metadata, data that describes a jar file's contents, in a jar file. In our example, that metadata consists of Specification-Title, Specification-Vendor, Specification-Version, Implementation-Title, Implementation-Vendor, and Implementation-Version.

Now that you have a testpkg.jar file, how do you reference that file so you can run Usetestpkg1 and Usetestpkg2? Include testpkg.jar and the current directory in your classpath environment variable by executing set classpath=testpkg.jar;. (or the platform-equivalent). Then execute java Usetestpkg1 and Usetestpkg2. You should see the same output as you saw earlier in this article.

Package information

I created a manifest file in the previous section to introduce you to the java.lang.Package class. That class allows you to obtain specification and implementation title, vendor, and version strings from a package. Specification refers to a plan to which a package's classes and interfaces conform. In contrast, implementation refers to a level of specification conformance. In both cases, the version string consists of period-separated positive integers, similar to the Dewey decimal system (for example, 1.6.3, 2.5). Specification and implementation strings prove useful in reporting package problems. Furthermore, the implementation version string is useful in programmatically determining if a package's correct version has been installed. To give you a taste of Package, I wrote an application that retrieves and displays a package's specification and implementation strings:

Listing 4. PkgInfo.java

1 2 3 Page 2
Page 2 of 3