Viewing Dependencies – Java Module System

Viewing Dependencies

The Java Dependency Analysis tool, jdeps, is a versatile command-line tool for static analysis of dependencies between Java artifacts like class files and JARs. It can analyze dependencies at all levels: module, package, and class. It allows the results to be filtered and aggregated in various ways, even generating various graphs to illustrate its findings. It is an indispensable tool when migrating non-modular code to make use of the module system.

In this section, we confine our discussion to modular code—that is, either an exploded module directory with the compiled module code (e.g., mods/main) or a modular JAR (e.g., mlib/main.jar).

In this section, the results from some of the jdeps commands have been edited to fit the width of the page without any loss of information, or elided to shorten repetitious outputs.

Viewing Package-Level Dependencies

The jdeps command with no options, shown below, illustrates the default behavior of the tool. Line numbers have been added for illustrative purposes.

When presented with the root module directory mods/model (1), the default behavior of jdeps is to print the name of the module (2), the path to its location (3), its module descriptor (4), followed by its module dependencies (5), and lastly the package-level dependencies (6). The package-level dependency at (6) shows that the package com.passion.model in the model module depends on the java.lang package in the proverbial java.base module.

Click here to view code image

(1) 
>jdeps mods/model

(2)  model
(3)  [file:…/adviceApp/mods/model/]
(4)     requires mandated java.base (@17.0.2)
(5)  model -> java.base
(6)     com.passion.model           -> java.lang           java.base

The following jdeps command if let loose on the model.jar archive will print the same information for the model module, barring the difference in the file location:

>
jdeps mlib/model.jar

However, the following jdeps command with the main.jar archive gives an error:

Click here to view code image

>jdeps mlib/main.jar

Exception in thread “main” java.lang.module.FindException: Module controller
not found, required by main
       at java.base/…

Dependency analysis cannot be performed on the main module by the jdeps tool because the modules the main module depends on cannot be found. In the case of the model module, which does not depend on any other user-defined module, the dependency analysis can readily be performed.

The two options –module-path (no short form) and –module (short form: -m) in the jdeps command below unambiguously specify the location of other modular JARs and the module to analyze, respectively. The format of the output is the same as before, and can be verified easily.

Click here to view code image

>jdeps –module-path mlib –module main
main
 [file:…/adviceApp/mlib/main.jar]
   requires controller
   requires mandated java.base (@17.0.2)
main -> controller
main -> java.base
   com.passion.main     -> com.passion.controller     controller
   com.passion.main     -> java.lang                  java.base

However, if one wishes to analyze all modules that the specified module depends on recursively, the –recursive option (short form: -R) can be specified. The output will be in the same format as before, showing the package-level dependencies for each module. The output from the jdeps command for each module is elided below, but has the same format we have seen previously.

Click here to view code image

>
jdeps –module-path mlib –module main –recursive

controller

main

model

view


Creating Path Objects with the Paths.get() Method – Java I/O: Part II

Creating Path Objects with the Paths.get() Method

The Paths utility class provides the get(String first, String… more) static factory method to construct Path objects. In fact, this method invokes the Path.of(String first, String… more) convenience method to obtain a Path.

Click here to view code image

Path absPath7 = Paths.get(nameSeparator, “a”, “b”, “c”);
Path relPath3 = Paths.get(“c”, “d”);

Creating Path Objects Using the Default File System

We have seen how to obtain the default file system that is accessible to the JVM:

Click here to view code image

FileSystem dfs = FileSystems.getDefault();     // The default file system

The default file system provides the getPath(String first, String… more) method to construct Path objects. In fact, the Path.of(String first, String… more) method is a convenience method that invokes the FileSystem.getPath() method to obtain a Path object.

Click here to view code image

Path absPath6 = dfs.getPath(nameSeparator, “a”, “b”, “c”);
Path relPath2 = dfs.getPath(“c”, “d”);

Interoperability with the java.io.File Legacy Class

An object of the legacy class java.io.File can be used to query the file system for information about a file or a directory. The class also provides methods to create, rename, and delete directory entries in the file system. Although there is an overlap of functionality between the Path interface and the File class, the Path interface is recommended over the File class for new code. The interoperability between a File object and a Path object allows the limitations of the legacy class java.io.File to be addressed.

Click here to view code image

File(String pathname)
Path toPath()

This constructor and this method of the java.io.File class can be used to create a File object from a pathname and to convert a File object to a Path object, respectively.

