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


Symbolic Links – Java I/O: Part II

Symbolic Links

Apart from regular files, a file system may allow symbolic links to be created. A symbolic link (also called a hard link, a shortcut, or an alias) is a special file that acts as a reference to a directory entry, called the target of the symbolic link. Creating an alias of a directory entry in the file system is similar to creating a symbolic link. Using symbolic links is transparent to the user, as the file system takes care of using the target when a symbolic link is used in a file operation, with one caveat: Deleting the symbolic link does not delete the target. In many file operations, it is possible to indicate whether symbolic links should be followed or not.

21.2 Creating Path Objects

File operations that a Java program invokes are performed on a file system. Unless the program utilizes a specific file system constructed by the factory methods of the java.nio.file.FileSystems utility class, file operations are performed on a file system called the default file system that is accessible to the JVM. We can think of the default file system as the local file system. The default file system is platform specific and can be obtained as an instance of the java.nio.file.FileSystem abstract class by invoking the getDefault() factory method of the FileSystems utility class.

Click here to view code image

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

File operations can access the default file system directly to perform their operations. The default file system can be queried for various file system properties.

Click here to view code image

static FileSystem getDefault()        
Declared in
 java.nio.file.FileSystems

Returns the platform-specific default file system. If the method is called several times, it returns the same FileSystem instance for the default file system.

Click here to view code image

abstract String getSeparator()        
Declared in
 java.nio.file.FileSystem

Returns the platform-specific name separator for name elements in a path, represented as a string.

Paths in the file system are programmatically represented by objects that implement the Path interface. The Path interface and various other classes provide factory methods that create Path objects (see below). A Path object is also platform specific.

A Path object implements the Iterable<Path> interface, meaning it is possible to traverse over its name elements from the first name element to the last. It is also immutable and therefore thread-safe.

In this section, we look at how to create Path objects. A Path object can be queried and manipulated in various ways, and may not represent an existing directory entry in the file system (p. 1294). A Path object can be used in file operations to access and manipulate the directory entry it denotes in the file system (p. 1304).

The methods below can be used to create Path objects. The methods are also shown in Figure 21.1, together with the relationship between the different classes of these methods. Note in particular the interoperability between the java.nio.file.Path interface that represents a path and the two classes java.io.File and java.net.URI. The class java.io.File represents a pathname in the standard I/O API and the class java.net.URI represents a Uniform Resource Identifier (URI) that identifies a resource (e.g., a file or a website).

Click here to view code image

static Path of(String first, String… more)
static Path of(URI uri)

These methods are declared in the java.nio.file.Path interface.

Click here to view code image

abstract Path getPath(String first, String… more)

This method is declared in the java.nio.file.FileSystem class.

Click here to view code image

static Path get(String first, String… more)
static Path get(URI uri)

These static methods are declared in the java.nio.file.Paths class.

Path toPath()

This method is declared in the java.io.File class.

Figure 21.1 Creating Path Objects

Summary of Selected Operations with the JDK Tools – Java Module System

19.14 Summary of Selected Operations with the JDK Tools

Table 19.6 provides a ready reference for commands to perform miscellaneous operations using the JDK tools introduced in this chapter. Short forms for the options are shown where appropriate. Use the indicated reference for each operation to learn more about it. The only way to become familiar with these tools is to use them.

Table 19.6 Selected Operations with the JDK Tools

OperationCommand
Compiling modular code (p. 1186)Click here to view code image javac –module-path
modulepath
 -d
directory sourceAndModuleInfoFiles

javac  -p          
modulepath
 -d
directory sourceAndModuleInfoFiles

javac –module-source-path
modulepath
 -d
directory \

      –module
moduleName1
,…
javac –module-source-path
modulepath
 -d
directory
 \
       -m
moduleName1
,…

Launching modular application (p. 1189)Click here to view code image java –module-path
modulepath
 –module
moduleName/qualifiedClassName

java  -p          
modulepath
  -m     
moduleName/qualifiedClassName

java –module-path
modulepath
  -module
moduleName

java  -p          
modulepath
  -m     
