THIS chapter covers the Java platform classes used for basic I/O. It focuses primarily on I/O streams, a powerful concept that greatly simplifies I/O operations. The chapter also looks at serialization, which lets a program write whole objects out to streams and read them back again. Then the chapter looks at some file system operations, including random access files. Finally, it touches briefly on the advanced features of the New I/O API. Most of the classes covered are in the java.io
package.
Some I/O operations are subject to approval by the current security manager. The example programs contained in these chapters are standalone applications, which by default have no security manager. To work in an applet, most of these examples would have to be modified. See the Security Restrictions section (page 578) for information about the security restrictions placed on applets.
An I/O stream represents an input source or an output destination. A stream can represent many different kinds of sources and destinations, including disk files, devices, other programs, and memory arrays.
Streams support many different kinds of data, including simple bytes, primitive data types, localized characters, and objects. Some streams simply pass on data; others manipulate and transform the data in useful ways.
No matter how they work internally, all streams present the same simple model to programs that use them: A stream is a sequence of data. A program uses an input stream to read data from a source, one item at a time (Figure 10.1).
A program uses an output stream to write data to a destination, one item at time (Figure 10.2).
In this chapter, we’ll see streams that can handle all kinds of data, from primitive values to advanced objects.
The data source and data destination pictured in Figure 10.2 can be anything that holds, generates, or consumes data. Obviously this includes disk files, but a source or destination can also another program, a peripheral device, a network socket, or an array.
In the next section, we’ll use the most basic kind of streams, byte streams, to demonstrate the common operations of stream I/O. For sample input, we’ll use the example file, xanadu.txt
,[1] which contains the following verse:
In Xanadu did Kubla Khan A stately pleasure-dome decree: Where Alph, the sacred river, ran Through caverns measureless to man Down to a sunless sea.
Programs use byte streams to perform input and output of 8-bit bytes. All byte stream classes are descended from InputStream
[2] and OutputStream
.[3]
There are many byte stream classes. To demonstrate how byte streams work, we’ll focus on the file I/O byte streams, FileInputStream
[4] and FileOutputStream
[5]. Other kinds of byte streams are used in much the same way; they differ mainly in the way they are constructed.
We’ll explore FileInputStream
and FileOutputStream
by examining an example program named CopyBytes
,[6] which uses byte streams to copy xanadu.txt
:
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class CopyBytes { public static void main(String[] args) throws IOException { FileInputStream in = null; FileOutputStream out = null; try { in = new FileInputStream("xanadu.txt"); out = new FileOutputStream("outagain.txt"); int c; while ((c = in.read()) != -1) { out.write(c); } } finally { if (in != null) { in.close(); } if (out != null) { out.close(); } } } }
CopyBytes
spends most of its time in a simple loop that reads the input stream and writes the output stream, one byte at a time, as shown in Figure 10.3.
Notice that read()
returns an int
value. If the input is a stream of bytes, why doesn’t read()
return a byte
value? Using an int
as a return type allows read()
to use -1
to indicate that it has reached the end of the stream.
Closing a stream when it’s no longer needed is very important—so important that CopyBytes
uses a finally
block to guarantee that both streams will be closed even if an error occurs. This practice helps avoid resource leaks.
One possible error is that CopyBytes
was unable to open one or both files. When that happens, the stream variable corresponding to the file never changes from its initial null
value. That’s why CopyBytes
makes sure that each stream variable contains an object reference before invoking close
.
CopyBytes
seems like a normal program, but it actually represents a kind of low-level I/O that you should avoid. Since xanadu.txt
contains character data, the best approach is to use character streams, as discussed in the next section. There are also streams for more complicated data types. Byte streams should only be used for the most primitive I/O.
So why talk about byte streams? Because all other stream types are built on byte streams.
The Java platform stores character values using Unicode conventions. Character stream I/O automatically translates this internal format to and from the local character set. In Western locales, the local character set is usually an 8-bit superset of ASCII.
For most applications, I/O with character streams is no more complicated than I/O with byte streams. Input and output done with stream classes automatically translates to and from the local character set. A program that uses character streams in place of byte streams automatically adapts to the local character set and is ready for internationalization—all without extra effort by the programmer.
If internationalization isn’t a priority, you can simply use the character stream classes without paying much attention to character set issues. Later, if internationalization becomes a priority, your program can be adapted without extensive recoding. See the online lesson on internationalization.[7]
All character stream classes are descended from Reader
[8] and Writer
.[9] As with byte streams, there are character stream classes that specialize in file I/O: FileReader
[10] and FileWriter
.[11] The CopyCharacters
[12] example illustrates these classes:
import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; public class CopyCharacters { public static void main(String[] args) throws IOException { FileReader inputStream = null; FileWriter outputStream = null; try { inputStream = new FileReader("xanadu.txt"); outputStream = new FileWriter("characteroutput.txt"); int c; while ((c = inputStream.read()) != -1) { outputStream.write(c); } } finally { if (inputStream != null) { inputStream.close(); } if (outputStream != null) { outputStream.close(); } } } }
CopyCharacters
is very similar to CopyBytes
. The most important difference is that CopyCharacters
uses FileReader
and FileWriter
for input and output in place of FileInputStream
and FileOutputStream
. Notice that both CopyBytes
and CopyCharacters
use an int
variable to read to and write from. However, in CopyCharacters
, the int
variable holds a character value in its last 16 bits; in CopyBytes
, the int
variable holds a byte
value in its last 8 bits.
Character streams are often “wrappers” for byte streams. The character stream uses the byte stream to perform the physical I/O, while the character stream handles translation between characters and bytes. FileReader
, for example, uses FileInputStream
, while FileWriter
uses FileOutputStream
.
There are two general-purpose byte-to-character “bridge” streams: InputStream-Reader
[13] and OutputStreamWriter
.[14] Use them to create character streams when there are no prepackaged character stream classes that meet your needs. For an example that creates character streams from network byte streams, refer to the online sockets lesson.[15]
Character I/O usually occurs in bigger units than single characters. One common unit is the line: a string of characters with a line terminator at the end. A line terminator can be a carriage-return/line-feed sequence ("
"
), a single carriage-return ("
"
), or a single line-feed ("
"
). Supporting all possible line terminators allows programs to read text files created on any of the widely used operating systems.
Let’s modify the CopyCharacters
example to use line-oriented I/O. To do this, we have to use two classes we haven’t seen before, BufferedReader
[16] and PrintWriter
.[17] We’ll explore these classes in greater depth in the Buffered Streams (page 269) and the Formatting (page 272) sections. Right now, we’re just interested in their support for line-oriented I/O.
The CopyLines
[18] example invokes BufferedReader.readLine
and PrintWriter.println
to do input and output one line at a time:
import java.io.FileReader; import java.io.FileWriter; import java.io.BufferedReader; import java.io.PrintWriter; import java.io.IOException; public class CopyLines { public static void main(String[] args) throws IOException { BufferedReader inputStream = null; PrintWriter outputStream = null; try { inputStream = new BufferedReader(new FileReader("xanadu.txt")); outputStream = new PrintWriter(new FileWriter("characteroutput.txt")); String l; while ((l = inputStream.readLine()) != null) { outputStream.println(l); } } finally { if (inputStream != null) { inputStream.close(); } if (outputStream != null) { outputStream.close(); } } } }
Invoking readLine
returns a line of text with the line terminator removed. CopyLines
outputs each line using println
, which appends the line terminator for the current operating system. This might not be the same line terminator that was used in the input file.
There are many ways to structure text input and output beyond characters and lines. For more information, see the Scanning and Formatting section (page 270).
Most of the examples we’ve seen so far use unbuffered I/O. This means that each read or write request is handled directly by the underlying OS. This can make a program much less efficient, since each such request often triggers disk access, network activity, or some other operation that is relatively expensive.
To reduce this kind of overhead, the Java platform implements buffered I/O streams. Buffered input streams read data from a memory area known as a buffer; the native input API is called only when the buffer is empty. Similarly, buffered output streams write data to a buffer, and the native output API is called only when the buffer is full.
A program can convert an unbuffered stream into a buffered stream using the wrapping idiom we’ve used several times now, where the unbuffered stream object is passed to the constructor for a buffered stream class. Here’s how you might modify the constructor invocations in the CopyCharacters
example to use buffered I/O:
inputStream = new BufferedReader(new FileReader("xanadu.txt")); outputStream = new BufferedWriter(new FileWriter("characteroutput.txt"));
There are four buffered stream classes used to wrap unbuffered streams: BufferedInputStream
[19] and BufferedOutputStream
[20] create buffered byte streams, while BufferedReader
and BufferedWriter
[21] create buffered character streams.
It often makes sense to write out a buffer at critical points, without waiting for it to fill. This is known as flushing the buffer.
Some buffered output classes support autoflush, specified by an optional constructor argument. When autoflush is enabled, certain key events cause the buffer to be flushed. For example, an autoflush PrintWriter
object flushes the buffer on every invocation of println
or format
. See the Formatting section (page 272) for more on these methods.
To flush a stream manually, invoke its flush
method. The flush
method is valid on any output stream, but has no effect unless the stream is buffered.
Programming I/O often involves translating to and from the neatly formatted data humans like to work with. To assist you with these chores, the Java platform provides two APIs. The scanner API breaks input into individual tokens associated with bits of data. The formatting API assembles data into nicely formatted, human-readable form.
Objects of type Scanner
[22] are useful for breaking down formatted input into tokens and translating individual tokens according to their data types.
By default, a scanner uses white space to separate tokens. (White space characters include blanks, tabs, and line terminators. For the full list, refer to the documentation for Character.isWhitespace
.[23]) To see how scanning works, let’s look at ScanXan
, a program that reads the individual words in xanadu.txt
and prints them out, one per line:
import java.io.*; import java.util.Scanner; public class ScanXan { public static void main(String[] args) throws IOException { Scanner s = null; try { s = new Scanner(new BufferedReader(new FileReader("xanadu.txt"))); while (s.hasNext()) { System.out.println(s.next()); } } finally { if (s != null) { s.close(); } } } }
Notice that ScanXan
invokes Scanner
’s close
method when it is done with the scanner object. Even though a scanner is not a stream, you need to close it to indicate that you’re done with its underlying stream.
The output of ScanXan
looks like this:
In Xanadu did Kubla Khan A stately pleasure-dome ...
To use a different token separator, invoke useDelimiter()
, specifying a regular expression. For example, suppose you wanted the token separator to be a comma, optionally followed by white space. You would invoke:
s.useDelimiter(",\s*");
The ScanXan
example treats all input tokens as simple String
values. Scanner
also supports tokens for all of the Java language’s primitive types (except for char
), as well as BigInteger
and BigDecimal
. Also, numeric values can use thousands separators. Thus, in a US
locale, Scanner
correctly reads the string "32,767"
as representing an integer value.
We have to mention the locale, because thousands of separators and decimal symbols are locale specific. So, the following example would not work correctly in all locales if we didn’t specify that the scanner should use the US
locale. That’s not something you usually have to worry about, because your input data usually comes from sources that use the same locale as you do. But this example is part of the Java Tutorial and gets distributed all over the world.
The ScanSum
[24] example reads a list of double
values and adds them up. Here’s the source:
import java.io.FileReader; import java.io.BufferedReader; import java.io.IOException; import java.util.Scanner; import java.util.Locale; public class ScanSum { public static void main(String[] args) throws IOException { Scanner s = null; double sum = 0; try { s = new Scanner(new BufferedReader(new FileReader("usnumbers.txt"))); s.useLocale(Locale.US); while (s.hasNext()) { if (s.hasNextDouble()) { sum += s.nextDouble(); } else { s.next(); } } } finally { s.close(); } System.out.println(sum); } }
And here’s the sample input file, usnumbers.txt
:[25]
8.5 32,767 3.14159 1,000,000.1
The output string is “1032778.74159
”. The period will be a different character in some locales, because System.out
is a PrintStream
object, and that class doesn’t provide a way to override the default locale. We could override the locale for the whole program—or we could just use formatting, as described in the next section.
Stream objects that implement formatting are instances of either PrintWriter
, a character stream class, or PrintStream
,[26] a byte stream class.
The only PrintStream
objects you are likely to need are are System.out
and System.err
.[27] See the I/O from the Command Line section (page 276) for more on these objects. When you need to create a formatted output stream, instantiate PrintWriter
, not PrintStream
.
Like all byte and character stream objects, instances of PrintStream
and PrintWriter
implement a standard set of write
methods for simple byte and character output. In addition, both PrintStream
and PrintWriter
implement the same set of methods for converting internal data into formatted output. Two levels of formatting are provided:
print
and println
format individual values in a standard way.
format
formats almost any number of values based on a format string, with many options for precise formatting.
Invoking print
or println
outputs a single value after converting the value using the appropriate toString
method. We can see this in the Root
[28] example:
public class Root { public static void main(String[] args) { int i = 2; double r = Math.sqrt(i); System.out.print("The square root of "); System.out.print(i); System.out.print(" is "); System.out.print(r); System.out.println("."); i = 5; r = Math.sqrt(i); System.out.println("The square root of " + i + " is " + r + "."); } }
Here is the output of Root
:
The square root of 2 is 1.4142135623730951. The square root of 5 is 2.23606797749979.
The i
and r
variables are formatted twice: the first time using code in an overload of print
, the second time by conversion code automatically generated by the Java compiler, which also utilizes toString
. You can format any value this way, but you don’t have much control over the results.
The format
method formats multiple arguments based on a format string. The format string consists of static text embedded with format specifiers; except for the format specifiers, the format string is output unchanged.
Format strings support many features. In this tutorial, we’ll just cover some basics. For a complete description, see Format String Syntax in the API specification.[29]
The Root2
[30] example formats two values with a single format
invocation:
public class Root2 { public static void main(String[] args) { int i = 2; double r = Math.sqrt(i); System.out.format("The square root of %d is %f.%n", i, r); } }
Here is the output:
The square root of 2 is 1.414214.
Like the three used in this example, all format specifiers begin with a %
and end with a 1- or 2-character conversion that specifies the kind of formatted output being generated. The three conversions used here are:
d
formats an integer value as a decimal value.
f
formats a floating point value as a decimal value.
n
outputs a platform-specific line terminator.
Here are some other conversions:
x
formats an integer as a hexadecimal value.
s
formats any value as a string.
tB
formats an integer as a locale-specific month name.
There are many other conversions.
Except for %%
and %n
, all format specifiers must match an argument. If they don’t, an exception is thrown.
In the Java programming language, the
escape always generates the linefeed character (u000A
). Don’t use
unless you specifically want a linefeed character. To get the correct line separator for the local platform, use %n
.
In addition to the conversion, a format specifier can contain several additional elements that further customize the formatted output. Here’s an example, Format
,[31] that uses every possible kind of element:
public class Format { public static void main(String[] args) { System.out.format("%f, %1$+020.10f %n", Math.PI); } }
Here’s the output:
3.141593, +00000003.1415926536
The additional elements are all optional.
Figure 10.4 shows how the longer specifier breaks down into elements.
The elements must appear in the order shown. Working from the right, the optional elements are:
Precision. For floating point values, this is the mathematical precision of the formatted value. For s
and other general conversions, this is the maximum width of the formatted value; the value is right-truncated if necessary.
Width. The minimum width of the formatted value; the value is padded if necessary. By default the value is left-padded with blanks.
Flags. Specify additional formatting options. In the Format
example, the +
flag specifies that the number should always be formatted with a sign, and the 0
flag specifies that 0
is the padding character. Other flags include -
(pad on the right) and ,
(format number with locale-specific thousands separators). Note that some flags cannot be used with certain other flags or with certain conversions.
Argument Index. Allows you to explicitly match a designated argument. You can also specify <
to match the same argument as the previous specifier. Thus the example could have said:
System.out.format("%f, %<+020.10f %n", Math.PI);
A program is often run from the command line and interacts with the user in the command-line environment. The Java platform supports this kind of interaction in two ways: through the Standard Streams and through the Console.
Standard Streams are a feature of many operating systems. By default, they read input from the keyboard and write output to the display. They also support I/O on files and between programs, but that feature is controlled by the command line interpreter, not the program.
The Java platform supports three Standard Streams: Standard Input, accessed through System.in
; Standard Output, accessed through System.out
; and Standard Error, accessed through System.err
. These objects are defined automatically and do not need to be opened. Standard Output and Standard Error are both for output; having error output separately allows the user to divert regular output to a file and still be able to read error messages. For more information, refer to the documentation for your command-line interpreter.
You might expect the Standard Streams to be character streams, but, for historical reasons, they are byte streams. System.out
and System.err
are defined as PrintStream
objects. Although it is technically a byte stream, PrintStream
uses an internal character stream object to emulate many of the features of character streams.
By contrast, System.in
is a byte stream with no character stream features. To use Standard Input as a character stream, wrap System.in
in InputStreamReader
:
InputStreamReader cin = new InputStreamReader(System.in);
A more advanced alternative to the Standard Streams is the Console. This is a single, predefined object of type Console
[32] that has most of the features provided by the Standard Streams, and others besides. The Console is particularly useful for secure password entry. The Console object also provides input and output streams that are true character streams, through its reader
and writer
methods.
Before a program can use the Console, it must attempt to retrieve the Console object by invoking System.console()
. If the Console object is available, this method returns it. If System.console
returns NULL
, then Console operations are not permitted, either because the OS doesn’t support them or because the program was launched in a noninteractive environment.
The Console object supports secure password entry through its readPassword
method. This method helps secure password entry in two ways. First, it suppresses echoing, so the password is not visible on the user’s screen. Second, readPassword
returns a character array, not a String
, so the password can be overwritten, removing it from memory as soon as it is no longer needed.
The Password
[33] example is a prototype program for changing a user’s password. It demonstrates several Console
methods:
import java.io.Console; import java.util.Arrays; import java.io.IOException; public class Password { public static void main (String args[]) throws IOException { Console c = System.console(); if (c == null) { System.err.println("No console."); System.exit(1); } String login = c.readLine("Enter your login: "); char [] oldPassword = c.readPassword("Enter your " + old password: "); if (verify(login, oldPassword)) { boolean noMatch; do { char [] newPassword1 = c.readPassword("Enter your new password: "); char [] newPassword2 = c.readPassword("Enter new password again: "); noMatch = ! Arrays.equals(newPassword1, newPassword2); if (noMatch) { c.format("Passwords don't match. Try again.%n"); } else { change(login, newPassword1); c.format("Password for %s changed.%n", login); } Arrays.fill(newPassword1, ' '), Arrays.fill(newPassword2, ' '), } while (noMatch); } Arrays.fill(oldPassword, ' '), } // Dummy verify method. static boolean verify(String login, char[] password) { return true; } // Dummy change method. static void change(String login, char[] password) {} }
Attempt to retrieve the Console object. If the object is not available, abort.
Invoke Console.readLine
to prompt for and read the user’s login name.
Invoke Console.readPassword
to prompt for and read the user’s existing password.
Invoke verify
to confirm that the user is authorized to change the password. (In this example, verify
is a dummy method that always returns true
.)
Repeat the following steps until the user enters the same password twice:
Invoke Console.readPassword
twice to prompt for and read a new password.
If the user entered the same password both times, invoke change
to change it. (Again, change
is a dummy method.)
Overwrite both passwords with blanks.
Overwrite the old password with blanks.
Data streams support binary I/O of primitive data type values (boolean
, char
, byte
, short
, int
, long
, float
, and double
) as well as String
values. All data streams implement either the DataInput
[34] or the DataOutput
[35] interface. This section focuses on the most widely-used implementations of these interfaces, DataInputStream
[36] and DataOutputStream
.[37]
The following example demonstrates data streams by writing out a set of data records, and then reading them in again. Each record consists of three values related to an item on an invoice, as shown in Table 10.1.
Table 10.1. Fields in DataStreams
Example
Data Type | Data Description | Output Method | Input Method | Sample Value | |
---|---|---|---|---|---|
1 |
| Item price |
|
|
|
2 |
| Unit count |
|
|
|
3 |
| Item description |
|
|
|
The example is a long one, so we’ll present it in segments, separated by commentary. The program starts by defining some constants containing the name of the data file and the data that will be written to it:
public class DataStreams { static final String dataFile = "invoicedata"; static final double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 }; static final int[] units = { 12, 8, 13, 29, 50 }; static final String[] descs = { "Java T-shirt", "Java Mug", "Duke Juggling Dolls", "Java Pin", "Java Key Chain" };
Then DataStreams
opens an output stream. Since a DataOutputStream
can only be created as a wrapper for an existing byte stream object, DataStreams
provides a buffered file output byte stream:
public static void main(String[] args) throws IOException { DataOutputStream out = null; try { out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(dataFile)));
DataStreams
writes out the records and closes the output stream:
for (int i = 0; i < prices.length; i ++) { out.writeDouble(prices[i]); out.writeInt(units[i]); out.writeUTF(descs[i]); } } finally { out.close(); }
The writeUTF
method writes out String
values in a modified form of UTF-8. This is a variable-width character encoding that only needs a single byte for common Western characters.
Now DataStreams
reads the data back in again. First it must provide an input stream and variables to hold the input data. Like DataOutputStream
, DataInputStream
must be constructed as a wrapper for a byte stream:
DataInputStream in = null; double total = 0.0; try { in = new DataInputStream(new BufferedInputStream(new FileInputStream(dataFile))); double price; int unit; String desc;
Now DataStreams
can read each record in the stream, reporting on the data it encounters:
try { while (true) { price = in.readDouble(); unit = in.readInt(); desc = in.readUTF(); System.out.format("You ordered %d units of %s " + "at $%.2f%n", unit, desc, price); total += unit * price; } } catch (EOFException e) { } System.out.format("For a TOTAL of: $%.2f%n", total); } finally { in.close(); } } }
Notice that DataStreams
detects an end-of-file condition by catching EOFException
,[38] instead of testing for an invalid return value. All implementations of DataInput
methods use EOFException
instead of return values.
Also notice that each specialized write
in DataStreams
is exactly matched by the corresponding specialized read
. It is up to the programmer to make sure that output types and input types are matched in this way: The input stream consists of simple binary data, with nothing to indicate the type of individual values or where they begin in the stream.
DataStreams
uses one very bad programming technique: It uses floating point numbers to represent monetary values. In general, floating point is bad for precise values. It’s particularly bad for decimal fractions, because common values (such as 0.1
) do not have a binary representation.
The correct type to use for currency values is java.math.BigDecimal
.[39] Unfortunately, BigDecimal
is an object type, so it won’t work with data streams. However, BigDecimal
will work with object streams, which are covered in the next section.
Just as data streams support I/O of primitive data types, object streams support I/O of objects. Most, but not all, standard classes support serialization of their objects. Those that do implement the marker interface Serializable
.[40]
The object stream classes are ObjectInputStream
[41] and ObjectOutputStream
.[42] These classes implement ObjectInput
[43] and ObjectOutput
,[44] which are subinterfaces of DataInput
and DataOutput
. That means that all the primitive data I/O methods covered in the Data Streams section (page 279) are also implemented in object streams. So an object stream can contain a mixture of primitive and object values. The ObjectStreams
[45] example illustrates this:
import java.io.*; import java.math.BigDecimal; import java.util.Calendar; public class ObjectStreams { static final String dataFile = "invoicedata"; static final BigDecimal[] prices = { new BigDecimal("19.99"), new BigDecimal("9.99"), new BigDecimal("15.99"), new BigDecimal("3.99"), new BigDecimal("4.99") }; static final int[] units = { 12, 8, 13, 29, 50 }; static final String[] descs = { "Java T-shirt", "Java Mug", "Duke Juggling Dolls", "Java Pin", "Java Key Chain" }; public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream out = null; try { out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(dataFile))); out.writeObject(Calendar.getInstance()); for (int i = 0; i < prices.length; i ++) { out.writeObject(prices[i]); out.writeInt(units[i]); out.writeUTF(descs[i]); } } finally { out.close(); } ObjectInputStream in = null; try { in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(dataFile))); Calendar date = null; BigDecimal price; int unit; String desc; BigDecimal total = new BigDecimal(0); date = (Calendar) in.readObject(); System.out.format ("On %tA, %<tB %<te, %<tY:%n", date); try { while (true) { price = (BigDecimal) in.readObject(); unit = in.readInt(); desc = in.readUTF(); System.out.format("You ordered %d units of %s " + "at $%.2f%n", unit, desc, price); total = total.add(price.multiply(new BigDecimal(unit))); } } catch (EOFException e) {} System.out.format("For a TOTAL of: $%.2f%n", total); } finally { in.close(); } } }
ObjectStreams
creates the same application as DataStreams
, with a couple of changes. First, prices are now BigDecimal
objects. Second, a Calendar
[46] object is written to the data file, indicating an invoice date.
If readObject()
doesn’t return the object type expected, attempting to cast it to the correct type may throw a ClassNotFoundException
.[47] In this simple example, that can’t happen, so we don’t try to catch the exception. Instead, we notify the compiler that we’re aware of the issue by adding ClassNotFoundException
to the main
method’s throws
clause.
The writeObject
and readObject
methods are simple to use, but they contain some very sophisticated object management logic. This isn’t important for a class like Calendar
, which just encapsulates primitive values. But many objects contain references to other objects. If readObject
is to reconstitute an object from a stream, it has to be able to reconstitute all of the objects the original object referred to. These additional objects might have their own references, and so on. In this situation, writeObject
traverses the entire web of object references and writes all objects in that web onto the stream. Thus a single invocation of writeObject
can cause a large number of objects to be written to the stream.
This is demonstrated in Figure 10.5, where writeObject
is invoked to write a single object named a. This object contains references to objects b and c, while b contains references to d and e. Invoking writeObject(a)
writes not just a, but all the objects necessary to reconstitute a, so the other four objects in this web are written as well. When a is read back by readObject
, the other four objects are read back also, and all the original object references are preserved.
You might wonder what happens if two objects on the same stream both contain references to a single object. Will they both refer to a single object when they’re read back? The answer is “yes.” A stream can only contain one copy of an object, though it can contain any number of references to it. Thus if you explicitly write an object to a stream twice, you’re really writing only the reference twice. For example, if the following code writes an object ob
twice to a stream:
Object ob = new Object(); out.writeObject(ob); out.writeObject(ob);
each writeObject
has to be matched by a readObject
, so the code that reads the stream back will look something like this:
Object ob1 = in.readObject(); Object ob2 = in.readObject();
This results in two variables, ob1
and ob2
, that are references to a single object.
However, if a single object is written to two different streams, it is effectively duplicated—a single program reading both streams back will see two distinct objects.
So far, this chapter has focused on streams, which provide a simple model for reading and writing data. Streams work with a large variety of data sources and destinations, including disk files. However, streams don’t support all the operations that are common with disk files. In this part of the chapter, we’ll focus on non-stream file I/O. There are two topics:
File
is a class that helps you write platform-independent code that examines and manipulates files and directories.
Random access files support nonsequential access to disk file data.
The File
[48] class makes it easier to write platform-independent code that examines and manipulates files. The name of this class is misleading: File
instances represent file names, not files. The file corresponding to the file name might not even exist.
Why create a File
object for a file that doesn’t exist? A program can use the object to parse a file name. Also, the file can be created by passing the File
object to the constructor of some classes, such as FileWriter
.
If the file does exist, a program can examine its attributes and perform various operations on the file, such as renaming it, deleting it, or changing its permissions.
A File
object contains the file name string used to construct it. That string never changes throughout the lifetime of the object. A program can use the File
object to obtain other versions of the file name, some of which may or may not be the same as the original file name string passed to the constructor.
Suppose a program creates a File
object with the constructor invocation:
File a = new File("xanadu.txt");
The program invokes a number of methods to obtain different versions of the file name. The program is then run both on a Microsoft Windows system (in directory c:javaexamples
) and a Solaris system (in directory /home/cafe/java/examples
). Table 10.2 shows what the methods would return.
Table 10.2. File Method Examples
Returns on Microsoft Windows | Returns on Solaris | |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Then the same program constructs a File
object from a more complicated file name, using File.separator
to specify the file name in a platform-independent way:
File b = new File(".." + File.separator + "examples" + File.separator + "xanadu.txt");
Although b
refers to the same file as a
, the methods return slightly different values, as shown in Table 10.3.
Table 10.3. More File Method Examples
Method Invoked | Returns on Microsoft Windows | Returns on Solaris |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Running the same program on a Linux system would give results similar to those on the Solaris system.
It’s worth mentioning that File.compareTo()
would not consider a
and b
to be the same. Even though they refer to the same file, the names used to construct them are different.
The FileStuff
[49] example creates File
objects from names passed from the command line and exercises various information methods on them. You’ll find it instructive to run FileStuff
on a variety of file names. Be sure to include directory names as well as the names of files that don’t actually exist. Try passing FileStuff
a variety of relative and absolute path names:
import java.io.File; import java.io.IOException; import static java.lang.System.out; public class FileStuff { public static void main(String args[]) throws IOException { out.print("File system roots: "); for (File root : File.listRoots()) { out.format("%s ", root); } out.println(); for (String fileName : args) { out.format("%n------ %nnew File(%s)%n", fileName); File f = new File(fileName); out.format("toString(): %s%n", f); out.format("exists(): %b%n", f.exists()); out.format("lastModified(): %tc%n", f.lastModified()); out.format("isFile(): %b%n", f.isFile()); out.format("isDirectory(): %b%n", f.isDirectory()); out.format("isHidden(): %b%n", f.isHidden()); out.format("canRead(): %b%n", f.canRead()); out.format("canWrite(): %b%n", f.canWrite()); out.format("canExecute(): %b%n", f.canExecute()); out.format("isAbsolute(): %b%n", f.isAbsolute()); out.format("length(): %d%n", f.length()); out.format("getName(): %s%n", f.getName()); out.format("getPath(): %s%n", f.getPath()); out.format("getAbsolutePath(): %s%n", f.getAbsolutePath()); out.format("getCanonicalPath(): %s%n", f.getCanonicalPath()); out.format("getParent(): %s%n", f.getParent()); out.format("toURI: %s%n", f.toURI()); } } }
If a File
object names an actual file, a program can use it to perform a number of useful operations on the file. These include passing the object to the constructor for a stream to open the file for reading or writing.
The delete
method deletes the file immediately, while the deleteOnExit
method deletes the file when the virtual machine terminates.
The setLastModified
sets the modification date/time for the file. For example, to set the modification time of xanadu.txt
to the current time, a program could do:
new File("xanadu.txt").setLastModified(new Date().getTime());
The renameTo()
method renames the file. Note that the file name string behind the File
object remains unchanged, so the File
object will not refer to the renamed file.
File
has some useful methods for working with directories.
The mkdir
method creates a directory. The mkdirs
method does the same thing, after first creating any parent directories that don’t yet exist.
The list
and listFiles
methods list the contents of a directory. The list
method returns an array of String
file names, while listFiles
returns an array of File
objects.
File
contains some useful static methods.
The createTempFile
method creates a new file with an unique name and returns a File
object referring to it.
The listRoots
returns a list of file system root names. On Microsoft Windows, this will be the root directories of mounted drives, such as a:
and c:
. On UNIX and Linux systems, this will be the root directory, /
.
Random access files permit nonsequential, or random, access to a file’s contents.
Consider the archive format known as ZIP. A ZIP archive contains files and is typically compressed to save space. It also contain a directory entry at the end that indicates where the various files contained within the ZIP archive begin, as shown in Figure 10.6.
Suppose that you want to extract a specific file from a ZIP archive. If you use a sequential access stream, you have to:
Open the ZIP archive.
Search through the ZIP archive until you locate the file you want to extract.
Extract the file.
Close the ZIP archive.
Using this procedure, on average, you’d have to read half the ZIP archive before finding the file that you want to extract. You can extract the same file from the ZIP archive more efficiently by using the seek feature of a random access file and following these steps:
Open the ZIP archive.
Seek to the directory entry and locate the entry for the file you want to extract from the ZIP archive.
Seek (backward) within the ZIP archive to the position of the file to extract.
Extract the file.
Close the ZIP archive.
This algorithm is more efficient because you read only the directory entry and the file that you want to extract.
The java.io.RandomAccessFile
[50] class implements both the DataInput
and DataOutput
interfaces and therefore can be used for both reading and writing. RandomAccessFile
is similar to FileInputStream
and FileOutputStream
in that you specify a file on the native file system to open when you create it. When you create a RandomAccessFile
, you must indicate whether you will be just reading the file or writing to it also. (You have to be able to read a file in order to write it.) The following code creates a RandomAccessFile
to read the file named farrago.txt
:
new RandomAccessFile("xanadu.txt", "r");
And this one opens the same file for both reading and writing:
new RandomAccessFile("xanadu.txt", "rw");
After the file has been opened, you can use the common read
or write
methods defined in the DataInput
and DataOutput
interfaces to perform I/O on the file.
RandomAccessFile
supports the notion of a file pointer (Figure 10.7). The file pointer indicates the current location in the file. When the file is first created, the file pointer is set to 0, indicating the beginning of the file. Calls to the read
and write
methods adjust the file pointer by the number of bytes read or written.
In addition to the normal file I/O methods that implicitly move the file pointer when the operation occurs, RandomAccessFile
contains three methods for explicitly manipulating the file pointer.
int skipBytes(int)
. Moves the file pointer forward the specified number of bytes.
void seek(long)
. Positions the file pointer just before the specified byte.
long getFilePointer()
. Returns the current byte location of the file pointer.
This chapter has mostly talked about the java.io
package. This package provides all the I/O features most programmers will ever need. The package implements basic I/O services—data streams, random access files, character translation, and buffering—with a simple and easy-to-use API.
However, some programmers of high-performance applications will need more flexibility than the java.io
package supplies. They’ll find it in the java.nio.*
packages. These packages provide APIs for scalable I/O, fast buffered byte and character I/O, and character set conversion.
The java.io
package contains many classes that your programs can use to read and write data. Most of the classes implement sequential access streams. The sequential access streams can be divided into two groups: those that read and write bytes, and those that read and write Unicode characters. Each sequential access stream has a speciality, such as reading from or writing to a file, filtering data as it’s read or written, or serializing an object.
One class, RandomAccessFile
, implements random input/output access to a file. An object of this type maintains a file pointer, which indicates the current location from which data will be read or to which data will be written.
1. | Implement a pair of classes, one |
2. | The file |
[1] tutorial/essential/io/examples/xanadu.txt
[2] docs/api/java/io/InputStream.html
[3] docs/api/java/io/OutputStream.html
[4] docs/api/java/io/FileInputStream.html
[5] docs/api/java/io/FileOutputStream.html
[6] tutorial/essential/io/examples/CopyBytes.java
[7] docs/books/tutorial/i18n/index.html
[8] docs/api/java/io/Reader.html
[9] docs/api/java/io/Writer.html
[10] docs/api/java/io/FileReader.html
[11] docs/api/java/io/FileWriter.html
[12] tutorial/essential/io/examples/CopyCharacters.java
[13] docs/api/java/io/InputStreamReader.html
[14] docs/api/java/io/OutputStreamWriter.html
[15] docs/books/tutorial/networking/sockets/readingWriting.html
[16] docs/api/java/io/BufferedReader.html
[17] docs/api/java/io/PrintWriter.html
[18] tutorial/essential/io/examples/CopyLines.java
[19] docs/api/java/io/BufferedInputStream.html
[20] docs/api/java/io/BufferedOutputStream.html
[21] docs/api/java/io/BufferedWriter.html
[22] docs/api/java/util/Scanner.html
[23] docs/api/java/lang/Character.html
[24] tutorial/essential/io/examples/ScanSum.java
[25] tutorial/essential/io/examples/usnumbers.txt
[26] docs/api/java/io/PrintStream.html
[27] docs/api/java/lang/System.html
[28] tutorial/essential/io/examples/Root.java
[29] docs/api/java/util/Formatter.html
[30] tutorial/essential/io/examples/Root2.java
[31] tutorial/essential/io/examples/Format.java
[32] docs/api/java/io/Console.html
[33] tutorial/essential/io/examples/Password.java
[34] docs/api/java/io/DataInput.html
[35] docs/api/java/io/DataOutput.html
[36] docs/api/java/io/DataInputStream.html
[37] docs/api/java/io/DataOutputStream.html
[38] docs/api/java/io/EOFException.html
[39] docs/api/java/math/BigDecimal.html
[40] docs/api/java/io/Serializable.html
[41] docs/api/java/io/ObjectInputStream.html
[42] docs/api/java/io/ObjectOutputStream.html
[43] docs/api/java/io/ObjectInput.html
[44] docs/api/java/io/ObjectOutput.html
[45] tutorial/essential/io/examples/ObjectStreams.java
[46] docs/api/java/util/Calendar.html
[47] docs/api/java/lang/ClassNotFoundException.html
[48] docs/api/java/io/File.html
[49] tutorial/essential/io/examples/FileStuff.java
[50] docs/api/java/io/RandomAccessFile.html
[51] tutorial/essential/io/QandE/datafile
18.116.81.162