default File toFile()

This method of the java.nio.file.Path interface can be used to convert a Path object to a File object.

The code below illustrates the round trip between a File object and a Path object:

Click here to view code image

File file = new File(File.separator + “a” +
                     File.separator + “b” +
                     File.separator + “c”);        // /a/b/c
// File –> Path, using the java.io.File.toPath() instance method
Path fileToPath = file.toPath();                   // /a/b/c
// Path –> File, using the java.nio.file.Path.toFile() default method.
File pathToFile = fileToPath.toFile();             // /a/b/c

Selected Options for the jlink Tool – Java Module System

Selected Options for the jlink Tool

The jlink tool creates a runtime image of an application. A typical command to create a runtime image of an application requires the location of its modules (path), names of modules to include (module_names), and output directory to store the runtime image (output_dir):

Click here to view code image

jlink –module-path
path
 –add-modules
module_names
 –output
output_dir

The runtime image can be executed by the output_dir/java command.

Selected options for the jlink tool are summarized in Table 19.11.

Table 19.11 Selected Options for the jlink Tool

OptionDescription
–module-path
modulepath…

 -p          
modulepath

Specifies the location where the modules for the application can be found. This can be a root directory for the exploded modules with the class files or a directory where the modular JARs can be found. Multiple directories of modules can be specified, separated by a colon (:) on Unix-based platforms and semicolon (;) on Windows platforms.
–add-modules module,…Specifies the modules to include in the generated runtime image. All application modules must be listed. Any standard or JDK modules needed will be automatically included.
–output pathSpecifies the location of the generated runtime image.

Final Remarks on Options for the JDK Tools

It is worth taking a note of how the command options having the short form -p, -m, and -d are specified in different JDK tools. Table 19.12 gives an overview of which long form they represent in which tool and how they are specified.

Table 19.12 Selected Common Shorthand Options for JDK Tools

javacjavajarjdeps
–module-path
path

 -p           
path

–module-path
path

 -p          
path

–module-path
path

 -p          
path

–module-path path (no short form as -p is reserved for –package)
–module
module

 -m     
module

–module
module
[/
mainclass
]
 -m     
module
[/
mainclass
]

–manifest
TXTFILE

 -m       
TXTFILE

–module
module

 -m
module

(no long form)
-d classesDirectory
–describe-module
module

 -d              
module

–describe-module
 -d


(no -d option)

File Streams – Java I/O: Part I

File Streams

The subclasses FileInputStream and FileOutputStream represent low-level streams that define byte input and output streams that are connected to files. Data can only be read or written as a sequence of bytes. Such file streams are typically used for handling image data.

A FileInputStream for reading bytes can be created using the following constructor:

Click here to view code image

FileInputStream(String name) throws FileNotFoundException

The file designated by the file name is assigned to a new file input stream.

If the file does not exist, a FileNotFoundException is thrown. If it exists, it is set to be read from the beginning. A SecurityException is thrown if the file does not have read access.

A FileOutputStream for writing bytes can be created using the following constructor:

Click here to view code image

FileOutputStream(String name) throws FileNotFoundException
FileOutputStream(String name, boolean append) throws FileNotFoundException

The file designated by the file name is assigned to a new file output stream.

If the file does not exist, it is created. If it exists, its contents are reset, unless the appropriate constructor is used to indicate that output should be appended to the file. A SecurityException is thrown if the file does not have write access or it cannot be created. A FileNotFoundException is thrown if it is not possible to open the file for any other reasons.

The FileInputStream class provides an implementation for the read() methods in its superclass InputStream. Similarly, the FileOutputStream class provides an implementation for the write() methods in its superclass OutputStream.

Example 20.1 demonstrates using a buffer to read bytes from and write bytes to file streams. The input and the output file names are specified on the command line. The streams are created at (1) and (2).

The bytes are read into a buffer by the read() method that returns the number of bytes read. The same number of bytes from the buffer are written to the output file by the write() method, regardless of whether the buffer is full or not after every read operation.

The end of file is reached when the read() method returns the value -1. The code at (3a) using a buffer can be replaced by a call to the transferTo() method at (3b) to do the same operation. The streams are closed by the try-with-resources statement. Note that most of the code consists of a try-with-resources statement with catch clauses to handle the various exceptions.

Example 20.1 Copying a File Using a Byte Buffer

Click here to view code image