moduleName

Creating and listing a modular JAR archive (p. 1189)Click here to view code image jar –verbose –create –file
jarfile -C directory files

jar -vcf
jarfile
 -C
directory
 .
jar -vcf
jarfile
 –main-class
qualifiedMainClassName
 -C
directory
 .
jar -vcfe
jarfile qualifiedMainClassName
 -C
directory
 .

jar –list –file
jarfile

jar -tf
jarfile

Listing available modules (p. 1212)Click here to view code image java –list-modules
java –module-path
modulepath
 –list-modules
java  -p          
modulepath
 –list-modules

Describing a module—that is, printing the module descriptor (p. 1213)Click here to view code image java –module-path
modulepath
 –describe-module
moduleName

java  -p          
modulepath
  -d              
moduleName


jar –file
jarFile
 –describe-module
jar  -f   
jarFile
  -d

Viewing package-level dependencies (p. 1214)Click here to view code image jdeps –module-path
modulepath
 -m
moduleName

jdeps –module-path
modulepath
 -m
moduleName
 –recursive
jdeps –module-path
modulepath
 -m
moduleName
 -R

Viewing module dependencies (p. 1216)Click here to view code image jdeps –module-path
modulepath
 -m
moduleName
 -summary
jdeps –module-path
modulepath
 -m
moduleName
 -summary –recursive
jdeps –module-path
modulepath
 -m
moduleName
 -s -R

Viewing class-level dependencies (p. 1216)Click here to view code image jdeps –module-path
modulepath
 -m
moduleName
 -verbose
jdeps –module-path
modulepath
 -m
moduleName
 -verbose –recursive
jdeps –module-path
modulepath
 -m
moduleName
 -v -R

Input and Output – Java I/O: Part I

20.1 Input and Output

Java provides I/O streams as a general mechanism for dealing with data input and output. I/O streams implement sequential processing of data. An input stream allows an application to read a sequence of data, and an output stream allows an application to write a sequence of data. An input stream acts as a source of data, and an output stream acts as a destination of data. The following entities can act as both input and output streams:

  • A file—which is the focus in this chapter
  • An array of bytes or characters
  • A network connection

There are two categories of I/O streams:

  • Byte streams that process bytes as a unit of data
  • Character streams that process characters as a unit of data

A low-level I/O stream operates directly on the data source (e.g., a file or an array of bytes), and processes the data primarily as bytes.

A high-level I/O stream is chained to an underlying stream, and provides additional capabilities for processing data managed by the underlying stream—for example, processing bytes from the underlying stream as Java primitive values or objects. In other words, a high-level I/O stream acts as a wrapper for the underlying stream.

In the rest of this chapter we primarily explore how to use I/O streams of the standard I/O API provided by the java.io package to read and write various kinds of data that is stored in files.

20.2 Byte Streams: Input Streams and Output Streams

The abstract classes InputStream and OutputStream in the java.io package are the root of the inheritance hierarchies for handling the reading and writing of data as bytes (Figure 20.1). Their subclasses, implementing different kinds of input and output (I/O) streams, override the following methods from the InputStream and OutputStream classes to customize the reading and writing of bytes, respectively:

Figure 20.1 Partial Byte Stream Inheritance Hierarchies in the java.io Package

From the InputStream abstract class:

Click here to view code image

int read() throws IOException
int read(byte[] b) throws IOException
int read(byte[] b, int off, int len) throws IOException

Note that the first read() method reads a byte, but returns an int value. The byte read resides in the eight least significant bits of the int value, while the remaining bits in the int value are zeroed out. The read() methods return the value –1 when the end of the input stream is reached.

Click here to view code image

long transferTo(OutputStream out) throws IOException

Reads bytes from this input stream and writes to the specified output stream in the order they are read. The I/O streams are not closed after the operation (see below).

From the OutputStream abstract class:

Click here to view code image

void write(int b) throws IOException
void write(byte[] b) throws IOException
void write(byte[] b, int off, int len) throws IOException

The first write() method takes an int as an argument, but truncates it to the eight least significant bits before writing it out as a byte to the output stream.

