Chapter 16
Input/Output
Input/output (I/O) is one of the most common operations performed by computer programs. Examples of I/O operations include
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 introduced 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 features in NIO.2 is the Path interface, which was 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 in JDK 7 and later 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 can still be found in applications written in older versions of Java.
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 file 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 a 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.
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.png file in your Mac’s /home/user directory as /home/user/image1.png, which is a path. A temp directory under your Windows’ C: drive is C: emp, which is also a path. Paths must be unique throughout the 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 path 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 brought 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 models a file system. It is an abstract class and its static method getDefault returns the current file system:
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: emp");
Path path4 = Paths.get("C:", "temp");
Paths like /home/user/images or C: emp can be broken into its elements. /home/user/images has three names, home, user, and images. C: emp 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 returns 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.
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.
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 the file 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 link 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 or a 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 link target. 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.
You can retrieve the files, subdirectories and symbolic links in a directory with the newDirectoryStream method of the Files class. This method returns a DirectoryStream to iterate over all objects in a directory. Here is the signature of newDirectoryStream.
public static DirectoryStream<Path> newDirectoryStream(Path
path
)
The returned DirectoryStream 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();
}
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:
As an example, the following code creates a copy of the C: empline1.bmp file in the same directory and names it backup.bmp.
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: empackup.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();
}
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 also takes a Charset. The OpenOption interface defines options for opening a file for write access. The StandardOpenOption enum implements OpenOption and provides these values.
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: empspeech.txt and read them back.
// write to and read from 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.
I/O streams can be likened to water pipes. Just like water pipes connect city houses to a water reservoir, a Java I/O 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.
The benefit of streams is they define methods for data reading and writing that can be used regardless of the data source or destination. To connect to a particular sink, you just 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.
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 16.1.
Figure 16.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 is 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 by 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 can employ 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 16.1 shows an InputStreamDemo1 class that contains a compareFiles method for comparing two files. You need to adjust the values of path1 and path2 and make sure the files exist before running this class.
Listing 16.1: The compareFiles method that uses InputStream
package app16;
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 InputStreamDemo1 {
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
");
InputStreamDemo1 test = new InputStreamDemo1();
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.
The OutputStream abstract class represents a stream for writing binary data to a sink. Its child classes are shown in Figure 16.2.
Figure 16.2: OutputStream implementation classes
In pre-7 JDKs 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 16.2 shows how to copy a file using OutputStream.
Listing 16.2: The OutputStreamDemo1 class
package app16;
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 OutputStreamDemo1 {
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
) {
OutputStreamDemo1 test = new OutputStreamDemo1();
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 byte array readData 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);
The abstract class Writer defines a stream used for writing characters. Figure 16.3 shows the implementations of Writer.
Figure 16.3: Writer subclasses
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.
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.
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 16.3 shows an example of OutputStreamWriter.
Listing 16.3: Using OutputStreamWriter
package app16;
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 OutputStreamWriterDemo1 {
public static void main(String[]
args
) {
char[] chars = new char[2];
chars[0] = 'u4F60'; // representing
?
chars[1] = 'u597D'; // representing
?
;
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 16.3 creates an OutputStreamWriter based on a OutputStream that writes to C: empmyFile.txt on Windows. Therefore, if you are using Linux or Mac OS X, 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 16.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 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 16.4 presents an example of PrintWriter.
Listing 16.4: Using PrintWriter
package app16;
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 PrintWriterDemo1 {
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
You use the Reader class to read text (characters, i.e. human readable data). The hierarchy of this class is shown in Figure 16.4.
Figure 16.4: Reader and its descendants
The following sections discuss Reader and some of its descendants.
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.
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 16.5 presents an example that uses a PrintWriter to write two Chinese characters and read them back.
Listing 16.5: Using InputStreamReader
package app16;
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 InputStreamReaderDemo1 {
public static void main(String[]
args
) {
Path textFile = Paths.get("C:\
temp
\myFile.txt");
Charset
chineseSimplifiedCharset =
Charset.forName("GB2312");
char[] chars = new char[2];
chars[0] = 'u4F60'; // representing ?
chars[1] = 'u597D'; // representing ?
// 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 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 16.6 shows a getUserInput method for taking user input on the console.
Listing 16.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.”
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 16.7 presents an example that redirects System.out to a file.
Listing 16.7: Redirecting System.out to a file
package app16;
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 PrintStreamDemo1 {
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.
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. 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 second 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 16.8 shows how to use SeekableByteChannel.
Listing 16.8: Random access file
package app16;
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 SeekableByteChannelDemo1 {
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 SeekableByteChannelDemo1 class in Listing 16.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());
Occasionally you need to persist objects into permanent storage so that the states of the objects can be retained and retrieved later. 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 16.10 shows a class that serializes an int and a Customer object. Note that the Customer class, given in Listing 16.9, implements Serializable. The serialization runtime associates with each serializable class a version number called serialVersionUID. This number is used during deserialization to verify that the sender and receiver of a serialized object have loaded classes for that object that are compatible with respect to serialization. All classes implementing Serializable should declare a static final long serialVersionUID field. Otherwise, one will be calculated by the serialization runtime automatically.
Listing 16.9: The Customer class
package app16;
import java.io.Serializable;
public class Customer implements Serializable {
private static final long serialVersionUID = 1L;
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 16.10: Object serialization example
package app16;
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 ObjectSerializationDemo1 {
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());
}
}
}
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.
18.217.207.23