/* Copy a file using a byte buffer.
   Command syntax: java CopyFile <from_file> <to_file> */
import java.io.*;
class CopyFile {
  public static void main(String[] args) {
    try (// Assign the files:
        FileInputStream fromFile = new FileInputStream(args[0]);       // (1)
        FileOutputStream toFile = new FileOutputStream(args[1]))  {    // (2)
      // Copy bytes using buffer:                                      // (3a)
      byte[] buffer = new byte[1024];
      int length = 0;
      while((length = fromFile.read(buffer)) != -1) {
        toFile.write(buffer, 0, length);
      }
      // Transfer bytes:
//    fromFile.transferTo(toFile);                                     // (3b)

    } catch(ArrayIndexOutOfBoundsException e) {
      System.err.println(“Usage: java CopyFile <from_file> <to_file>”);
    } catch(FileNotFoundException e) {
      System.err.println(“File could not be copied: ” + e);
    } catch(IOException e) {
      System.err.println(“I/O error.”);
    }
  }
}

Character Streams: Readers and Writers – Java I/O: Part I

20.3 Character Streams: Readers and Writers

A character encoding is a scheme for representing characters. Java programs represent values of the char type internally in the 16-bit Unicode character encoding, but the host platform might use another character encoding to represent and store characters externally. For example, the ASCII (American Standard Code for Information Interchange) character encoding is widely used to represent characters on many platforms. However, it is only one small subset of the Unicode standard.

The abstract classes Reader and Writer are the roots of the inheritance hierarchies for streams that read and write Unicode characters using a specific character encoding (Figure 20.3). A reader is an input character stream that implements the Readable interface and reads a sequence of Unicode characters, and a writer is an output character stream that implements the Writer interface and writes a sequence of Unicode characters. Character encodings (usually called charsets) are used by readers and writers to convert between external bytes and internal Unicode characters. The same character encoding that was used to write the characters must be used to read those characters. The java.nio.charset.Charset class represents charsets. Kindly refer to the Charset class API documentation for more details.

Figure 20.3 Selected Character Streams in the java.io Package

Click here to view code image

static Charset forName(String charsetName)

Returns a charset object for the named charset. Selected common charset names are “UTF-8”, “UTF-16”, “US-ASCII”, and “ISO-8859-1”.

Click here to view code image

static Charset defaultCharset()

Returns the default charset of this Java virtual machine.

Table 20.4 and Table 20.5 give an overview of some selected character streams found in the java.io package.

Table 20.4 Selected Readers

ReaderDescription
BufferedReaderA reader is a high-level input stream that buffers the characters read from an underlying stream. The underlying stream must be specified and an optional buffer size can be given.
InputStreamReaderCharacters are read from a byte input stream which must be specified. The default character encoding is used if no character encoding is explicitly specified in the constructor. This class provides the bridge from byte streams to character streams.
FileReaderCharacters are read from a file, using the default character encoding, unless an encoding is explicitly specified in the constructor. The file can be specified by a String file name. It automatically creates a FileInputStream that is associated with the file.

Table 20.5 Selected Writers

WritersDescription
BufferedWriterA writer is a high-level output stream that buffers the characters before writing them to an underlying stream. The underlying stream must be specified, and an optional buffer size can be specified.
OutputStreamWriterCharacters are written to a byte output stream that must be specified. The default character encoding is used if no explicit character encoding is specified in the constructor. This class provides the bridge from character streams to byte streams.
FileWriterCharacters are written to a file, using the default character encoding, unless an encoding is explicitly specified in the constructor. The file can be specified by a String file name. It automatically creates a FileOutputStream that is associated with the file. A boolean parameter can be specified to indicate whether the file should be overwritten or appended with new content.
PrintWriterA print writer is a high-level output stream that allows text representation of Java objects and Java primitive values to be written to an underlying output stream or writer. The underlying output stream or writer must be specified. An explicit encoding can be specified in the constructor, and also whether the print writer should do automatic line flushing.

Readers use the following methods for reading Unicode characters:

Click here to view code image

int read() throws IOException
int read(char cbuf[]) throws IOException
int read(char cbuf[], int off, int len) throws IOException

Note that the read() methods read the character as an int in the range 0 to 65,535 (0x0000–0xFFFF).

The first method returns the character as an int value. The last two methods store the characters in the specified array and return the number of characters read. The value -1 is returned if the end of the stream has been reached.