Click here to view code image

void close() throws IOException       
Both
 InputStream
and
 OutputStream
void flush() throws IOException       
Only for
 OutputStream

A I/O stream should be closed when no longer needed, to free system resources. Closing an output stream automatically flushes the output stream, meaning that any data in its internal buffer is written out.

Since byte streams also implement the AutoCloseable interface, they can be declared in a try-with-resources statement (§7.7, p. 407) that will ensure they are properly closed after use at runtime.

An output stream can also be manually flushed by calling the second method.

Read and write operations on streams are blocking operations—that is, a call to a read or write method does not return before a byte has been read or written.

Many methods in the classes contained in the java.io package throw the checked IOException. A calling method must therefore either catch the exception explicitly, or specify it in a throws clause.

Table 20.1 and Table 20.2 give an overview of selected byte streams. Usually an output stream has a corresponding input stream of the same type.

Table 20.1 Selected Input Streams

FileInputStreamData is read as bytes from a file. The file acting as the input stream can be specified by a File object, a FileDescriptor, or a String file name.
FilterInputStreamThe superclass of all input filter streams. An input filter stream must be chained to an underlying input stream.
DataInputStreamA filter stream that allows the binary representation of Java primitive values to be read from an underlying input stream. The underlying input stream must be specified.
ObjectInputStreamA filter stream that allows binary representations of Java objects and Java primitive values to be read from a specified input stream.

Table 20.2 Selected Output Streams

FileOutputStreamData is written as bytes to a file. The file acting as the output stream can be specified by a File object, a FileDescriptor, or a String file name.
FilterOutputStreamThe superclass of all output filter streams. An output filter stream must be chained to an underlying output stream.
PrintStreamA filter output stream that converts a text representation of Java objects and Java primitive values to bytes before writing them to an underlying output stream, which must be specified. This is the type of System.out and System.err (p. 1255). However, the PrintWriter class is recommended when writing characters rather than bytes (p. 1247).
DataOutputStreamA filter stream that allows the binary representation of Java primitive values to be written to an underlying output stream. The underlying output stream must be specified.
ObjectOutputStreamA filter stream that allows the binary representation of Java objects and Java primitive values to be written to a specified underlying output stream.

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.

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.”);
    }
  }
}

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

Interoperability with the java.net.URI Class – Java I/O: Part II

Interoperability with the java.net.URI Class

A URI consists of a string that identifies a resource, which can be a local resource or a remote resource. Among other pertinent information, a URI specifies a scheme (e.g., file, ftp, http, and https) that indicates what protocol to use to handle the resource. The examples of URIs below show two schema: file and http. We will not elaborate on the syntax of URIs, or different schema, as it is beyond the scope of this book.

Click here to view code image

file:///a/b/c/d                      // Scheme: file, to access a local file.
http://www.whatever.com              // Scheme: http, to access a remote website.

Click here to view code image

URI(String str) throws URISyntaxException
static URI create(String str)

This constructor and this static method in the java.net.URI class create a URL object based on the specified string. The second method is preferred when it is known that the URI string is well formed.

Click here to view code image

