Chapter 10. Basic I/O

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.

Security Consideration

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.

I/O Streams

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

Reading information into a program.

Figure 10.1. Reading information into a program.

A program uses an output stream to write data to a destination, one item at time (Figure 10.2).

Writing information from a program.

Figure 10.2. Writing information from a program.

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.

Byte Streams

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.

Using Byte Streams

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.

Simple byte stream input and output.

Figure 10.3. Simple byte stream input and output.

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.

Always Close Streams

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.

When Not to Use Byte Streams

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.

Character 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]

Using Character Streams

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 That Use Byte Streams

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]

Line-Oriented I/O

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

Buffered Streams

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.

Flushing Buffered 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.

Scanning and Formatting

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.

Scanning

Objects of type Scanner[22] are useful for breaking down formatted input into tokens and translating individual tokens according to their data types.

Breaking Input into Tokens

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*");

Translating Individual Tokens

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.

Formatting

Stream objects that implement formatting are instances of either PrintWriter, a character stream class, or PrintStream,[26] a byte stream class.

Note

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.

The print and println Methods

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

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.

Note

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.

Elements of a format specifier.

Figure 10.4. Elements of a format specifier.

The elements must appear in the order shown. Working from the right, the optional elements are:

  • PrecisionFor 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.

  • WidthThe minimum width of the formatted value; the value is padded if necessary. By default the value is left-padded with blanks.

  • FlagsSpecify 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 IndexAllows 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);
    

I/O from the Command Line

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

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

The Console

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.

readPassword

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) {}
}

Password follows these steps:

  1. Attempt to retrieve the Console object. If the object is not available, abort.

  2. Invoke Console.readLine to prompt for and read the user’s login name.

  3. Invoke Console.readPassword to prompt for and read the user’s existing password.

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

  5. Repeat the following steps until the user enters the same password twice:

    1. Invoke Console.readPassword twice to prompt for and read a new password.

    2. If the user entered the same password both times, invoke change to change it. (Again, change is a dummy method.)

    3. Overwrite both passwords with blanks.

  6. Overwrite the old password with blanks.

Data Streams

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

Order in Record

Data Type

Data Description

Output Method

Input Method

Sample Value

1

double

Item price

DataOutputStream.writeDouble

DataInputStream.readDouble

19.99

2

int

Unit count

DataOutputStream.writeInt

DataInputStream.readInt

12

3

String

Item description

DataOutputStream.writeUTF

DataInputStream.readUTF

"Java T-Shirt"

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.

Object Streams

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.

Output and Input of Complex Objects

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.

I/O of multiple referred-to objects.

Figure 10.5. I/O of multiple referred-to objects.

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.

File I/O

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.

File Objects

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 Has Many Names

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

Method Invoked

Returns on Microsoft Windows

Returns on Solaris

a.toString()

xanadu.txt

xanadu.txt

a.getName()

xanadu.txt

xanadu.txt

a.getParent()

NULL

NULL

a.getAbsolutePath()

c:javaexamplesxanadu.txt

/home/cafe/java/examples/xanadu.txt

a.getCanonicalPath()

c:javaexamplesxanadu.txt

/home/cafe/java/examples/xanadu.txt

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

b.toString()

..examplesxanadu.txt

../examples/xanadu.txt

b.getName()

xanadu.txt

xanadu.txt

b.getParent()

..examples

../examples

b.getAbsolutePath()

c:javaexamples..examplesxanadu.txt

/home/cafe/java/examples/xanadu.txt

b.getCanonicalPath()

c:javaexamplesxanadu.txt

/home/cafe/java/examples/xanadu.txt

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());
    }
  }
}

Manipulating Files

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.

Working with Directories

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.

Static Methods

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

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.

A ZIP archive.

Figure 10.6. A ZIP archive.

Suppose that you want to extract a specific file from a ZIP archive. If you use a sequential access stream, you have to:

  1. Open the ZIP archive.

  2. Search through the ZIP archive until you locate the file you want to extract.

  3. Extract the file.

  4. 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:

  1. Open the ZIP archive.

  2. Seek to the directory entry and locate the entry for the file you want to extract from the ZIP archive.

  3. Seek (backward) within the ZIP archive to the position of the file to extract.

  4. Extract the file.

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

A ZIP file has the notion of a current file pointer.

Figure 10.7. A ZIP file has the notion of a current file pointer.

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.

The New I/O Packages

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.

Summary

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.

Questions and Exercises: Basic I/O

Questions

1.

What class would you use to read a few pieces of data that are at known positions near the end of a large file?

2.

In a format call, what’s the best way to indicate a new line?

3.

How would you append data to the end of a file? Show the constructor for the class you would use and explain your answer.

Exercises

1.

Implement a pair of classes, one Reader and one Writer, that count the number of times a particular character, such as e, is read or written. The character can be specified when the stream is created. Write a program to test your classes. You can use xanadu.txt as the input file.

2.

The file datafile[51] begins with a single long that tells you the offset of a single int piece of data within the same file. Using the RandomAccessFile class, write a program that gets the int piece of data. What is the int data?

Answers

You can find answers to these Questions and Exercises at:

tutorial/essential/io/QandE/answers.html


[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

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.6.85