Click here to view code image

long skip(long n) throws IOException

A reader can skip over characters using the skip() method.

Click here to view code image

void close() throws IOException

Like byte streams, a character stream should be closed when no longer needed in order to free system resources.

Click here to view code image

boolean ready() throws IOException

When called, this method returns true if the next read operation is guaranteed not to block. Returning false does not guarantee that the next read operation will block.

Click here to view code image

long transferTo(Writer out) throws IOException

Reads all characters from this reader and writes the characters to the specified writer in the order they are read. The I/O streams are not closed after the operation.

Writers use the following methods for writing Unicode characters:

Click here to view code image

void write(int c) throws IOException

The write() method takes an int as an argument, but writes only the least significant 16 bits.

Click here to view code image

void write(char[] cbuf) throws IOException
void write(String str) throws IOException
void write(char[] cbuf, int off, int length) throws IOException
void write(String str, int off, int length) throws IOException

Write the characters from an array of characters or a string.

Click here to view code image

void close() throws IOException
void flush() throws IOException

Like byte streams, a character stream should be closed when no longer needed in order to free system resources. Closing a character output stream automatically flushes the stream. A character output stream can also be manually flushed.

Like byte streams, many methods of the character stream classes throw a checked IOException that a calling method must either catch explicitly or specify in a throws clause. They also implement the AutoCloseable interface, and can thus be declared in a try-with-resources statement (§7.7, p. 407) that will ensure they are automatically closed after use at runtime.

Analogous to Example 20.1 that demonstrates usage of a byte buffer for writing and reading bytes to and from file streams, Example 20.3 demonstrates using a character buffer for writing and reading characters to and from file streams. Later in this section, we will use buffered readers (p. 1251) and buffered writers (p. 1250) for reading and writing characters from files, respectively.

Example 20.3 Copying a File Using a Character Buffer

Click here to view code image

/* Copy a file using a character buffer.
   Command syntax: java CopyCharacterFile <from_file> <to_file> */
import java.io.*;
class CopyCharacterFile {
  public static void main(String[] args) {
    try (// Assign the files:
        FileReader fromFile = new FileReader(args[0]);                 // (1)
        FileWriter toFile = new FileWriter(args[1]))  {                // (2)
      // Copy characters using buffer:                                 // (3a)
      char[] buffer = new char[1024];
      int length = 0;
      while((length = fromFile.read(buffer)) != -1) {
        toFile.write(buffer, 0, length);
      }
      // Transfer characters:
//    fromFile.transferTo(toFile);                                     // (3b)
    } catch(ArrayIndexOutOfBoundsException e) {
      System.err.println(“Usage: java CopyCharacterFile <from_file> <to_file>”);
    } catch(FileNotFoundException e) {
      System.err.println(“File could not be copied: ” + e);
    } catch(IOException e) {
      System.err.println(“I/O error.”);
    }
  }
}

Print Writers – Java I/O: Part I

Print Writers

The capabilities of the OutputStreamWriter and the InputStreamReader classes are limited, as they primarily write and read characters.

In order to write a text representation of Java primitive values and objects, a Print-Writer should be chained to either a writer, or a byte output stream, or accept a String file name, using one of the following constructors:

Click here to view code image

PrintWriter(Writer out)
PrintWriter(Writer out, boolean autoFlush)
PrintWriter(OutputStream out)
PrintWriter(OutputStream out, boolean autoFlush)
PrintWriter(String fileName) throws FileNotFoundException
PrintWriter(String fileName, Charset charset)
            throws FileNotFoundException
PrintWriter(String fileName, String charsetName)
            throws FileNotFoundException, UnsupportedEncodingException

The boolean autoFlush argument specifies whether the PrintWriter should do automatic line flushing.

When the underlying writer is specified, the character encoding supplied by the underlying writer is used. However, an OutputStream has no notion of any character encoding, so the necessary intermediate OutputStreamWriter is automatically created, which will convert characters into bytes, using the default character encoding.

boolean checkError()
protected void clearError()

The first method flushes the output stream if it’s not closed and checks its error state.

The second method clears the error state of this output stream.

Writing Text Representation of Primitive Values and Objects

