Section 11.5. Example: Reading and Writing Binary Files


[Page 533]

11.5. Example: Reading and Writing Binary Files

Although text files are extremely useful and often employed, they can't and shouldn't be used for every data-processing application. For example, your college's administrative data system undoubtedly uses files to store student records. Because your student record contains a variety of different types of dataStrings, ints, doublesit cannot be processed as text. Similarly, a company's inventory files, which also include data of a wide variety of types, cannot be processed as text. Files such as these must be processed as binary data.

Suppose you are asked to write an application that involves the use of a company's employee records. Recall that a record is a structure that combines different types of data into a single entity. It's like an object with no methods, just instance variables.

A binary file is a sequence of bytes. Unlike a text file, which is terminated by a special end-of-file marker, a binary file consists of nothing but data. A binary file doesn't have an end-of-file character because any such character would be indistinguishable from a binary datum.

Debugging Tip: End of Binary File

Because a binary file does not have an end-of-file character, it would be an error to use the same loop-entry conditions we used in the loops we designed for reading text files.


Generally speaking, the steps involved in reading and writing binary files are the same as for text files:

1.

Connect a stream to the file.

2.

Read or write the data, possibly using a loop.

3.

Close the stream.

The difference between text and binary file I/O resides in the Java streams we use.

11.5.1. Writing Binary Data

Let's begin by designing a method that will output employee data to a binary file. As the developer of this program, one thing you'll have to do is build some sample data files. These can't easily be built by handremember you can't use a text editor to create themso you'll want to develop a method that can generate some random data of the sort your application will have to process.

Generating binary data


Effective Design: I/O Design

When designing file I/O applications, it is good to design the input and output methods together. This is especially important for binary I/O.


The first thing we need to know is exactly what the data look like. Let's assume that each record contains three individual pieces of datathe employee's name, age, and pay rate. For example, the data in a file containing four records might look like this, once the data is interpreted:

Name0 24 15.06 Name1 25 5.09 Name2 40 11.45 Name3 52 9.25 



[Page 534]

As you can see, these data items look as if they were randomly generated, but they resemble the real data in some important respects: they are of the right typeString, int, doubleand have the right kind of values. Of course, when these data terms are stored in the file or in the program's memory, they just look like one long string of 0's and 1's.

Our approach to designing this output method will be the same as the approach we used in designing methods for text I/O. That is, we start with two questions:

  • What stream classes should I use?

  • What methods can I use?

And we find the answers to these by searching through the java.io package (Fig. 11.3 and Table 11.1).

Because we are performing binary output, we need to use a subclass of OutputStream. Because we're outputting to a file, one likely candidate is FileOutputStream (Fig. 11.17). This class has the right kind of constructors, but it only contains write() methods for writing ints and bytes, and we need to be able to write Strings and doubles as well as ints.

Figure 11.17. The FileOutputStream class.


These kinds of methods are found in DataOutputStream (Fig. 11.18), which contains a write() method for each different type of data. As you can see, there is one method for each primitive type. However, note that the writeChar() takes an int parameter, which indicates that the character is written in binary format rather than as a ASCII or Unicode character. Although you can't tell by just reading its method signature, the writeChars(String) method also writes its data in binary format rather than as a sequence of characters. This is the main difference between these write() methods and the ones defined in the Writer branch of Java's I/O hierarchy.


[Page 535]

Figure 11.18. The java.io.DataOutputStream class contains methods for writing all types of data.
(This item is displayed on page 534 in the print version)


Now that we've found the appropriate classes and methods, we need to create a pipeline to write data to the file and develop an output algorithm. To construct a stream to use in writing employee records, we want to join together a DataOutputStream and a FileOutputStream. The DataOutputStream gives us the output methods we need, and the FileOutputStream lets us use the file's name to create the stream:

DataOutputStream outStream   = new DataOutputStream(new FileOutputStream (fileName)); 


This enables the program to write data to the DataOutputStream, which will pass it through the FileOutputStream to the file. That settles the first question.

To develop the output algorithm, we need a loop of some kind that involves calls to the appropriate methods. In this case, because we are generating random data, we can use a simple for loop to generate, say, five records of employee data. We need one write() statement for each of the elements in the employee record: the name (String), age (int), and pay rate (double):

