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


Viewing Module Dependencies – Java Module System

Viewing Module Dependencies

If the default output from the jdeps command is overwhelming, it can be filtered. The -summary option (short form: -s) will only print the module dependencies, as shown by the jdeps command below. Only the module dependencies of the main module will be shown in the output.

Click here to view code image

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

main -> controller
main -> java.base

If we use the –recursive option (short form: -R) with the -summary option (short form: -s), then the module dependencies of each module will be printed recursively, starting with the specified module.

Click here to view code image

>
jdeps –module-path mlib –module main -summary –recursive              (1)

controller -> java.base
controller -> model
controller -> view
main -> controller
main -> java.base
model -> java.base
view -> java.base
view -> model

Finally, we illustrate the graph-generating capabilities of the jdeps tool. The jdeps command takes all JARs of the adviceApp application from the mlib directory and creates a module graph (options -summary and –recursive) in the DOT format (option -dotoutput) under the current directory. The DOT file summary.dot containing the graph will be created. Using the dot command, this graph can be converted to a pdf file (summary.pdf) as shown in Figure 19.18.

Figure 19.18 Module Graph Using the jdeps Tool

Click here to view code image

>
jdeps -dotoutput . -summary –recursive  mlib/*
>
dot -Tpdf summary.dot >summary.pdf

The module graph in Figure 19.18 shows the same module dependencies printed by the jdeps command above at (1). Comparing the module graph in Figure 19.18 with the one in Figure 19.8, we see that jdeps has added the implicit dependency of each module on the java.base module.

Viewing Class-Level Dependencies

It is possible to dive deeper into dependencies with the jdeps tool. The -verbose option (short form: -v) will elicit the class dependencies of the specified module. Instead of package dependencies to round off the output, class dependencies are listed. The last line in the output shows that the class com.passion.main.Main in the main module depends on the java.lang.String class in the java.base module.

Click here to view code image

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

If we use the –recursive option (short form: -R) with the -verbose option (short form: -v), then the class dependencies of each module will be printed recursively, starting with the specified module.

Click here to view code image

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

controller

main

model

view


Selected Options for the jar Tool – Java Module System

Selected Options for the jar Tool

The jar tool is an archiving and compression tool that can be used to bundle Java artifacts and any other resources that comprise the application. The archive file names have the .jar extension. A typical command to create a modular JAR (jarfile) with an application entry point (qualifiedMainClassName), based on the contents of a specific directory (DIR), is shown below. Note the obligatory dot (.) at the end of the command.

Click here to view code image

jar –create –file
jarfile
 –main-class
qualifiedMainClassName
 -C
DIR
 .

Table 19.9 gives an overview of some selected options that can be used for working with JARs.

Table 19.9 Selected Options for the jar Tool

OptionDescription
–create or -cCreates a new archive.
–extract or -xExtracts specified or all files in the archive.
–list or -tLists the contents of the archive.
–update or -uUpdates an existing archive with specified files.
–describe-module or -dPrints the module descriptor of the archive and the main-class, if one is specified in the manifest.
–verbose or -vPrints extra information about the operation.
–file
jarfile

–file=
jarfile

 -f
jarfile

 -f=
jarfile

Specifies the name of the archive.
-C DIR filesChanges to the specified directory and includes the contents of the specified files from this directory. If files is a dot (.), the contents under the specified directory DIR are included.
Click here to view code image –main-class
qualifiedMainClassName

–main-class=
qualifiedMainClassName

 -e
qualifiedMainClassName

 -e=
qualifiedMainClassName

Specifies the entry point of the application.
–manifest
TXTFILE

–manifest=
TXTFILE

 -m
TXTFILE

 -m=
TXTFILE

Reads the manifest information for the archive from the specified TXTFILE and incorporates it in the archive—for example, the value of the Main-Class attribute that specifies the entry point of the application.
–module-path
modulepath

 -p          
modulepath

Specifies the location of the modules for recording hashes.

Selected Options for the jdeps Tool

The Java Class Dependency Analyzer, jdeps, is the tool of choice when working with modules, as it is module savvy and highly versatile. Among its extensive module analyzing capabilities, it can be used to explore dependencies at different levels: module level, package level, and class level.

Table 19.10 gives an overview of some selected options for the jdeps tool that can be used for analyzing modules.

Table 19.10 Selected Options for the jdeps Tool

OptionDescription
–module-path modulepathSpecifies where to find the module JARs needed by the application. No short form, as -p is already reserved for –package.
–module-name
moduleName

 -m          
moduleName

Specifies the root module for module dependency analysis.
-summary or -sPresents only a summary of the module dependencies.
–recursive or -RForces jdeps to recursively iterate over the module dependencies. When used alone, also prints the package-level dependencies.
-verbose or -vAlso includes all class-level dependencies in the printout.
-verbose:packageIncludes package-level dependencies in the printout, excluding, by default, dependencies within the same package.
-verbose:classIncludes class-level dependencies in the printout, excluding, by default, dependencies within the same JAR.

I/O Filter Streams – Java I/O: Part I

I/O Filter Streams

An I/O filter stream is a high-level I/O stream that provides additional functionality to an underlying stream to which it is chained. The data from the underlying stream is manipulated in some way by the filter stream. The FilterInputStream and FilterOutputStream classes, together with their subclasses, define input and output filter streams. The subclasses BufferedInputStream and BufferedOutputStream implement filter streams that buffer input from and output to the underlying stream, respectively. The subclasses DataInputStream and DataOutputStream implement filter streams that allow binary representation of Java primitive values to be read and written, respectively, from and to an underlying stream.

Reading and Writing Binary Values

The java.io package contains the two interfaces DataInput and DataOutput, which streams can implement to allow reading and writing of binary representation of Java primitive values (boolean, char, byte, short, int, long, float, double). The methods for writing binary representations of Java primitive values are named writeX, where X is any Java primitive data type. The methods for reading binary representations of Java primitive values are similarly named readX. Table 20.3 gives an overview of the readX() and writeX() methods found in these two interfaces. A file containing binary values (i.e., binary representation of Java primitive values) is usually called a binary file.

Table 20.3 The DataInput and DataOutput Interfaces

TypeMethods in the DataInput interfaceMethods in the DataOutput interface
booleanreadBoolean()writeBoolean(boolean b)
charreadChar()writeChar(int c)
bytereadByte()writeByte(int b)
shortreadShort()writeShort(int s)
intreadInt()writeInt(int i)
longreadLong()writeLong(long l)
floatreadFloat()writeFloat(float f)
doublereadDouble()writeDouble(double d)
StringreadLine()writeChars(String str)
StringreadUTF()writeUTF(String str)

Note the methods provided for reading and writing strings. However, the recommended practice for reading and writing characters is to use character streams, called readers and writers, which are discussed in §20.3.

The filter streams DataOutputStream and DataInputStream implement the DataOutput and DataInput interfaces, respectively, and can be used to read and write binary representation of Java primitive values from and to an underlying stream. Both the writeX() and readX() methods throw an IOException in the event of an I/O error. In particular, the readX() methods throw an EOFException (a subclass of IOException) if the input stream does not contain the correct number of bytes to read. Bytes can also be skipped from a DataInput stream, using the skipBytes(int n) method which skips n bytes.

Click here to view code image

DataInputStream(InputStream in)
DataOutputStream(OutputStream out)

These constructors can be used to set up filter streams from an underlying stream for reading and writing Java primitive values, respectively.

Writing Binary Values to a File – Java I/O: Part I

Writing Binary Values to a File

To write the binary representation of Java primitive values to a binary file, the following procedure can be used, which is also depicted in Figure 20.2.

Figure 20.2 Stream Chaining for Reading and Writing Binary Values to a File

Use a try-with-resources statement for declaring and creating the necessary streams, which guarantees closing of the filter stream and any underlying stream.

Create a FileOutputStream:

Click here to view code image

FileOutputStream outputFile = new FileOutputStream(“primitives.data”);

Create a DataOutputStream which is chained to the FileOutputStream:

Click here to view code image

DataOutputStream outputStream = new DataOutputStream(outputFile);

Write Java primitive values using relevant writeX() methods:

Note that in the case of char, byte, and short data types, the int argument to the writeX() method is converted to the corresponding type, before it is written (see Table 20.3).

See also the numbered lines in Example 20.2 corresponding to the steps above.

Reading Binary Values from a File

To read the binary representation of Java primitive values from a binary file, the following procedure can be used, which is also depicted in Figure 20.2.

Use a try-with-resources statement for declaring and creating the necessary streams, which guarantees closing of the filter stream and any underlying stream.

Create a FileInputStream:

Click here to view code image

FileInputStream inputFile = new FileInputStream(“primitives.data”);

Create a DataInputStream which is chained to the FileInputStream:

Click here to view code image

DataInputStream inputStream = new DataInputStream(inputFile);

Read the (exact number of) Java primitive values in the same order they were written out to the file, using relevant readX() methods. Not doing so will unleash the wrath of the IOException.

See also the numbered lines in Example 20.2 corresponding to the steps above. Example 20.2 uses both procedures described above: first to write and then to read some Java primitive values to and from a file. It also checks to see if the end of the stream has been reached, signaled by an EOFException. The values are also written to the standard output stream.

Example 20.2 Reading and Writing Binary Values

Click here to view code image

import java.io.*;
public class BinaryValuesIO {
  public static void main(String[] args) throws IOException {
    // Write binary values to a file:
    try(                                                                   // (1)
        // Create a FileOutputStream.                                         (2)
        FileOutputStream outputFile = new FileOutputStream(“primitives.data”);
        // Create a DataOutputStream which is chained to the FileOutputStream.(3)
        DataOutputStream outputStream = new DataOutputStream(outputFile)) {
      // Write Java primitive values in binary representation:                (4)
      outputStream.writeBoolean(true);
      outputStream.writeChar(‘A’);               // int written as Unicode char
      outputStream.writeByte(Byte.MAX_VALUE);    // int written as 8-bits byte
      outputStream.writeShort(Short.MIN_VALUE);  // int written as 16-bits short
      outputStream.writeInt(Integer.MAX_VALUE);
      outputStream.writeLong(Long.MIN_VALUE);
      outputStream.writeFloat(Float.MAX_VALUE);
      outputStream.writeDouble(Math.PI);
    }
    // Read binary values from a file:
    try (                                                                  // (1)
        // Create a FileInputStream.                                          (2)
        FileInputStream inputFile = new FileInputStream(“primitives.data”);
        // Create a DataInputStream which is chained to the FileInputStream.  (3)
        DataInputStream inputStream = new DataInputStream(inputFile)) {
      // Read the binary representation of Java primitive values
      // in the same order they were written out:                             (4)
      System.out.println(inputStream.readBoolean());
      System.out.println(inputStream.readChar());
      System.out.println(inputStream.readByte());
      System.out.println(inputStream.readShort());
      System.out.println(inputStream.readInt());
      System.out.println(inputStream.readLong());
      System.out.println(inputStream.readFloat());
      System.out.println(inputStream.readDouble());
      // Check for end of stream:
      int value = inputStream.readByte();
      System.out.println(“More input: ” + value);
    } catch (FileNotFoundException fnf) {
      System.out.println(“File not found.”);
    } catch (EOFException eof) {
      System.out.println(“End of input stream.”);
    }
  }
}

Output from the program:

true
A
127
-32768
2147483647
-9223372036854775808
3.4028235E38
3.141592653589793
End of input stream.

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

The ObjectInputStream Class – Java I/O: Part I

The ObjectInputStream Class

An ObjectInputStream is used to restore (deserialize) objects that have previously been serialized using an ObjectOutputStream. An ObjectInputStream must be chained to an InputStream, using the following constructor:

Click here to view code image

ObjectInputStream(InputStream in) throws IOException

For example, in order to restore objects from a file, an ObjectInputStream can be chained to a FileInputStream:

Click here to view code image

FileInputStream inputFile = new FileInputStream(“obj-storage.dat”);
ObjectInputStream inputStream = new ObjectInputStream(inputFile);

The readObject() method of the ObjectInputStream class is used to read the serialized state of an object from the stream:

Click here to view code image

final Object readObject() throws ClassNotFoundException, IOException

Note that the reference type of the returned object is Object, regardless of the actual type of the retrieved object, and can be cast to the desired type. Objects and values must be read in the same order as when they were serialized.

Serializable, non-transient data members of an object, including those data members that are inherited, are restored to the values they had at the time of serialization. For compound objects containing references to other objects, the constituent objects are read to re-create the whole object structure. In order to deserialize objects, the appropriate classes must be available at runtime. Note that new objects are created during deserialization, so that no existing objects are overwritten.

The class ObjectSerializationDemo in Example 20.6 serializes some objects in the writeData() method at (1), and then deserializes them in the readData() method at (2). The readData() method also writes the data to the standard output stream.

The writeData() method at (1) writes the following values to the output stream: an array of strings (strArray), a long value (num), an array of int values (intArray), a String object (commonStr) which is shared with the array strArray of strings, and an instance (oneCD) of the record class CD whose component fields are all serializable.

Duplication is automatically avoided when the same object is serialized several times. The shared String object (commonStr) is actually only serialized once. Note that the array elements and the characters in a String object are not written out explicitly one by one. It is enough to pass the object reference in the writeObject() method call. The method also recursively goes through the array of strings, strArray, serializing each String object in the array. The current state of the oneCD instance is also serialized.

The method readData() at (2) deserializes the data in the order in which it was written. An explicit cast is needed to convert the reference of a deserialized object to a subtype. Applying the right cast is of course the responsibility of the application. Note that new objects are created by the readObject() method, and that an object created during the deserialization process has the same state as the object that was serialized.

Customizing Object Serialization – Java I/O: Part I

Customizing Object Serialization

As we have seen, the class of the object must implement the Serializable interface if we want the object to be serialized. If this object is a compound object, then all its constituent objects must also be serializable, and so on.

It is not always possible for a client to declare that a class is Serializable. It might be declared final, and therefore not extendable. The client might not have access to the code, or extending this class with a serializable subclass might not be an option. Java provides a customizable solution for serializing objects in such cases.

Customized serialization discussed here is not applicable to record classes.

The basic idea behind the scheme is to use default serialization as much as possible, and to provide hooks in the code for the serialization mechanism to call specific methods to deal with objects or values that should not or cannot be serialized by the default methods of the object streams.

Customizing serialization is illustrated in Example 20.8, using the Wheel and Unicycle classes from Example 20.7. The serializable class Unicycle would like to use the Wheel class, but this class is not serializable. If the wheel field in the Unicycle class is declared to be transient, it will be ignored by the default serialization procedure. This is not a viable option, as the unicycle will be missing the wheel size when a serialized unicycle is deserialized, as was illustrated in Example 20.7.

Any serializable object has the option of customizing its own serialization if it implements the following pair of methods:

Click here to view code image

  private void writeObject(ObjectOutputStream) throws IOException;
  private void readObject(ObjectInputStream)
                       throws IOException, ClassNotFoundException;

These methods are not part of any interface. Although private, these methods can be called by the JVM. The first method above is called on the object when its serialization starts. The serialization procedure uses the reference value of the object to be serialized that is passed in the call to the ObjectOutputStream.writeObject() method, which in turn calls the first method above on this object. The second method above is called on the object created when the deserialization procedure is initiated by the call to the ObjectInputStream.readObject() method.

Customizing serialization for objects of the class Unicycle in Example 20.8 is achieved by the private methods at (3c) and (3d). Note that the field wheel is declared transient at (3b) and excluded by the normal serialization process.

In the private method writeObject() at (3c) in Example 20.8, the pertinent lines of code are the following:

Click here to view code image

oos.defaultWriteObject();               // Method in the ObjectOutputStream class
oos.writeInt(wheel.getWheelSize());     // Method in the ObjectOutputStream class

The call to the defaultWriteObject() method of the ObjectOutputStream does what its name implies: normal serialization of the current object. The second line of code does the customization: It writes the binary int value of the wheel size to the ObjectOutputStream. The code for customization can be called both before and after the call to the defaultWriteObject() method, as long as the same order is used during deserialization.

In the private method readObject() at (3d), the pertinent lines of code are the following:

Click here to view code image

ois.defaultReadObject();                // Method in the ObjectInputStream class
int wheelSize = ois.readInt();          // Method in the ObjectInputStream class
this.wheel = new Wheel(wheelSize);

The call to the defaultReadObject() method of the ObjectInputStream does what its name implies: normal deserialization of the current object. The second line of code reads the binary int value of the wheel size from the ObjectInputStream. The third line of code creates a Wheel object, passes this value in the constructor call, and assigns its reference value to the wheel field of the current object. Again, code for customization can be called both before and after the call to the defaultReadObject() method, as long as it is in correspondence with the customization code in the writeObject() method.

The client class SerialClient in Example 20.8 is the same as the one in Example 20.7. The output from the program confirms that the object state prior to serialization is identical to the object state after deserialization.

Example 20.8 Customized Serialization

Click here to view code image

public class Wheel {                                               // (1b)
  private int wheelSize;
  public Wheel(int ws) { wheelSize = ws; }
  public int getWheelSize() { return wheelSize; }
  @Override
  public String toString() { return “wheel size: ” + wheelSize; }
}

Click here to view code image
import java.io.*;
public class Unicycle implements Serializable {                    // (2)
  transient private Wheel wheel;                                   // (3b)
  public Unicycle(Wheel wheel) { this.wheel = wheel; }
  @Override
  public String toString() { return “Unicycle with ” + wheel; }
  private void writeObject(ObjectOutputStream oos) {               // (3c)
    try {
      oos.defaultWriteObject();
      oos.writeInt(wheel.getWheelSize());
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
  private void readObject(ObjectInputStream ois) {                 // (3d)
    try {
      ois.defaultReadObject();
      int wheelSize = ois.readInt();
      this.wheel = new Wheel(wheelSize);
    } catch (IOException e) {
      e.printStackTrace();
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    }
  }
}

Click here to view code image

public class SerialClient { // Same as in
Example 20.7
 }

Output from the program:

Click here to view code image

Before writing: Unicycle with wheel size: 65
After reading: Unicycle with wheel size: 65

Superclass Is Not Serializable – Java I/O: Part I

Superclass Is Not Serializable

However, the result of deserialization is not the same when the superclass Person is not serializable, but the subclass Student is. We get the following output from the program in Example 20.9 when it is run with (1b) and (3b) in the Person class and the Student class, respectively—that is, when only the subclass is serializable, but not the superclass. The output shows that the object state prior to serialization is not identical to the object state after deserialization.

Click here to view code image

Before writing: Student state(Pendu, 1007)
No-argument constructor executed.
After reading: Student state(null, 1007)

During deserialization, the zero-argument constructor of the Person superclass at (2) is run. As we can see from the declaration of the Person class in Example 20.9, this zero-argument constructor does not initialize the name field, which remains initialized with the default value for reference types (null).

If the superclass Person does not provide the no-argument constructor or the default constructor, as in the declaration below, the call to the readObject() method to perform deserialization throws an InvalidClassException.

Click here to view code image

public class Person  {                                         // (1b)
  private String name;
  public Person(String name) { this.name = name; }
  public String getName() { return name; }
}

Output from the program (edited to fit on the page):

Click here to view code image

Before writing: Student state(Pendu, 1007)
Exception in thread “main” java.io.InvalidClassException:
        Student; no valid constructor
        …
        at SerialInheritance.main(SerialInheritance.java:28)

The upshot of serializing objects of subclasses is that the superclass should be serializable, unless there are compelling reasons for why it is not. And if the superclass is not serializable, it should at least provide either the default constructor or the no-argument constructor to avoid an exception during deserialization.

Although a superclass might be serializable, its subclasses can prevent their objects from being serialized by implementing the private method writeObject (ObjectOutputStream) that throws a java.io.NotSerializableException.

Example 20.9 Serialization and Inheritance

Click here to view code image

import java.io.Serializable;
// A superclass
public class Person implements Serializable {                  // (1a)
//public class Person  {                                       // (1b)
  private String name;
  public Person() {                                            // (2)
    System.out.println(“No-argument constructor executed.”);
  }
  public Person(String name) { this.name = name; }
  public String getName() { return name; }
}

Click here to view code image

import java.io.Serializable;
public class Student extends Person {                            // (3a)
//public class Student extends Person implements Serializable {  // (3b)
  private long studNum;
  public Student(String name, long studNum) {
    super(name);
    this.studNum = studNum;
  }
  @Override
  public String toString() {
    return “Student state(” + getName() + “, ” + studNum + “)”;
  }
}

Click here to view code image

import java.io.*;
public class SerialInheritance {
  public static void main(String[] args)
      throws IOException, ClassNotFoundException {
    // Serialization:
    try (// Set up the output stream:                                        (4)
        FileOutputStream outputFile = new FileOutputStream(“storage.dat”);
        ObjectOutputStream outputStream = new ObjectOutputStream(outputFile)) {
      // Write data:
      Student student = new Student(“Pendu”, 1007);
      System.out.println(“Before writing: ” + student);
      outputStream.writeObject(student);
    }
    // Deserialization:
    try (// Set up the input stream:                                          (5)
        FileInputStream inputFile = new FileInputStream(“storage.dat”);
        ObjectInputStream inputStream = new ObjectInputStream(inputFile)) {
      // Read data.
      Student student = (Student) inputStream.readObject();
      // Write data on standard output stream.
      System.out.println(“After reading: ” + student);
    }
  }
}