In addition to overriding the write() methods from its super class Writer, the Print-Writer class provides methods for writing text representation of Java primitive values and of objects (see Table 20.6). The println() methods write the text representation of their argument to the underlying stream, and then append a line separator. The println() methods use the correct platform-dependent line separator. For example, on Unix-based platforms the line separator is ‘\n’ (newline), while on Windows-based platforms it is “\r\n” (carriage return + newline) and on Mac-based platforms it is ‘\r’ (carriage return).

Table 20.6 Print Methods of the PrintWriter Class

The print() methodsThe println() methods
_
print(boolean b)
print(char c)
print(int i)
print(long l)
print(float f)
print(double d)
print(char[] s)
print(String s)
print(Object obj)

println()
println(boolean b)
println(char c)
println(int i)
println(long l)
println(float f)
println(double d)
println(char[] ca)
println(String str)
println(Object obj)

The print methods create a text representation of an object by calling the toString() method on the object. The print methods do not throw any IOException. Instead, the checkError() method of the PrintWriter class must be called to check for errors.

Reading Text Files – Java I/O: Part I

Reading Text Files

When reading characters from a file using the default character encoding, the following two procedures for setting up an InputStreamReader can be used.

Setting up an InputStreamReader which is chained to a FileInputStream (Figure 20.5(a)):

Figure 20.5 Setting Up Readers to Read Characters

Create a FileInputStream:

Click here to view code image

FileInputStream inputFile = new FileInputStream(“info.txt”);

Create an InputStreamReader which is chained to the FileInputStream:

Click here to view code image

InputStreamReader reader = new InputStreamReader(inputFile);

The InputStreamReader uses the default character encoding for reading the characters from the file.

Setting up a FileReader which is a subclass of InputStreamReader (Figure 20.5(b)):

Create a FileReader:

Click here to view code image

FileReader fileReader = new FileReader(“info.txt”);

This is equivalent to having an InputStreamReader chained to a FileInputStream for reading the characters from the file, using the default character encoding.

If a specific character encoding is desired for the reader, the first procedure can be used (Figure 20.5(a)), with the encoding being specified for the InputStreamReader:

Click here to view code image

Charset utf8 = Charset.forName(“UTF-8”);
FileInputStream inputFile = new FileInputStream(“info.txt”);
InputStreamReader reader = new InputStreamReader(inputFile, utf8);

This reader will use the UTF-8 character encoding to read the characters from the file. Alternatively, we can use one of the FileReader constructors that accept a character encoding:

Click here to view code image

Charset utf8 = Charset.forName(“UTF-8”);
FileReader reader = new FileReader(“info.txt”, utf8);

A BufferedReader can also be used to improve the efficiency of reading characters from the underlying stream, as explained later in this section (p. 1251).

Using Buffered Writers

A BufferedWriter can be chained to the underlying writer by using one of the following constructors:

Click here to view code image

BufferedWriter(Writer out)
BufferedWriter(Writer out, int size)

The default buffer size is used, unless the buffer size is explicitly specified.

Characters, strings, and arrays of characters can be written using the methods for a Writer, but these now use buffering to provide efficient writing of characters. In addition, the BufferedWriter class provides the method newLine() for writing the platform-dependent line separator.

The following code creates a PrintWriter whose output is buffered, and the characters are written using the UTF-8 character encoding (Figure 20.6(a)):

Figure 20.6 Buffered Writers

Click here to view code image

Charset utf8 = Charset.forName(“UTF-8”);
FileOutputStream   outputFile      = new FileOutputStream(“info.txt”);
OutputStreamWriter outputStream    = new OutputStreamWriter(outputFile, utf8);
BufferedWriter     bufferedWriter1 = new BufferedWriter(outputStream);
PrintWriter        printWriter1    = new PrintWriter(bufferedWriter1, true);

The following code creates a PrintWriter whose output is buffered, and the characters are written using the default character encoding (Figure 20.6(b)):

Click here to view code image

FileWriter     fileWriter      = new FileWriter(“info.txt”);
BufferedWriter bufferedWriter2 = new BufferedWriter(fileWriter);
PrintWriter    printWriter2    = new PrintWriter(bufferedWriter2, true);

Note that in both cases, the PrintWriter is used to write the characters. The Buffered-Writer is sandwiched between the PrintWriter and the underlying OutputStreamWriter (which is the superclass of the FileWriter class).

Efficient Record Serialization – Java I/O: Part I

Efficient Record Serialization

