Input / Output in java Chapter 13

Chapter 13

Input/Output

Input/output (I/O) is one of the most common operations performed by computer programs. Examples of I/O operations include

images creating and deleting files

images reading from and writing to a file or network socket

images serializing (or saving) objects to persistent storage and retrieving the saved objects

Java support for I/O has been available since JDK 1.0 in the form of the I/O API in the java.io package. JDK 1.4 added the New I/O (NIO) APIs that offer performance improvement in buffer management, scalable network and file I/O. Java NIO APIs are part of the java.nio package and its subpackages. JDK 7 introduces yet a new set of packages called NIO.2 to complement the existing technologies. There is no java.nio2 package. Instead, new types can be found in the java.nio.file package and its subpackages. One of the new features NIO.2 introduces is the Path interface designed to displace the java.io.File class, now considered inferior. The old File class has often been a source of frustration because many of its methods fail to throw exceptions, its delete method often fails for inexplicable reasons, and its rename method doesn't work consistently across different operating systems.

Another addition in JDK 7 that has big impacts on the I/O and NIO APIs is the java.lang.AutoCloseable interface. The majority of java.io classes now implement this interface to support try-with-resources.

This chapter presents topics based on functionality and select the most important members of java.io and java.nio.file. java.io.File is no longer discussed in favor of the new Path interface. However, java.io.File was widely used prior to JDK 7 and therefore is still everywhere in applications written in previous versions of Java. It is therefore recommended that you read the Input/Output chapter of the second edition of this book (included in the zipped file accompanying this book).

File systems and paths are the first topic in this chapter. Here you learn what a path is and how the file system is represented in Java.

The second section, “File and Directory Handling and Manipulation,” discusses the powerful java.nio.file.Files class. You can use Files to create and delete files and directories, check the existence of a file, and read from and write to a file.

Note that support for reading from and writing to a files in Files is only suitable for small files. For larger files and for added functionality, you need a stream. Streams, which are discussed in the section “Input/Output Streams,” act like water pipes that facilitate data transmission. There are four types of streams: InputStream, OutputStream, Reader, and Writer. For better performance, there are also classes that wrap these streams and buffer the data being read or written.

Reading from and writing to a stream dictate that you do so sequentially, which means to read the second unit of data, you must read the first one first. For random access files'in other words, to access any part of a file randomly—you need a different Java type. The java.io.RandomAccessFile class used to be a good choice for non-sequential operations, however a better way now is to use java.nio.channels.SeekableByteChannel. The latter is discussed in the section “Random Access Files.”

This chapter concludes with object serialization and deserialization.

File Systems and Paths

A file system can contain three types of objects: file, directory (a.k.a folder), and symbolic link. Not all operating systems support symbolic links, and early operating systems featured a flat-file system with no subdirectories. However most operating systems today support at least files and directories and allow directories to contain subdirectories. A directory on top of the directory tree is called a root. Linux/UNIX variants have one root: /. Windows can have multiple roots: C:\, D:\, and so on.

An object in a file system can be uniquely identified by a path. For instance, you can refer to the image1.jpg file in your Mac's /home/user directory as /home/user/image1.jpg, which is a path. A temp directory under your Windows’ C:\ is C:\temp, which is also a path. Paths must be unique throughout a file system. For example, you cannot create a document.bak directory in /home/user if there is already a file named document.bak in that directory.

A path can be absolute or relative. An absolute path has all the information to point to an object in the file system. For instance, /home/kyleen and /home/alexis are absolute paths. A relative does not have all the information needed. For example, home/jayden is relative to the current directory. Only if the current directory is known can home/jayden be resolved.

In Java a file or a directory was traditionally represented by a java.io.File object. However, the File class has many drawbacks and Java 7 brings with it a better replacement in its NIO.2 package, the java.nio.file.Path interface.

The aptly named Path interface represents a path, which can be a file, a directory, or a symbolic link. It can also represent a root. Before I explain Path in detail, let me introduce you to another member of the java.nio.file package, the FileSystem class.

As the name implies, FileSystem represents a file system. It is an abstract class and to obtain the current file system, you call the FileSystems.getDefault() static method:

FileSystem fileSystem = FileSystems.getDefault();

FileSystems has other methods. The getSeparator method returns the name separator as String. In Windows this will be “\” and in UNIX/Linux it will be “/”. Here is its signature.

public abstract java.lang.String getSeparator()

Another method of FileSystem, getRootDirectories, returns an Iterable for iterating root directories.

public abstract java.lang.Iterable<Path> getRootDirectories()

To create a Path, use FileSystem's getPath method:

public abstract Path getPath(String first, String... more)

Only the first argument in getPath is required, the more argument is optional. If more is present, it will be appended to first. For example, to create a path that refers to /home/user/images, you would write either of these two statements.


Path path = FileSystems.getDefault().getPath("/home/user/images");

Path path = FileSystems.getDefault().getPath("/home", "user",
        "images");

The java.nio.file.Paths class provides a shortcut for creating a Path through its static get method:


Path path1 = Paths.get("/home/user/images");
Path path2 = Paths.get("/home", "user", "images");
Path path3 = Paths.get("C:\temp");
Path path4 = Paths.get("C:\", "temp");

Paths like /home/user/images or C:\temp can be broken into its elements. /home/user/images has three names, home, user, and images. C:\temp has only one name, temp, because the root does not count. The getNameCount method in Path returns the number of names in a path. Each individual name can be retrieved using getName:

Path getName(int index)

The index parameter is zero-based. Its value must be between 0 and the number of elements minus 1. The first element closest to the root has index 0. Consider this code snippet.


Path path = Paths.get("/home/user/images");
System.out.println(path.getNameCount()); // prints 3
System.out.println(path.getName(0)); // prints home
System.out.println(path.getName(1)); // prints user
System.out.println(path.getName(2)); // prints images

Other important methods of Path include getFileName, getParent, and getRoot.


Path getFileName()

Path getParent()

Path getRoot()

getFileName returns the file name of the current Path. Therefore, if path1 denotes /home/user1/Calculator.java, path1.getFileName() will return a relative path referring to the Calculator.java file. Calling path1.getParent() would return /home/user1 and calling path1.getRoot() would return /. Calling getParent on a root would return null.

A very important note: Creating a Path does not create a physical file or directory. Often Path instances reference non-existent physical objects. To create a file or directory, you need to use the Files class, which is discussed in the next section.

File and Directory Handling and Manipulation

java.nio.file.Files is a very powerful class that provides static methods for handling files and directories as well as reading from and writing to a file. With it you can create and delete a path, copy files, check if a path exists, and so on. In addition, Files comes with methods for creating stream objects that you'll find useful when working with input and output streams.

The following subsections elaborate what you can do with Files.

Creating and Deleting Files and Directories

To create a file you use the createFile method of Files. Here is its signature.


public static Path createFile(Path file,
        java.nio.file.attribute.FileAttribute<?>... attrs)

The attrs argument is optional, so you can ignore it if you don't need to set attributes. For example:


Path newFile = Paths.get("/home/jayden/newFile.txt");
Files.createFile(newFile);

createFile throws an IOException if the parent directory does not exist. It throws a FileAlreadyExistsException if there already exists a file, a directory, or a symbolic by the name specified by file.

To create a directory, use the createDirectory method.


public static Path createDirectory(Path directory,
        java.nio.file.attribute.FileAttribute<?>... attrs)

Like createFile, createDirectory may throw an IOException ora FileAlreadyExistsException.

To delete a file, a directory or a symbolic link, use the delete method:

public static void delete(Path path)

If path is a directory, then the directory must be empty. If path is a symbolic link, only the link is deleted and not the target of the link. If path does not exist, a NoSuchFileException is thrown.

To avoid having to check if a path exists when deleting a path, use deleteIfExists:

public static void deleteIfExists(Path path)

If you're deleting a directory with deleteIfExists, the directory must be empty. If not, a DirectoryNotEmptyException will be thrown.

Retrieving A Directory's Objects

You can retrieve the files, subdirectories, and symbolic links in a directory by using the newDirectoryStream method of the Files class. This method will return a DirectoryStream to iterate over all objects in a directory. The signature of newDirectoryStream is as follows.

public static DirectoryStream<Path> newDirectoryStream(Path path)

The DirectoryStream returned by this method must be closed after use.

For example, the following snippet prints all the subdirectories and files in a directory.


Path parent = ...
try (DirectoryStream<Path> children =
        Files.newDirectoryStream(parent)) {
    for (Path child : children) {
        System.out.println(child);
    }
} catch (IOException e) {
    e.printStackTrace();
}

Copying and Moving Files

There are three copy methods for copying files and directories. The easiest one to use is this one.


public static Path copy(Path source, Path target,
        CopyOption... options) throws java.io.IOException

CopyOption is an interface in java.nio.file. The StandardCopyOption enum is one of its implementations and offers three copy options:

images ATOMIC_MOVE. Move the file as an atomic file system operation.

images COPY_ATTRIBUTES. Copy attributes to the new file.

images REPLACE_EXISTING. Replace an existing file if it exists.



As an example, the following snippet copies the line1.bmp file in C:\temp to backup.bmp in the same directory.


Path source = Paths.get("C:/temp/line1.bmp");
Path target = Paths.get("C:/temp/backup.bmp")
try {
    Files.copy(source, target,
            StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
    e.printStackTrace();
}

You use the move method to move a file.


public static Path move(Path source, Path target,
        CopyOption... options) throws java.io.IOException

For example, the following code moves C:\temp\backup.bmp to C:\data.


Path source = Paths.get("C:/temp/backup.bmp");
Path target = Paths.get("C:/data/backup.bmp")
try {
    Files.move(source, target,
            StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
    e.printStackTrace();
}

Reading from and Writing to Files

The Files class provides methods for reading from and writing to a small binary and text file. The readAllBytes and readAllLines methods are for reading from a binary and text file, respectively.


public static byte[] readAllBytes(Path path)
        throws java.io.IOException
public static List<String> readAllLines(Path path,
        java.nio.charset.Charset charset) throws java.io.IOException

These write methods are for writing to a binary and text file, respectively.


public static Path write(Path path, byte[] bytes,
        OpenOption... options) throws java.io.IOException
public static Path write(Path path, java.lang.Iterable<? extends
        CharSequence> lines, java.nio.charset.Charset charset,
        OpenOption... options) throws java.io.IOException

Both write method overloads take an optional OpenOption and the second overload a Charset. The OpenOption interface defines options for opening a file for write access. The StandardOpenOption enum implements OpenOption and provides the following values.

images APPEND. If the file is opened for write access, the data written will be appended to the end of the file.

images CREATE. Create a new file if it does not exist.

images CREATE_NEW. Create a new file and throws an exception if it already exists.

images DELETE_ON_CLOSE. Delete the file on close.

images DSYNC. Dictate that update to the file content be written synchronously.

images READ. Open for read access.

images SPARSE. Sparse file.

images SYNC. Dictate that update to the file content and metadata be written synchronously.

images TRUNCATE_EXISTING. Truncate the file's length to 0 if it is opened for write and it already exists.

images WRITE. Open for write access.



java.nio.charset.Charset is an abstract class that represents a character set. You need to specify the character set being used when encoding characters to bytes and decoding bytes to characters. See the discussion of ASCII and Unicode in Chapter 2, “Language Fundamentals,” if you've forgotten about it.

The easiest way to create a Charset is by calling the static Charset.forName method, passing a character set name. For instance, to create a US ASCII Charset, you would write

Charset usAscii = Charset.forName("US-ASCII");

Now that you know a little bit about OpenOption and Charset, have a look at the following code snippet, which writes a few lines of text to C:\temp\speech.txt and write them back.


// write and read to a text file
Path textFile = Paths.get("C:/temp/speech.txt");
Charset charset = Charset.forName("US-ASCII");
String line1 = "Easy read and write";
String line2 = "with java.nio.file.Files";
List<String> lines = Arrays.asList(line1, line2);
try {
    Files.write(textFile, lines, charset);
} catch (IOException ex) {
    ex.printStackTrace();
}

// read back
List<String> linesRead = null;
try {
    linesRead = Files.readAllLines(textFile, charset);
} catch (IOException ex) {
    ex.printStackTrace();
}

if (linesRead != null) {
    for (String line : linesRead) {
        System.out.println(line);
    }
}

Note that the read and write methods in Files are only good for small files. For medium-sized and large files, use streams instead.

Input/Output Streams

I/O streams can be likened to water pipes. Just like water pipes connect city houses to a water reservoir, a Java stream connects Java code to a “data reservoir.” In Java terminology, this “data reservoir” is called a sink and could be a file, a network socket, or memory. The good thing about streams is you employ a uniform way to transport data from and to different sinks, hence simplifying your code. You just need to construct the correct stream.

Depending on the data direction, there are two types of streams, input stream and output stream. You use an input stream to read from a sink and an output stream to write to a sink. Because data can be classified into binary data and characters (human readable data), there are also two types of input streams and two types of output streams. These streams are represented by the following four abstract classes in the java.io package.

images Reader. A stream for reading characters from a sink.

images Writer. A stream for writing characters to a sink.

images InputStream. A stream for reading binary data from a sink.

images OutputStream. A stream for writing binary data to a sink.



The benefit of streams is they define methods for data reading and writing that can be used regardless the data source or destination. To connect to a particular sink, you simply need to construct the correct implementation class. The java.nio.file.Files class provides methods for constructing streams that connect to a file.

A typical sequence of operations when working with a stream is as follows:

1. Create a stream. The resulting object is already open, there is no open method to call.

2. Perform reading or writing operations.

3. Close the stream by calling its close method. Since most stream classes now implement java.lang.AutoCloseable, you can create a stream in a try-with-resources statement and get the streams automatically closed for you.



The stream classes will be discussed in clear detail in the following sections.

Reading Binary Data

You use an InputStream to read binary data from a sink. InputStream is an abstract class with a number of concrete subclasses, as shown in Figure 13.1.

images

Figure 13.1: The hierarchy of InputStream

Prior to JDK 7 you used FileInputStream to read binary from a file. With the advent of NIO.2, you can call Files.newInputStream to obtain an InputStream with a file sink. Here's the signature of newInputStream:


public static java.io.InputStream newInputStream(Path path,
        OpenOption... options) throws java.io.IOException

InputStream implements java.lang.AutoCloseable so you can use it in a try-with-resources statement and don't need to explicitly close it. Here is some boilerplate code.


Path path = ...
try (InputStream inputStream = Files.newInputStream(path,
        StandardOpenOption.READ) {

    // manipulate inputStream

} catch (IOException e) {
    // do something with e
}

The InputStream returned from Files.newInputStream is not buffered so you should wrap it with a BufferedInputStream for better performance. As such, your boilerplate code would look like this.


Path path = ...
try (InputStream inputStream = Files.newInputStream(path,
        StandardOpenOption.READ;
        BufferedInputStream buffered =
                new BufferedInputStream(inputStream)) {

        // manipulate buffered, not inputStream

} catch (IOException e) {
    // do something with e
}

At the core of InputStream are three read method overloads.


public int read()

public int read(byte[] data)

public int read(byte[] data, int offset, int length)

InputStream employs an internal pointer that points to the starting position of the data to be read. Each of the read method overloads returns the number of bytes read or -1 if no data was read into the InputStream. It returns -1 when the internal pointer has reached the end of file.

The no-argument read method is the easiest to use. It reads the next single byte from this InputStream and returns an int, which you can then cast to byte. Using this method to read a file, you use a while block that keeps looping until the read method returns -1:


int i = inputStream.read();
while (i != -1) {
    byte b = (byte) I;
    // do something with b
}

For speedier reading, you should use the second or third read method overload, which requires you to pass a byte array. The data will then be stored in this array. The size of the array is a matter of compromise. If you assign a big number, the read operation will be faster because more bytes are read each time. However, this means allocating more memory space for the array. In practice, the array size should start from 1000 and up.

What if there are fewer bytes available than the size of the array? The read method overloads return the number of bytes read, so you always know which elements of your array contain valid data. For example, if you use an array of 1,000 bytes to read an InputStream and there are 1,500 bytes to read, you will need to invoke the read method twice. The first invocation will give you 1,000 bytes, the second 500 bytes.

You can choose to read fewer bytes than the array size using the three-argument read method overload:

public int read(byte[] data, int offset, int length)

This method overload reads length bytes into the byte array. The value of offset determines the position of the first byte read in the array.

In addition to the read methods, there are also these methods:

public int available() throws IOException

This method returns the number of bytes that can be read (or skipped over) without blocking.

public long skip(long n) throws IOException

Skips over the specified number of bytes from this InputStream. The actual number of bytes skipped is returned and this may be smaller than the prescribed number.

public void mark(int readLimit)

Remembers the current position of the internal pointer in this InputStream. Calling reset afterwards would return the pointer to the marked position. The readLimit argument specifies the number of bytes to be read before the mark position gets invalidated.

public void reset()

Repositions the internal pointer in this InputStream to the marked position.

public void close()

Closes this InputStream. Unless you created an InputStream in a try-with-resources statement, you should always call this method when you are done with the InputStream to release resources.

As an example, the code in Listing 13.1 shows an InputStreamTest class that contains a compareFiles method for comparing two files.

Listing 13.1: The compareFiles method that uses InputStream


import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class InputStreamTest {
    public boolean compareFiles(Path path1, Path path2)
            throws NoSuchFileException {
        if (Files.notExists(path1)) {
            throw new NoSuchFileException(path1.toString());
        }
        if (Files.notExists(path2)) {
            throw new NoSuchFileException(path2.toString());
        }
        try {
        if (Files.size(path1) != Files.size(path2)) {
            return false;
        }
        } catch (IOException e) {
            e.printStackTrace();
        }
        try (InputStream inputStream1 = Files.newInputStream(
                    path1, StandardOpenOption.READ);
            InputStream inputStream2 = Files.newInputStream(
                    path2, StandardOpenOption.READ)) {
            int i1, i2;
            do {
                i1 = inputStream1.read();
                i2 = inputStream2.read();
                if (i1 != i2) {
                    return false;
                }
            } while (i1 != -1);
            return true;
        } catch (IOException e) {
            return false;
        }
    }

    public static void main(String[] args) {
        Path path1 = Paths.get("C:\\temp\\line1.bmp");
        Path path2 = Paths.get("C:\\temp\\line2.bmp");
        InputStreamTest test = new InputStreamTest();
        try {
            if (test.compareFiles(path1, path2)) {
                System.out.println("Files are identical");
            } else {
                System.out.println("Files are not identical");
            }
        } catch (NoSuchFileException e) {
            e.printStackTrace();
        }

        // the compareFiles method is not the same as
        // Files.isSameFile
        try {
            System.out.println(Files.isSameFile(path1, path2));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

compareFiles returns true if the two files compared are identical. The brain of the method is this block.


int i1, i2;
do {
    i1 = inputStream1.read();
    i2 = inputStream2.read();
    if (i1 != i2) {
        return false;
    }
} while (i1 != -1);
return true;

It reads the next byte from the first InputStream to i1 and the second InputStream to i2 and compares i1 with i2. It will continue reading until i1 and i2 are different or the end of file is reached.

Writing Binary Data

The OutputStream abstract class represents a stream for writing binary data to a sink. Its child classes are shown in Figure 13.2.

images

Figure 13.2: The implementation classes of OutputStream

In pre-7 JDK you would use java.io.FileOutputStream to write binary to a file. Thanks to NIO.2, you can now call Files.newOutputStream to obtain an OutputStream with a file sink. Here's the signature of newOutputStream:


public static java.io.OutputStream newOutputStream(Path path,
        OpenOption... options) throws java.io.IOException

OutputStream implements java.lang.AutoCloseable so you can use it in a try-with-resources statement and don't need to explicitly close it. Here is how you can create an OutputStream with a file sink:


Path path = ...
try (OutputStream outputStream = Files.newOutputStream(path,
        StandardOpenOption.CREATE, StandardOpenOption.APPEND) {

    // manipulate outputStream

} catch (IOException e) {
    // do something with e
}

The OutputStream returned from Files.newOutputStream is not buffered so you should wrap it with a BufferedOutputStream for better performance. Therefore, your boilerplate code would look like this.


Path path = ...
try (OutputStream outputStream = Files.newOututStream(path,
        StandardOpenOption.CREATE, StandardOpenOption.APPEND;
        BufferedOutputStream buffered =
                new BufferedOutputStream(outputStream)) {

        // manipulate buffered, not outputStream

} catch (IOException e) {
    // do something with e
}

OutputStream defines three write method overloads, which are mirrors of the read method overloads in InputStream:


public void write(int b)

public void write(byte[] data)

public void write(byte[] data, int offset, int length)

The first overload writes the lowest 8 bits of integer b to this OutputStream. The second writes the content of a byte array to this OutputStream. The third overload writes length bytes of the data starting at position offset.

In addition, there are also the no-argument close and flush methods. close closes the OutputStream and flush forces any buffered content to be written out to the sink. You don't need to call close if you created the OutputStream in a try-with-resources statement.

As an example, Listing 13.2 shows how to copy a file using OutputStream.

Listing 13.2: The OutputStreamTest class


package app13;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class OutputStreamTest {
    public void copyFiles(Path originPath, Path destinationPath)
            throws IOException {
        if (Files.notExists(originPath)
                || Files.exists(destinationPath)) {
            throw new IOException(
                    "Origin file must exist and " +
                    "Destination file must not exist");
        }
        byte[] readData = new byte[1024];
        try (InputStream inputStream =
                    Files.newInputStream(originPath,
                    StandardOpenOption.READ);
            OutputStream outputStream =
                    Files.newOutputStream(destinationPath,
                    StandardOpenOption.CREATE)) {
            int i = inputStream.read(readData);
            while (i != -1) {
                outputStream.write(readData, 0, i);
                i = inputStream.read(readData);
            }
        } catch (IOException e) {
            throw e;
        }
    }

    public static void main(String[] args) {
        OutputStreamTest test = new OutputStreamTest();
        Path origin = Paths.get("C:\\temp\\line1.bmp");
        Path destination = Paths.get("C:\\temp\\line3.bmp");
        try {
            test.copyFiles(origin, destination);
            System.out.println("Copied Successfully");
        } catch (IOException e) {
            e.printStackTrace();
       }
    }
}

This part of the copyFile method does the work.


byte[] readData = new byte[1024];
try (InputStream inputStream =
            Files.newInputStream(originPath,
            StandardOpenOption.READ);
    OutputStream outputStream =
            Files.newOutputStream(destinationPath,
            StandardOpenOption.CREATE)) {
    int i = inputStream.read(readData);
    while (i != -1) {
        outputStream.write(readData, 0, i);
        i = inputStream.read(readData);
    }
} catch (IOException e) {
    throw e;
}

The readData byte array is used to store the data read from the InputStream. The number of bytes read is assigned to i. The code then calls the write method on the OutputStream, passing the byte array and i as the third argument.

outputStream.write(readData, 0, i);

Writing Text (Characters)

The abstract class Writer defines a stream used for writing characters. Figure 13.3 shows the implementations of Writer.

images

Figure 13.3: The subclasses of Writer

OutputStreamWriter facilitates the translation of characters into byte streams using a given character set. The character set guarantees that any Unicode characters you write to this OutputStreamWriter will be translated into the correct byte representation. FileWriter is a subclass of OutputStreamWriter that provides a convenient way to write characters to a file. However, FileWriter is not without flaws. When using FileWriter you are forced to output characters using the computer's encoding, which means characters outside the current character set will not be translated correctly into bytes. A better alternative to FileWriter is PrintWriter.

The following sections cover Writer and some of its descendants.

Writer

This class is similar to OutputStream, except that Writer deals with characters instead of bytes. Like OutputStream, Writer has three write method overloads:


public void write(int b)

public void write(char[] text)

public void write(char[] text, int offset, int length)

When working with text or characters, however, you ordinarily use strings. As such, there are two other overloads of the write method that accept a String object.


public void write(String text)

public void write(String text, int offset, int length)

The last write method overload allows you to pass a String and write part of the String to the Writer.

OutputStreamWriter

An OutputStreamWriter is a bridge from character streams to byte streams: Characters written to an OutputStreamWriter are encoded into bytes using a specified character set. The latter is an important element of OutputStreamWriter because it enables the correct translations of Unicode characters into byte representation.

The OutputStreamWriter class offers four constructors:


public OutputStreamWriter(OutputStream out)

public OutputStreamWriter(OutputStream out,
        java.nio.charset.Charset cs)

public OutputStreamWriter(OutputStream out,
        java.nio.charset.CharsetEncoder enc)

public OutputStreamWriter(OutputStream out, String encoding)

All the constructors accept an OutputStream, to which bytes resulting from the translation of characters written to this OutputStreamWriter will be written. Therefore, if you want to write to a file, you simply need to create an OutputStream with a file sink:


OutputStream os = Files.newOutputStream(path, openOption);
OutputStreamWriter writer = new OutputStreamWriter(os, charset);

Listing 13.3 shows an example of OutputStreamWriter.

Listing 13.3: Using OutputStreamWriter


package app13;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class OutputStreamWriterTest {
    public static void main(String[] args) {
        char[] chars = new char[2];
        chars[0] = '\u4F60'; // representing images
        chars[1] = '\u597D'; // representing images;
        Path path = Paths.get("C:\\temp\\myFile.txt");
        Charset chineseSimplifiedCharset =
                Charset.forName("GB2312");

        try (OutputStream outputStream =
                Files.newOutputStream(path,
                StandardOpenOption.CREATE);
            OutputStreamWriter writer = new OutputStreamWriter(
                    outputStream, chineseSimplifiedCharset)) {

            writer.write(chars);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

The code in Listing 13.3 creates an OutputStreamWriter based on a OutputStream that writes to C:\temp\myFile.txt on Windows. Therefore, if you are using Linux/Unix you need to change the value of textFile. The use of an absolute path is intentional since most readers find it easier to find if they want to open the file. The OutputStreamWriter uses the GB2312 character set (simplified Chinese).

The code in Listing 13.3 passes two Chinese characters: (represented by the Unicode 4F60) and (Unicode 597D). means ‘How are you?’ in Chinese.

When executed, the OutputStreamWriterTest class will create a myFile.txt file. It is 4 bytes long. You can open it and see the Chinese characters. For the characters to be displayed correctly, you need to have the Chinese font installed in your computer.

PrintWriter

PrintWriter is a better alternative to OutputStreamWriter. Like OutputStreamWriter, PrintWriter lets you choose an encoding by passing the encoding information to one of its constructors. Here are two of its constructors:


public PrintWriter(OutputStream out)

public PrintWriter(Writer out)

To create a PrintWriter that writes to a file, simply create an OutputStream with a file sink.

PrintWriter is more convenient to work with than OutputStreamWriter because the former adds nine print method overloads for printing any type of Java primitives and objects. Here are the method overloads:


public void print(boolean b)

public void print(char c)

public void print(char[] s)

public void print(double d)

public void print(float f)

public void print(int i)

public void print(long l)

public void print(Object object)

public void print(String string)

There are also nine println method overloads, which are the same as the print method overloads, except that they print a new line character after the argument.

In addition, there are two format method overloads that enable you to print according to a print format. This method was covered in Chapter 5, “Core Classes.”

Always wrap your Writer with a BufferedWriter for better performance. BufferedWriter has the following constructors that allow you to pass a Writer object.


public BufferedWriter(Writer writer)

public BufferedWriter(Writer writer, in bufferSize)

The first constructor creates a BufferedWriter with the default buffer size (the documentation does not say how big). The second one lets you choose the buffer size.

With PrintWriter, however, you cannot wrap it like this


PrintWriter printWriter = ...;
BufferedWriter bw = new BufferedWriter(printWriter);

because then you would not be able to use the methods of the PrintWriter. Instead, wrap the Writer that is passed to a PrintWriter.

PrintWriter pw = new PrintWriter(new BufferedWriter(writer));

Listing 13.4 presents an example of PrintWriter.

Listing 13.4: Using PrintWriter


package app13;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class PrintWriterTest {
    public static void main(String[] args) {
        Path path = Paths.get("c:\\temp\\printWriterOutput.txt");
        Charset usAsciiCharset = Charset.forName("US-ASCII");
        try (BufferedWriter bufferedWriter =
                Files.newBufferedWriter(path, usAsciiCharset,
                StandardOpenOption.CREATE);
            PrintWriter printWriter =
                    new PrintWriter(bufferedWriter)) {
            printWriter.println("PrintWriter is easy to use.");
            printWriter.println(1234);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

The nice thing about writing with a PrinterWriter is when you open the resulting file, everything is human-readable. The file created by the preceding example says:


PrinterWriter is easy to use.
1234

Reading Text (Characters)

You use the Reader class to read text (characters, i.e. human readable data). The hierarchy of this class is shown in Figure 13.4.

images

Figure 13.4: Reader and its descendants

The following sections discuss Reader and some of its descendants.

Reader

Reader is an abstract class that represents an input stream for reading characters. It is similar to InputStream except that Reader deals with characters and not bytes. Reader has three read method overloads that are similar to the read methods in InputStream:


public int read()

public int read(char[] data)

public int read(char[] data, int offset, int length)

These method overloads allow you to read a single character or multiple characters that will be stored in a char array. Additionally, there is a fourth read method for reading characters into a java.nio.CharBuffer.

public int read(java.nio.CharBuffer target)

Reader also provides the following methods that are similar to those in InputStream: close, mark, reset, and skip.

InputStreamReader

An InputStreamReader reads bytes and translates them into characters using the specified character set. Therefore, InputStreamReader is ideal for reading from the output of an OutputStreamWriter or a PrintWriter. The key is you must know the encoding used when writing the characters to correctly read them back.

The InputStreamReader class has four constructors, all of which require you to pass an InputStream.


public InputStreamReader(InputStream in)

public InputStreamReader(InputStream in,
        java.nio.charset.Charset charset)

public InputStreamReader(InputStream in,
        java.nio.charset.CharsetDecoder decoder)

public InputStreamReader(InputStream in, String charsetName)

For instance, to create an InputStreamReader that reads from a file, you can pass to its constructor an InputStream from Files.newInputStream.


Path path = ...
Charset charset = ...
InputStream inputStream = Files.newInputStream(path,
        StandardOpenOption.READ);
InputStreamReader reader = new InputStreamReader(
        inputStream, charset);

Listing 13.5 presents an example that uses a PrintWriter to write two Chinese characters and read them back.

Listing 13.5: Using InputStreamReader


package app13;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class InputStreamReaderTest {
    public static void main(String[] args) {
        Path textFile = Paths.get("C:\\temp\\myFile.txt");
        Charset chineseSimplifiedCharset =
                Charset.forName("GB2312");
        char[] chars = new char[2];
s        chars[0] = '\u4F60'; // representing images
        chars[1] = '\u597D'; // representing images;

        // write text
        try (BufferedWriter writer =
                Files.newBufferedWriter(textFile,
                    chineseSimplifiedCharset,
                    StandardOpenOption.CREATE)) {
            writer.write(chars);
        } catch (IOException e) {
            System.out.println(e.toString());
        }

        // read back
        try (InputStream inputStream =
                Files.newInputStream(textFile,
                StandardOpenOption.READ);

            InputStreamReader reader = new
                    InputStreamReader(inputStream,
                            chineseSimplifiedCharset)) {
            char[] chars2 = new char[2];
            reader.read(chars2);
            System.out.print(chars2[0]);
            System.out.print(chars2[1]);
        } catch (IOException e) {
            System.out.println(e.toString());
        }
    }
}

BufferedReader

BufferedReader is good for two things:

1. Wraps another Reader and provides a buffer that will generally improve performance.

2. Provides a readLine method to read a line of text.



The readLine method has the following signature:

public java.lang.String readLine() throws IOException

It returns a line of text from this Reader or null if the end of the stream has been reached.

The java.nio.file.Files class offers a newBufferedReader method that returns a BufferedReader. Here is the signature.


public static java.io.BufferedReader newBufferedReader(Path path,
        java.nio.charset.Charset charset)

As an example, this snippet reads a text file and prints all lines.


Path path = ...
BufferedReader br = Files.newBufferedReader(path, charset);
String line = br.readLine();
while (line != null) {
    System.out.println(line);
    line = br.readLine();
}

Also, prior to the addition of the java.util.Scanner class in Java 5, you had to use a BufferedReader to read user input to the console. Listing 13.6 shows a getUserInput method for taking user input on the console.

Listing 13.6: The getUserInput method


public static String getUserInput() {
    BufferedReader br = new BufferedReader(
            new InputStreamReader(System.in));
    try {
        return br.readLine();
    } catch (IOException ioe) {
    }
    return null;
}

You can do this because System.in is of type java.io.InputStream.

Note

java.util.Scanner was discused in Chapter 5, “Core Classes.”

Logging with PrintStream

By now you must be familiar with the print method of System.out. You use it especially for displaying messages to help you debug your code. However, by default System.out sends the message to the console, and this is not always preferable. For instance, if the amount of data displayed exceeds a certain lines, previous messages are no longer visible. Also, you might want to process the messages further, such as sending the messages by email.

The PrintStream class is an indirect subclass of OutpuStream. Here are some of its constructors:


public PrintStream(OutputStream out)

public PrintStream(OutputStream out, boolean autoFlush)

public PrintStream(OutputStream out, boolean autoFlush,
        String encoding)

PrintStream is very similar to PrintWriter. For example, both have nine print method overloads. Also, PrintStream has a format method similar to the format method in the String class.

System.out is of type java.io.PrintStream. The System object lets you replace the default PrintStream by using the setOut method. Listing 13.7 presents an example that redirects System.out to a file.

Listing 13.7: Redirecting System.out to a file


package app13;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class PrintStreamTest {
    public static void main(String[] args) {
        Path debugFile = Paths.get("C:\\temp\\debug.txt");
        try (OutputStream outputStream = Files.newOutputStream(
                debugFile, StandardOpenOption.CREATE,
                StandardOpenOption.APPEND);
            PrintStream printStream = new PrintStream(outputStream,
                    true)) {

            System.setOut(printStream);
            System.out.println("To file");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Note

You can also replace the default in and out in the System object by using the setIn and setErr methods.

Random Access Files

Using a stream to access a file dictates that the file is accessed sequentially, e.g. the first character must be read before the second, etc. Streams are ideal when the data comes in a sequential fashion, for example if the medium is a tape (widely used long ago before the emergence of harddisk) or a network socket. Streams are good for most of your applications, however sometimes you need to access a file randomly and using a stream would not be fast enough. For example, you may want to change the 1000th byte of a file without having to read the first 999 bytes. For random access like this, there are a few Java types that offer a solution. The first is the java.io.RandomAccessFile class, which is easy to use but now out-dated. The second is the java.nio.channels.SeekableByteChannel interface, which should be used in new applications. A discussion of RandomAccessFile can be found in Chapter 13 of the previous edition of this book. This edition, however, teaches random access files using SeekableByteChannel.

A SeekableByteChannel can perform both read and write operations. You can get an implementation of SeekableByteChannel using one of the Files class's newByteChannel methods:


public static java.nio.channels.SeekableByteChannel
        newByteChannel(Path path, OpenOption... options)

When using Files.newByteChannel() to open a file, you can choose an open option such as read-only or read-write or create-append. For instance


Path path1 = ...
SeekableByteChannel readOnlyByteChannel =
        Files.newByteChannel(path1, EnumSet.of(READ)));

Path path2 = ...
SeekableByteChannel writableByteChannel =
        Files.newByteChannel(path2, EnumSet.of(CREATE,APPEND));

SeekableByteChannel employs an internal pointer that points to the next byte to read or write. You can obtain the pointer position by calling the position method:

long position() throws java.io.IOException

When a SeekableByteChannel is created, initially it points to the first byte and position() would return 0L. You can change the pointer's position by invoking another position method whose signature is as follows.


SeekableByteChannel position(long newPosition)
        throws java.io.IOException

This pointer is zero-based, which means the first byte is indicated by index 0. You can pass a number greater than the file size without throwing an exception, but this will not change the size of the file. The size method returns the current size of the resource to which the SeekableByteChannel is connected:

long size() throws java.io.IOException

SeekableByteChannel is extremely simple. To read from or write to the underlying file, you call its read or write method, respectively.


int read(java.nio.ByteBuffer buffer) throws java.io.IOException

int write(java.nio.ByteBuffer buffer) throws java.io.IOException

Both read and write take a java.nio.ByteBuffer. This means to use SeekableByteChannel you need to be familiar with the ByteBuffer class. So, here is a crash course in ByteBuffer.

ByteBuffer is one of the many descendants of java.nio.Buffer, a data container for a specific primitive type. A ByteBuffer is of course a buffer for bytes. Other subclasses of Buffer include CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, and ShortBuffer.

A buffer has a capacity, which is the number of elements it contains. It also employs an internal pointer to indicate the next element to read or write. An easy way to create a ByteBuffer is by calling the static allocate method of the ByteBuffer class:

public static ByteBuffer allocate(int capacity)

For example, to create a ByteBuffer with a capacity of 100, you would write

ByteBuffer byteBuffer = ByteBuffer.allocate(100);

As you may suspect, a ByteBuffer is backed by a byte array. To retrieve this array, call the array method of ByteBuffer:

public final byte[] array()

The length of the array is the same as the ByteBuffer's capacity.

ByteBuffer provides two put methods for writing a byte:


public abstract ByteBuffer put(byte b)

public abstract ByteBuffer put(int index, byte b)

The first put method writes on the element pointed by the ByteBuffer's internal pointer. The second allows you to put a byte anywhere by specifying an index.

There are also two put methods for writing a byte array. The first one allows the content of a byte array or a subset of it to be copied to the ByteBuffer. It has this signature:

public ByteBuffer put(byte[] src, int offset, int length)

The src argument is the source byte array, offset is the location of the first byte in src, and length is the number of bytes to be copied.

The second put method puts the whole source byte array to be copied from position 0:

public ByteBuffer put(byte[] src)

ByteBuffer also provides various putXXX methods for writing different data types to the buffer. The putInt method, for example, writes an int whereas putShort puts a short. There are two versions of putXXX, one for putting a value at the next location pointed by the ByteBuffer's internal pointer, one for putting a value at an absolute position. The signatures of the putInt methods are as follows.


public abstract ByteBuffer putInt(int value)

public abstract ByteBuffer putInt(int index, int value)

To read from a ByteBuffer, the ByteBuffer class provides a number of get and getXXX methods, which come in two flavors: one for reading from the relative position and one for reading from an absolute element. Here are the signatures of some of the get and getXXX methods:


public abstract byte get()

public abstract byte get(int index)

public abstract float getFloat()

public abstract float getFloat(int index)

Okay. That's all you need to know about ByteBuffer, and now you are ready for SeekableByteChannel. Listing 13.8 shows how to use SeekableByteChannel.

Listing 13.8: Random access file


package app13;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class SeekableByteChannelTest {

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(12);
        System.out.println(buffer.position()); // prints 0
        buffer.putInt(10);
        System.out.println(buffer.position()); // prints 8
        buffer.putLong(1234567890L);
        System.out.println(buffer.position()); // prints 16
        buffer.rewind(); // sets position to 0
        System.out.println(buffer.getInt()); // prints 10000
        System.out.println(buffer.getLong()); // prints 1234567890
        buffer.rewind();
        System.out.println(buffer.position()); // prints 0

        Path path = Paths.get("C:/temp/channel");
        System.out.println("-------------------------");
        try (SeekableByteChannel byteChannel =
                Files.newByteChannel(path,
                    StandardOpenOption.CREATE,
                    StandardOpenOption.READ,
                    StandardOpenOption.WRITE);) {
            System.out.println(byteChannel.position()); // prints 0
            byteChannel.write(buffer);
            System.out.println(byteChannel.position()); //prints 20

            // read file
            ByteBuffer buffer3 = ByteBuffer.allocate(40);
            byteChannel.position(0);
            byteChannel.read(buffer3);
            buffer3.rewind();
            System.out.println("get int:" + buffer3.getInt());
            System.out.println("get long:" + buffer3.getLong());
            System.out.println(buffer3.getChar());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

The SeekableByteChannelTest class in Listing 13.8 starts by creating a ByteBuffer with a capacity of twelve and putting an int and a long in it. Remember that an int is four bytes long and a long takes 8 bytes.


        ByteBuffer buffer = ByteBuffer.allocate(12);
        buffer.putInt(10);
        buffer.putLong(1234567890L);

After receiving an int and a long, the buffer's position is at 16.

        System.out.println(buffer.position()); // prints 16

The class then creates a SeekableByteChannel and calls its write method, passing the ByteBuffer.


Path path = Paths.get("C:/temp/channel");
try (SeekableByteChannel byteChannel =
        Files.newByteChannel(path,
            StandardOpenOption.CREATE,
            StandardOpenOption.READ,
            StandardOpenOption.WRITE);) {
    byteChannel.write(buffer);

It then reads the file back and prints the results to the console.


// read file
ByteBuffer buffer3 = ByteBuffer.allocate(40);
byteChannel.position(0);
byteChannel.read(buffer3);
buffer3.rewind();
System.out.println("get int:" + buffer3.getInt());
System.out.println("get long:" + buffer3.getLong());
System.out.println(buffer3.getChar());

Object Serialization

Occasionally you need to persist objects into permanent storage so that the states of the objects can be retained and later retrieved. Java supports this through object serialization. To serialize an object, i.e. save it to permanent storage, you use an ObjectOutputStream. To deserialize an object, namely to retrieve the saved object, use ObjectInputStream. ObjectOutputStream is a subclass of OutputStream and ObjectInputStream is derived from InputStream.

The ObjectOutputStream class has one public constructor:

public ObjectOutputStream(OutputStream out)

After you create an ObjectOutputStream, you can serialize objects or primitives or the combination of both. The ObjectOutputStream class provides a writeXXX method for each individual type, where XXX denotes a type. Here is the list of the writeXXX methods.


public void writeBoolean(boolean value)

public void writeByte(int value)

public void writeBytes(String value)

public void writeChar(int value)

public void writeChars(String value)

public void writeDouble(double value)

public void writeFloat(float value)

public void writeInt(int value)

public void writeLong(long value)

public void writeShort(short value)

public void writeObject(java.lang.Object value)

For objects to be serializable their classes must implement java.io.Serializable. This interface has no method and is a marker interface. A marker interface is one that tells the JVM that an instance of an implementing class belongs to a certain type.

If a serialized object contains other objects, the contained objects’ classes must also implement Serializable for the contained objects to be serializable.

The ObjectInputStream class has one public constructor:

public ObjectInputStream(InputStream in)

To deserialize from a file, you can pass a InputStream that is connected to a file sink. The ObjectInputStream class has methods that are the opposites of the writeXXX methods in ObjectOutputStream. They are as follows:


public boolean readBoolean()

public byte readByte()

public char readChar()

public double readDouble()

public float readFloat()

public int readInt()

public long readLong()

public short readShort()

public java.lang.Object readObject()

One important thing to note: object serialization is based on a last in first out method. When deserializing multiple primitives/objects, the objects that were serialized first must be deserialized last.

Listing 13.10 shows a class that serializes an int and a Customer object. Note that the Customer class, given in Listing 13.9, implements Serializable.

Listing 13.9: The Customer class


package app13;
import java.io.Serializable;

public class Customer implements Serializable {
    public int id;
    public String name;
    public String address;
    public Customer (int id, String name, String address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }
}

Listing 13.10: Object serialization example


package app13;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class ObjectSerializationTest {

    public static void main(String[] args) {
        // Serialize
        Path path = Paths.get("C:\\temp\\objectOutput");
        Customer customer = new Customer(1, "Joe Blog",
                "12 West Cost");
        try (OutputStream outputStream =
                Files.newOutputStream(path,
                        StandardOpenOption.CREATE);
            ObjectOutputStream oos = new
                    ObjectOutputStream(outputStream)) {
            // write first object
            oos.writeObject(customer);
            // write second object
            oos.writeObject("Customer Info");
        } catch (IOException e) {
            System.out.print("IOException");
        }

        // Deserialize
        try (InputStream inputStream = Files.newInputStream(path,
                StandardOpenOption.READ);
            ObjectInputStream ois = new
                    ObjectInputStream(inputStream)) {
            // read first object
            Customer customer2 = (Customer) ois.readObject();
            System.out.println("First Object: ");
            System.out.println(customer2.id);
            System.out.println(customer2.name);
            System.out.println(customer2.address);

            // read second object
            System.out.println();
            System.out.println("Second object: ");
            String info = (String) ois.readObject();
            System.out.println(info);
        } catch (ClassNotFoundException ex) { // readObject still
      throws this exception
            System.out.print("ClassNotFound " + ex.getMessage());
        } catch (IOException ex2) {
            System.out.print("IOException " + ex2.getMessage());
        }
    }
}

s

Summary

Input/output operations are supported through the members of the java.io package. You can read and write data through streams and data is classified into binary data and text. In addition, Java support object serialization through the Serializable interface and the ObjectInputStream and ObjectOutputStream classes.

Questions

1. What is a stream?

2. Name four abstract classes that represent streams in the java.io package.

3. What is object serialization?

4. What is the requirement for a class to be serializable?

About Sudhir Kumar

This is a short description in the author block about the author. You edit it by entering text in the "Biographical Info" field in the user admin panel.

0 σχόλια :

Post a Comment