for (int k = 0; k < 5; k++) {                         // Output 5 data records  outStream.writeUTF("Name"  + k);                     // Name  outStream.writeInt((int)(20 + Math.random() * 25));  // Age  outStream.writeDouble(Math.random() * 500);          // Payrate } 


Within the loop body we have one output statement for each data element in the record. The names of the methods reflect the type of data they write. Thus, we use writeInt() to write an int and writeDouble() to write a double. But why do we use writeUTF to write the employee's name, a String?

The Unicode Text Format (UTF)

There is no DataOutputStream.writeString() method. Instead, Strings are written using the writeUTF() method. UTF stands for Unicode Text Format, a coding scheme for Java's Unicode character set. Recall that Java uses the Unicode character set instead of the ASCII set. As a 16-bit code, Unicode can represent 8-bit ASCII characters plus a wide variety of Asian and other international characters. However, Unicode is not a very efficient coding scheme if you aren't writing an international program. If your program just uses the standard ASCII characters, which can be stored in 1 byte, you would be wasting 1 byte per character if you stored them as straight Unicode characters. Therefore, for efficiency purposes, Java uses the UTF format. UTF encoding can still represent all of the Unicode characters, but it provides a more efficient way of representing the ASCII subset.

ASCII vs. Unicode



[Page 536]

It is now time to combine these separate elements into a single method (Fig. 11.19). The writeRecords() method takes a single String parameter that specifies the name of the file. This is a void method. It will output data to a file, but it will not return anything to the calling method. The method follows the standard output algorithm: create an output stream, write the data, close the stream. Note also that the method includes a try/catch block to handle any IOExceptions that might be thrown.

Figure 11.19. A method to write a binary file consisting of five randomly constructed records.

private void writeRecords( String fileName )  {  try {    DataOutputStream outStream                           // Open stream       = new DataOutputStream(new FileOutputStream(fileName));    for (int k = 0; k < 5; k++) {                        // Output 5 data records       String name = "Name" + k;                         // of name, age, payrate       outStream.writeUTF("Name"  + k);       outStream.writeInt((int)(20 + Math.random() * 25));       outStream.writeDouble(5.00 + Math.random() * 10);    } // for    outStream.close();                                   // Close the stream  } catch (IOException e) {    display.setText("IOERROR:  " + e.getMessage() + "\n");  } } // writeRecords() 

11.5.2. Reading Binary Data

The steps involved in reading data from a binary file are the same as for reading data from a text file: create an input stream and open the file, read the data, close the file. The main difference lies in the way you check for the end-of-file marker in a binary file.

Let's design a method to read the binary data that were output by the writeRecords() method. We'll call this method readRecords(). It, too, will consist of a single String parameter that provides the name of the file to be read, and it will be a void method. It will just display the data on System.out.

The next questions we need to address are: What stream classes should we use, and what methods should we use? For binary input, we need an InputStream subclass (Fig. 11.3 and Table 11.1). As you have probably come to expect, the FileInputStream class contains constructors that let us create a stream from a file name (Fig. 11.20). However, it does not contain useful read() methods. Fortunately, the DataInputStream class contains the input counterparts of the methods we found in DataOutputStream (Fig. 11.21). Therefore, our input stream for this method will be a combination of DataInputStream and FileInputStream:


[Page 537]

DataInputStream inStream = new DataInputStream(new FileInputStream(file)); 


Figure 11.20. The java.io.FileInputStream class.
(This item is displayed on page 536 in the print version)


Figure 11.21. The java.io.DataInputStream class contains methods for reading all types of data.


Now that we have identified the classes and methods we'll use to read the data, the most important remaining issue is designing a read loop that will terminate correctly. Unlike text files, binary files do not contain a special end-of-file marker. Therefore, the read methods can't see anything in the file that tells them they're at the end of the file. Instead, when a binary read method attempts to read past the end of the file, an end-of-file exception EOFException is thrown. Thus, the binary loop is coded as an infinite loop that is exited when the EOFException is raised:

try {   while (true) {                                            // Infinite loop     String name = inStream.readUTF();                       // Read a record     int age = inStream.readInt();     double pay = inStream.readDouble();     display.append(name + " "  + age + " "  + pay + "\n");   } // while } catch (EOFException e) {}                                 // Until EOF exception 


The read loop is embedded within a TRy/catch statement. Note that the catch clause for the EOFException does nothing. Recall that when an exception is thrown in a try block, the block is exited for good, which is precisely the action we want to take. That is why we needn't do anything when we catch the EOFException. We have to catch the exception or else Java will catch it and terminate the program. This is an example of an expected exception.

An expected exception


Effective Design: EOFException

An attempt to read past the end of a binary file will cause an EOFException to be thrown. Catching this exception is the standard way of terminating a binary input loop.



[Page 538]

Note also that the read() statements within the loop are mirror opposites of the write() statements in the method that created the data. This will generally be true for binary I/O routines: the statements that read data from a file should "match" those that wrote the data in the first place.

Effective Design: Matching Input to Output

The statements used to read binary data should match those that wrote the data. If a writeX() method were used to write the data, a readX() should be used to read it.


To complete the method, the only remaining task is to close() the stream after the data are read. The complete definition is shown in Figure 11.22.

Figure 11.22. A method for reading binary data.

private void readRecords( String fileName ) {  try {    DataInputStream inStream                               // Open stream      = new DataInputStream(new FileInputStream(fileName));    display.setText("Name    Age Pay\n");    try {      while (true) {                                       // Infinite loop         String name = inStream.readUTF();                 // Read a record         int age = inStream.readInt();         double pay = inStream.readDouble();         display.append(name + " " + age + " " + pay + "\n");      } // while    } catch (EOFException e) {                             // Until EOF exception    } finally {        inStream.close();                                  // Close the stream    }    } catch (FileNotFoundException e) {       display.setText("IOERROR: "+ fileName + " NOT Found: \n");    } catch (IOException e) {       display.setText("IOERROR: " + e.getMessage() + "\n");    } } // readRecords() 

It is important to place a close() statement after the catch EOFException clause. If it were placed in the try block, it would never get executed. Note also that the entire method is embedded in an outer try block that catches the IOException thrown by the various read() methods and the FileNotFoundException thrown by the FileInputStream() constructor. These make the method a bit longer, but conceptually they belong in the method.

Java Programming Tip: The finally Block

In coding a binary read loop, the TRy block is exited as soon as the EOFException is raised. Therefore, the close() statement must be placed in the finally clause, which is executed after the catch clause.



[Page 539]

Effective Design: Nested try/Catch

Nested TRy blocks must be used to perform binary I/O correctly. The outer block encapsulates statements that throw IOExceptions. The inner block encapsulates the read loop and catches the EOFException. No particular action need be taken when the EOFException is caught.


Self-Study Exercise

Exercise 11.5

Identify the error in the following method, which is supposed to read a binary file of ints from a DataInputStream:

public void readIntegers(DataInputStream inStream) {     try {         while (true) {             int num = inStream.readInt();             System.out.println(num);         }         inStream.close();     } catch (EOFException e) {     } catch (IOException e) {     } } // readIntegers() 


11.5.3. The BinaryIO Application

Given the methods we wrote in the preceding section, we can now specify the overall design of the BinaryIO class (Fig. 11.23). The program sets up the same interface we used in the text file example (Fig. 11.24). It allows the user to specify the name of a data file to read or write. One button allows the user to write random employee records to a binary file, and the other allows the user to display the contents of a file in a JTextArea. The BinaryIO program in Figure 11.25 incorporates both readRecords() and writeRecords() into a complete Java program.

Figure 11.23. Design of the BinaryIO class.



[Page 540]

Figure 11.24. A program to read and write binary files.


Figure 11.25. BinaryIO class illustrating simple input and output from a binary file.

import javax.swing.*;                                     // Swing components import java.awt.*; import java.io.*; import java.awt.event.*; public class BinaryIO extends JFrame implements ActionListener{     private JTextArea display = new JTextArea();     private JButton read = new JButton("Read Records From File"),                     write = new JButton("Generate Random Records");     private JTextField nameField = new JTextField(10);     private JLabel prompt = new JLabel("Filename:", JLabel.RIGHT);     private JPanel commands = new  JPanel();     public BinaryIO() {         super("BinaryIO Demo");                          // Set window title         read.addActionListener(this);         write.addActionListener(this);         commands.setLayout(new GridLayout(2,2,1,1));     // Control panel         commands.add(prompt);         commands.add(nameField);         commands.add(read);         commands.add(write);         display.setLineWrap(true);         this.getContentPane().setLayout(new BorderLayout () );         this.getContentPane().add("North", commands);         this.getContentPane().add( new JScrollPane(display));         this.getContentPane().add("Center", display);     } // BinaryIO()     private void readRecords( String fileName ) {         try {             DataInputStream inStream                     // Open stream                = new DataInputStream(new FileInputStream(fileName));             display.setText("Name Age Pay\n");             try {                 while (true) {                           // Infinite loop                     String name = inStream.readUTF();    // Read a record                     int age = inStream.readInt();                     double pay = inStream.readDouble();                     display.append(name + "   " + age + "   " + pay + "\n");                 } // while             } catch (EOFException e) {                   // Until EOF exception             } finally {                 inStream.close();                        // Close the stream             } 
[Page 541]
} catch (FileNotFoundException e) { display.setText("IOERROR: File NOT Found: " + fileName + "\n"); } catch (IOException e) { display.setText("IOERROR: " + e.getMessage() + "\n"); } } // readRecords() private void writeRecords( String fileName ) { try { DataOutputStream outStream // Open stream = new DataOutputStream(new FileOutputStream(fileName)); for (int k = 0; k < 5; k++) { // Output 5 data records String name = "Name" + k; // of name, age, payrate outStream.writeUTF("Name" + k); outStream.writeInt((int)(20 + Math.random() * 25)); outStream.writeDouble(5.00 + Math.random() * 10); } // for outStream.close(); // Close the stream } catch (IOException e) { display.setText("IOERROR: " + e.getMessage() + "\n"); } } // writeRecords() public void actionPerformed(ActionEvent evt) { String fileName = nameField.getText(); if (evt.getSource() == read) readRecords(fileName); else writeRecords(fileName); } // actionPerformed() public static void main(String args[]) { BinaryIO bio = new BinaryIO(); bio.setSize(400, 200); bio.setVisible(true); bio.addWindowListener(new WindowAdapter() { // Quit public void windowClosing(WindowEvent e) { System.exit(0); } }); } // main() } // BinaryIO class

11.5.4. Abstracting Data from Files

The method to read a binary file must exactly match the order of the write and read statements of the method that wrote the binary file. For example, if the file contains records that consist of a String followed by an int followed by a double, then they must be written in a sequence consisting of

writeUTF(); writeInt(): writeDouble(); 



[Page 542]

And they must thereafter be read by the sequence of

readUTF(); readInt(): readDouble(); 


Attempting to do otherwise would make it impossible to interpret the data in the file.

This point should make it evident why (non-Java) binary files are not portable, whereas text files are. With text files, each character consists of 8 bits, and each 8-bit chunk can be interpreted as an ASCII character. So even though a text file consists of a long sequence of 0's and 1's, we know how to find the boundaries between each character. That is why any text editor can read a text file, no matter what program created it.

Portability


On the other hand, binary files are also just a long sequence of 0's and 1's, but we can't tell where one data element begins and another one ends. For example, the 64-bit sequence

010100110011001001010100110011000 010100110011001011010100110011000 


could represent two 32-bit ints or two 32-bit floats or one 64-bit double or four 16-bit chars or a single String of eight ASCII characters. We can't tell what data we have unless we know exactly how the data were written.

Debugging Tip: Interpreting Binary Data

The fact that you can read the data in a binary file is no guarantee that you are interpreting it correctly. To interpret it correctly, you must read it the same way it was written.


Effective Design: Data Abstraction

A string of binary data is "raw" and has no inherent structure. The programs that read and write the string provide it with structure. A string of 64 0's and 1's can be interpreted as two ints or one long or even as some kind of object, so an int, long or an object is an abstraction imposed upon the data by the program.





Java, Java, Java(c) Object-Orienting Problem Solving
Java, Java, Java, Object-Oriented Problem Solving (3rd Edition)
ISBN: 0131474340
EAN: 2147483647
Year: 2005
Pages: 275

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net