// Create a URI object, using the URL.create(String str) static factory method.
URI uri1 = URI.create(“file:///a/b/c/d”);   // Local file.

The following methods can be used to convert a URI object to a Path object. The Paths.get(URI uri) static factory method actually invokes the Path.of(URI uri) static factory method.

Click here to view code image

// URI –> Path, using the Path.of(URI uri) static factory method.
Path uriToPath1 = Path.of(uri1);       // /a/b/c/d
// URI –> Path, using the Paths.get(URI uri) static factory method.
Path uriToPath2 = Paths.get(uri1);     // /a/b/c/d

The following method in the Path interface can be used to convert a Path object to a URI object:

Click here to view code image

// Path –> URI, using the Path.toUri() instance method.
URI pathToUri = uriToPath1.toUri();    // file:///a/b/c/d

Interoperability between a Path object and a URI object allows an application to leverage both the NIO.2 API and the network API.



Selective Serialization – Java I/O: Part I

Selective Serialization

As noted earlier, static fields are not serialized, as these are not part of the state of an object. An instance field of an object can be omitted from being serialized by specifying the transient modifier in the declaration of the field—typically used for sensitive data in a field. Selective serialization discussed here is not applicable to record classes.

Example 20.7 illustrates some salient aspects of serialization. The setup comprises the classes Wheel and Unicycle, and their client class SerialClient. The class Unicycle has a field of type Wheel, and the class Wheel has a field of type int. The class Unicycle is a compound object with a Wheel object as a constituent object. The class Serial-Client serializes and deserializes a unicycle in the try-with-resources statements at (4) and (5), respectively. The state of the objects is printed to the standard output stream before serialization, and so is the state of the object created by deserialization.

Both the Compound Object and Its Constituents Are Serializable

If we run the program with the following declarations for the Wheel and the Unicycle classes, where a compound object of the serializable class Unicycle uses an object of the serializable class Wheel as a constituent object:

Click here to view code image

class Wheel implements Serializable {                               // (1a)
  private int wheelSize;
  …
}
class Unicycle implements Serializable {                            // (2)
  private Wheel wheel;                                              // (3a)
  …
}

we get the following output, showing that both serialization and deserialization were successful:

Click here to view code image

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

A compound object with its constituent objects is often referred to as an object graph. Serializing a compound object serializes its complete object graph—that is, the compound object and its constituent objects are recursively serialized.

Example 20.7 Non-Serializable Objects

Click here to view code image

import java.io.Serializable;
// public class Wheel implements Serializable {                   // (1a)
public class Wheel {                                              // (1b)
  private int wheelSize;
  public Wheel(int ws) { wheelSize = ws; }
  @Override
  public String toString() { return “wheel size: ” + wheelSize; }
}

Click here to view code image

import java.io.Serializable;
public class Unicycle implements Serializable {                     // (2)
  private Wheel wheel;                                              // (3a)
//transient private Wheel wheel;                                    // (3b)
  public Unicycle (Wheel wheel) { this.wheel = wheel; }
  @Override
  public String toString() { return “Unicycle with ” + wheel; }
}

Click here to view code image

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

Serialization and Inheritance – Java I/O: Part I

Serialization and Inheritance

The inheritance hierarchy of an object also determines what its state will be after it is deserialized. An object will have the same state at deserialization as it had at the time it was serialized if all its superclasses are also serializable. This is because the normal object creation and initialization procedure using constructors is not run during deserialization.

However, if any superclass of an object is not serializable, then the normal creation procedure using no-argument or default constructors is run, starting at the first non-serializable superclass, all the way up to the Object class. This means that the state at deserialization might not be the same as at the time the object was serialized because superconstructors run during deserialization may have initialized the state of the object. If the non-serializable superclass does not provide a non-argument constructor or the default constructor, a java.io.InvalidClassException is thrown during deserialization.

Example 20.9 illustrates how inheritance affects serialization. The Student class is a subclass of the Person class. Whether the superclass Person is serializable or not has implications for serializing objects of the Student subclass, in particular, when their byte representation is deserialized.

Superclass Is Serializable

If the superclass is serializable, then any object of a subclass is also serializable. In Example 20.9, the code at (4) in the class SerialInheritance serializes a Student object:

Click here to view code image

Student student = new Student(“Pendu”, 1007);
System.out.println(“Before writing: ” + student);
outputStream.writeObject(student);

The corresponding code for deserialization of the streamed object is at (5) in the class SerialInheritance:

Click here to view code image

Student student = (Student) inputStream.readObject();
System.out.println(“After reading: ” + student);

We get the following output from the program in Example 20.9 when it is run with (1a) and (3a) in the Person class and the Student class, respectively—that is, when the superclass is serializable and so is the subclass, by virtue of inheritance.

The results show that the object state prior to serialization is identical to the object state after deserialization. In this case, no superclass constructors were run during deserialization.

Click here to view code image

Before writing: Student state(Pendu, 1007)
After reading: Student state(Pendu, 1007)