Example 20.6 shows an example of serializing and deserializing an instance of a record class (§5.14, p. 299). It is worth noting that both processes on records are very efficient as the state components of a record entirely describe the state values to serialize, and as the canonical constructor of a record class is always used to create the complete state of a record, the canonical constructor is also always used during deserialization. By design, selective and customized serialization, discussed later in this section, are not allowed for records.

Example 20.6 Object Serialization

Click here to view code image

import java.io.Serializable;
import java.time.Year;
/** A record class that represents a CD. */
public record CD(String artist, String title, int noOfTracks,
                 Year year, Genre genre) implements Serializable {
  public enum Genre implements Serializable {POP, JAZZ, OTHER}
}

Click here to view code image

//Reading and Writing Objects
import java.io.*;
import java.time.Year;
import java.util.Arrays;
public class ObjectSerializationDemo {
  void writeData() {                                    // (1)
    try (// Set up the output stream:
        FileOutputStream outputFile = new FileOutputStream(“obj-storage.dat”);
        ObjectOutputStream outputStream = new ObjectOutputStream(outputFile)) {
      // Write data:
      String[] strArray = {“Seven”, “Eight”, “Six”};
      long num = 2014;
      int[] intArray = {1, 3, 1949};
      String commonStr = strArray[2];                  // “Six”
      CD oneCD = new CD(“Jaav”, “Java Jive”, 8, Year.of(2017), CD.Genre.POP);
      outputStream.writeObject(strArray);
      outputStream.writeLong(num);
      outputStream.writeObject(intArray);
      outputStream.writeObject(commonStr);
      outputStream.writeObject(oneCD);
    } catch (FileNotFoundException e) {
      System.err.println(“File not found: ” + e);
    } catch (IOException e) {
      System.err.println(“Write error: ” + e);
    }
  }
  void readData() {                                     // (2)
    try (// Set up the input stream:
        FileInputStream inputFile = new FileInputStream(“obj-storage.dat”);
        ObjectInputStream inputStream = new ObjectInputStream(inputFile)) {
      // Read the data:
      String[] strArray = (String[]) inputStream.readObject();
      long num = inputStream.readLong();
      int[] intArray = (int[]) inputStream.readObject();
      String commonStr = (String) inputStream.readObject();
      CD oneCD = (CD) inputStream.readObject();
      // Write data to the standard output stream:
      System.out.println(Arrays.toString(strArray));
      System.out.println(num);
      System.out.println(Arrays.toString(intArray));
      System.out.println(commonStr);
      System.out.println(oneCD);
    } catch (FileNotFoundException e) {
      System.err.println(“File not found: ” + e);
    } catch (EOFException e) {
      System.err.println(“End of stream: ” + e);
    } catch (IOException e) {
      System.err.println(“Read error: ” + e);
    } catch (ClassNotFoundException e) {
      System.err.println(“Class not found: ” + e);
    }
  }
  public static void main(String[] args) {
    ObjectSerializationDemo demo = new ObjectSerializationDemo();
    demo.writeData();
    demo.readData();
  }
}

Output from the program:

Click here to view code image [Seven, Eight, Six]
2014
[1, 3, 1949]
Six
CD[artist=Jaav, title=Java Jive, noOfTracks=8, year=2017, genre=POP]

Listing All Observable Modules – Java Module System

Listing All Observable Modules

The java command with the –list-modules option (no short form) lists the system modules that are installed in the JDK, and then exits. These modules are available to every application. This lengthy list gives an idea of how the JDK has been modularized. The java command lists each module with its version; in this case, it is Java 17.0.2. Module names starting with the prefix java implement the Java SE Language Specification, whereas those starting with jdk are JDK specific. The reader will no doubt recognize some names, specially java.base.

The system modules are found in the jmods directory under the installation directory of the JDK. These are JMOD files having a module name with the extension “.jmod”. JMOD files have a special non-executable format that allows native binary libraries and other configuration files to be packaged with bytecode artifacts that can then be linked to create runtime images with the jlink tool (p. 1222).

>java –list-modules
[email protected]

[email protected]

[email protected]

[email protected]

Specifying a module path in the java command below not only lists the system modules, but also the modular JARs found in the specified module path—in other words, all observable modules. In the java command below, the absolute path of all JARs found in the mlib directory is listed last in the output.

Click here to view code image

>java –module-path mlib –list-modules
[email protected]

[email protected]

[email protected]

controller file:…/adviceApp/mlib/controller.jar
main file:…/adviceApp/mlib/main.jar
model file:…/adviceApp/mlib/model.jar
view file:…/adviceApp/mlib/view.jar

