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

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