5.5.2 Modularity woes

JAR files are the preferred deployment format for Java libraries. Such files contain compiled Java bytecode, as well as a “manifest” file holding metadata. JAR files cannot be considered true components, as they have no identity at runtime. All packages that are part of a JAR file are placed in the same global namespace as packages loaded from a different JAR file.1 This has subtle implications that serve to illustrate the necessity for components to be identifiable at runtime.

For simple cases, JAR files work well. Java’s use of packages ensures that two classes or interfaces that share the same name and are provided by two different libraries can co-exist, as they are placed in different packages. The system breaks down, however, when an attempt to load multiple versions of the same JAR file is attempted.

Complex Java projects often have a large number of dependencies, which can number in the hundreds. A dependency often has dependencies of its own, resulting in complex dependency graphs. One JAR file may be dependent on a certain version of a library, while another JAR file is dependent on another version. Java simply attempts to load the first version it encounters,2 which may or may not be compatible with the JAR file expecting a different version. Compounding the problem, if the loaded JAR file does not contain a class or interface that is part of a JAR file that was initially not loaded, Java will attempt to load the desired class or interface from the latter JAR file. The end result is a toxic mix of incompatible classes and interfaces, resulting in runtime errors, or worse, unpredictable behavior.

Java provides access specifiers that allow developers to control access to classes, interfaces, and fields. A class or interface can either be exposed to all other classes running in the same virtual machine using the public access specifier, or it can be designated package-private, meaning that a particular type may not be accessed from outside its parent package. It is not, however, possible to expose a class or interface only to the JAR file in which it resides, which illustrates another problem with JAR files not having an identity at runtime.3

Footnotes

  1. Packages that house the Java class library and packages that are part of user-installed extensions are actually part of different namespaces.
  2. This issue is related to the mechanism used by Java to load classes into a virtual machine, known as class loaders (discussed in section 5.5.3). The application class loader, which is normally responsible for loading classes that are neither part of the core runtime system nor part of a system extension, consults the classpath to find classes. The classpath is a delimiter-separated string containing the locations used to find classes, either directory names or the locations of JAR files. The application class loader is satisfied when it encounters a directory or JAR file that contains the sought class or interface. Once a class or interface has been found, no further classpath entries are consulted.
  3. The common work-around is to define a package named “internal,” and place classes and interfaces that should not be exposed to the outside world in that package or its subpackages. This informal means of limiting access to internal types cannot easily be enforced at compile-time, though.