Describing the Module Descriptor of a JAR

Both the java tool and the jar tool have the –describe-module option (short form: -d) to show the information contained in the module descriptor of a JAR. Both commands are used respectively below.

Click here to view code image

>java –module-path mlib –describe-module main
main file:…/adviceApp/mlib/main.jar
requires java.base mandated
requires controller
contains com.passion.main
>jar –file mlib/main.jar –describe-module

main jar:file:…/adviceApp/mlib/main.jar/!module-info.class
requires controller
requires java.base mandated
contains com.passion.main
main-class com.passion.main.Main

Note that in the java command, the –describe-module option requires a module name, whereas that is not the case in the jar command, where the –file option specifies the modular JAR.

Since the module descriptor is a Java bytecode class file, it must be disassembled to display its information. In both cases, first the module name is printed, followed by the path of the JAR. In the case of the jar command, the name module-info.class is appended to the path of the JAR. In both cases, the names of modules required (java.base and controller) are reported. The main module also contains an internal package (com.passion.main). Not surprisingly, the java.base module is mandated. However, only the jar command reports that the main-class is com.passion.main.Main. The jar command is useful to find the entry point of an application.

The –describe-module option (short form: -d) can also be used on a system module. The following java command describes the module descriptor of the java.base system module. The command lists all modules the java.base module exports, uses, exports qualified, and contains (p. 1177). As can be expected from its status as a mandated module for all other modules, it does not require any module.

Click here to view code image

>java –describe-module java.base
[email protected]
exports java.io
exports java.lang

uses java.util.spi.CurrencyNameProvider
uses java.util.spi.TimeZoneNameProvider

qualified exports jdk.internal to jdk.jfr
qualified exports sun.net.www to java.desktop java.net.http jdk.jartool

contains com.sun.crypto.provider
contains com.sun.java.util.jar.pack


Serialization and Versioning – Java I/O: Part I

Serialization and Versioning

Class versioning comes into play when we serialize an object with one definition of a class, but deserialize the streamed object with a different class definition. By streamed object, we mean the serialized representation of an object. Between serialization and deserialization of an object, the class definition can change.

Note that at serialization and at deserialization, the definition of the class (i.e., its bytecode file) should be accessible. In the examples so far, the class definition has been the same at both serialization and deserialization. Example 20.10 illustrates the problem of class definition mismatch at deserialization and the solution provided by Java.

Example 20.10 makes use of the following classes (numbering refers to code lines in the example):

(1) The original version of the serializable class Item. It has one field named price. An object of this class will be serialized and read using different versions of this class.

(2) A newer version of the class Item that has been augmented with a field for the weight of an item. This class will only be used for deserialization of objects that have been serialized with the original version of the class.

(3) The class Serializer serializes an object of the original version of the class Item.

(4) The class DeSerializer deserializes a streamed object of the class Item. In the example, deserialization is based on a different version of the Item class than the one used at serialization.

There are no surprises if we use the original class Item to serialize and deserialize an object of the Item class.

Click here to view code image

// Original version of the Item class.
public class Item implements Serializable {                            // (1)
  private double price;
//…
}

Result:

Before writing: Price: 100.00
After reading: Price: 100.00

If we deserialize a streamed object of the original class Item at (1) based on the byte-code file of the augmented version of the Item class at (2), an InvalidClassException is thrown at runtime.

Click here to view code image

// New version of the Item class.
public class Item implements Serializable {                            // (2)
  private double price;
  private double weight;                                  // Additional field
//…
}

Result (edited to fit on the page):

Click here to view code image

Exception in thread “main” java.io.InvalidClassException: Item;
local class incompatible:
  stream classdesc serialVersionUID = -4194294879924868414,
  local class serialVersionUID = -1186964199368835959
     …
     at DeSerializer.main(DeSerializer.java:14)

The question is, how was the class definition mismatch discovered at runtime? The answer lies in the stack trace of the exception thrown. The local class was incompatible, meaning the class we are using to deserialize is not compatible with the class that was used when the object was serialized. In addition, two long numbers are printed, representing the serialVersionUID of the respective class definitions. The first serialVersionUID is generated by the serialization process based on the class definition and becomes part of the streamed object. The second serialVersionUID is generated based on the local class definition that is accessible at deserialization. The two are not equal, and deserialization fails.

A serializable class can provide its serialVersionUID by declaring it in its class declaration, exactly as shown below, except for the initial value which of course can be different:

Click here to view code image

static final long serialVersionUID = 100L;            // Appropriate value.

As we saw in the example above, if a serializable class does not provide a serialVersionUID, one is implicitly generated. By providing an explicit serialVersionUID, it is possible to control what happens at deserialization. As newer versions of the class are created, the serialVersionUID can be kept the same until it is deemed that older streamed objects are no longer compatible for deserialization. After the change to the serialVersionUID, it will not be possible to deserialize older streamed objects of the class based on newer versions of the class. Although static members of a class are not serialized, the only exception is the value of the static final long serialVersionUID field.

In the scenario below, the original version and the newer version of the class Item both declare a serialVersionUID at (1a) and at (2a), respectively, that has the same value. An Item object is serialized using the original version, but deserialized based on the newer version. We see that serialization succeeds, and the weight field is initialized to the default value 0.0. In other words, the object created is of the newer version of the class.

Click here to view code image

// Original version of the Item class.
public class Item implements Serializable {    // (1)
  static final long serialVersionUID = 1000L;  // (1a)
  private double price;
//…
}

// New version of the Item class.
public class Item implements Serializable {    // (2)
  static final long serialVersionUID = 1000L;  // (2a) Same serialVersionUID
  private double price;
  private double weight;                       // Additional field
//…
}

Result:

Click here to view code image

Before writing: Price: 100.00
After reading: Price: 100.00, Weight: 0.00

However, if we now deserialize the streamed object of the original class having 1000L as the serialVersionUID, based on the newer version of the class having the serialVersionUID equal to 1001L, deserialization fails as we would expect because the serialVersionUIDs are different.

Click here to view code image

// New version of the Item class.
public class Item implements Serializable {    // (2)
  static final long serialVersionUID = 1001L;  // (2b) Different serialVersionUID
  private double price;
  private double weight;
//…
}

Result (edited to fit on the page):

Click here to view code image

Exception in thread “main” java.io.InvalidClassException: Item;
local class incompatible:
  stream classdesc serialVersionUID = 1000,
  local class serialVersionUID = 1001
     …
     at DeSerializer.main(DeSerializer.java:14)

Best practices advocate that serializable classes should use the serialVersionUID solution for better control of what happens at deserialization as classes evolve.

Example 20.10 Class Versioning

Click here to view code image

import java.io.Serializable;
// Original version of the Item class.
public class Item implements Serializable {                            // (1)
//static final long serialVersionUID = 1000L;                          // (1a)
  private double price;
  public Item(double price) {
    this.price = price;
  }
  @Override
  public String toString() {
    return String.format(“Price: %.2f%n”, this.price);
  }
}

Click here to view code image

import java.io.Serializable;
// New version of the Item class.
public class Item implements Serializable {                            // (2)
//static final long serialVersionUID = 1000L;                          // (2a)
//static final long serialVersionUID = 1001L;                          // (2b)
  private double price;
  private double weight;
  public Item(double price, double weight) {
    this.price = price;
    this.weight = weight;
  }
  @Override
  public String toString() {
    return String.format(“Price: %.2f, Weight: %.2f”, this.price, this.weight);
  }
}

Click here to view code image

// Serializer for objects of class Item.
import java.io.*;
public class Serializer {                                                // (3)
  public static void main(String args[])
      throws IOException, ClassNotFoundException {
    try (// Set up the output stream:
        FileOutputStream outputFile = new FileOutputStream(“item_storage.dat”);
        ObjectOutputStream outputStream = new ObjectOutputStream(outputFile)) {
      // Serialize an object of the original class:
      Item item = new Item(100.00);
      System.out.println(“Before writing: ” + item);
      outputStream.writeObject(item);
    }
  }
}

Click here to view code image

// Deserializer for objects of class Item.
import java.io.*;
public class DeSerializer{                                               // (4)
  public static void main(String args[])
      throws IOException, ClassNotFoundException {
    try (// Set up the input streams:
        FileInputStream inputFile = new FileInputStream(“item_storage.dat”);
        ObjectInputStream inputStream = new ObjectInputStream(inputFile)) {
      // Read a serialized object of the Item class.
      Item item = (Item) inputStream.readObject();
      // Write data on standard output stream.
      System.out.println(“After reading: ” + item);
    }
  }
}