Putting Streams to Use

   

Core Java™ 2: Volume I - Fundamentals
By Cay S. Horstmann, Gary Cornell
Table of Contents
Chapter 12.  Streams and Files


In the next four sections, we will show you how to put some of the creatures in the stream zoo to good use. For these examples, we will assume you are working with the Employee class and some of its derived classes, such as Manager. (See Chapters 4 and 5 for more on these example classes.) We will consider four separate scenarios for saving an array of employee records to a file and then reading them back into memory.

  1. Saving data of the same type (Employee) in text format;

  2. Saving data of the same type in binary format;

  3. Saving and restoring polymorphic data (a mixture of Employee and Manager objects);

  4. Saving and restoring data containing embedded references (managers with pointers to other employees).

Writing Delimited Output

In this section, you will learn how to store an array of Employee records in the time-honored delimited format. This means that each record is stored in a separate line. Instance fields are separated from each other by delimiters. We use a vertical bar (|) as our delimiter. (A colon (:) is another popular choice. Part of the fun is that everyone uses a different delimiter.) Naturally, we punt on the issue of what might happen if a | actually occurred in one of the strings we save.

graphics/notes_icon.gif

Especially on UNIX systems, an amazing number of files are stored in exactly this format. We have seen entire employee databases with thousands of records in this format, queried with nothing more than the UNIX awk, sort, and join utilities. (In the PC world, where desktop database programs are available at low cost, this kind of ad hoc storage is much less common.)

Here is a sample set of records:

 Harry Hacker|35500|1989|10|1 Carl Cracker|75000|1987|12|15 Tony Tester|38000|1990|3|15 

Writing records is simple. Since we write to a text file, we use the PrintWriter class. We simply write all fields, followed by either a | or, for the last field, a \n. Finally, in keeping with the idea that we want the class to be responsible for responding to messages, we add a method, writeData, to our Employee class.

 public void writeData(PrintWriter out) throws IOException {    GregorianCalendar calendar = new GregorianCalendar();    calendar.setTime(hireDay);    out.println(name + "|"       + salary + "|"       + calendar.get(Calendar.YEAR) + "|"       + (calendar.get(Calendar.MONTH) + 1) + "|"       + calendar.get(Calendar.DAY_OF_MONTH)); } 

To read records, we read in a line at a time and separate the fields. This is the topic of the next section, in which we use a utility class supplied with Java to make our job easier.

String Tokenizers and Delimited Text

When reading a line of input, we get a single long string. We want to split it into individual strings. This means finding the | delimiters and then separating out the individual pieces, that is, the sequence of characters up to the next delimiter. (These are usually called tokens.) The StringTokenizer class in java.util is designed for exactly this purpose. It gives you an easy way to break up a large string that contains delimited text. The idea is that a string tokenizer object attaches to a string. When you construct the tokenizer object, you specify which characters are the delimiters. For example, we need to use

 StringTokenizer t = new StringTokenizer(line, "|"); 

You can specify multiple delimiters in the string. For example, to set up a string tokenizer that would let you search for any delimiter in the set

 " \t\n\r" 

use the following:

 StringTokenizer t = new StringTokenizer(line, " \t\n\r"); 

(Notice that this means that any white space marks off the tokens.)

graphics/notes_icon.gif

These four delimiters are used as the defaults if you construct a string tokenizer like this:

 StringTokenizer t = new StringTokenizer(line); 

Once you have constructed a string tokenizer, you can use its methods to quickly extract the tokens from the string. The nextToken method returns the next unread token. The hasMoreTokens method returns true if more tokens are available.

graphics/notes_icon.gif

In our case, we know how many tokens we have in every line of input. In general, you have to be a bit more careful: call hasMoreTokens before calling nextToken because the nextToken method throws an exception when no more tokens are available.

java.util.StringTokenizer 1.0

graphics/api_icon.gif
  • StringTokenizer(String str, String delim)

    Parameters:

    str

    The input string from which tokens are read

     

    delim

    A string containing delimiter characters (every character in this string is a delimiter)

  • StringTokenizer(String str)

    constructs a string tokenizer with the default delimiter set " \t\n\r".

  • boolean hasMoreTokens()

    returns true if more tokens exist.

  • String nextToken()

    returns the next token; throws a NoSuchElementException if there are no more tokens.

  • String nextToken(String delim)

    returns the next token after switching to the new delimiter set. The new delimiter set is subsequently used.

  • int countTokens()

    returns the number of tokens still in the string.

Reading Delimited Input

Reading in an Employee record is simple. We simply read in a line of input with the readLine method of the BufferedReader class. Here is the code needed to read one record into a string.

 BufferedReader in    = new BufferedReader(new FileReader("employee.dat")); . . . String line = in.readLine(); 

Next, we need to extract the individual tokens. When we do this, we end up with strings, so we need to convert them to numbers.

Just as with the writeData method, we add a readData method of the Employee class. When you call

 e.readData(in); 

this method overwrites the previous contents of e. Note that the method may throw an IOException if the readLine method throws that exception. There is nothing this method can do if an IOException occurs, so we just let it propagate up the call chain.

Here is the code for this method:

 public void readData(BufferedReader in) throws IOException {    String s = in.readLine();    StringTokenizer t = new StringTokenizer(s, "|");    name = t.nextToken();    salary = Double.parseDouble(t.nextToken());    int y = Integer.parseInt(t.nextToken());    int m = Integer.parseInt(t.nextToken());    int d = Integer.parseInt(t.nextToken());    GregorianCalendar calendar       = new GregorianCalendar(y, m - 1, d);       // GregorianCalendar uses 0 = January    hireDay = calendar.getTime(); } 

Finally, in the code for a program that tests these methods, the static method

 void writeData(Employee[] e, PrintWriter out) 

first writes the length of the array, then writes each record. The static method

 Employee[] readData(BufferedReader in) 

first reads in the length of the array, then reads in each record, as illustrated in Example 12-2.

Example 12-2 DataFileTest.java
   1. import java.io.*;   2. import java.util.*;   3.   4. public class DataFileTest   5. {   6.    public static void main(String[] args)   7.    {   8.       Employee[] staff = new Employee[3];   9.  10.       staff[0] = new Employee("Carl Cracker", 75000,  11.          1987, 12, 15);  12.       staff[1] = new Employee("Harry Hacker", 50000,  13.          1989, 10, 1);  14.      staff[2] = new Employee("Tony Tester", 40000,  15.          1990, 3, 15);  16.  17.      try  18.      {  19.          // save all employee records to the file employee.dat  20.          PrintWriter out = new PrintWriter(new  21.             FileWriter("employee.dat"));  22.          writeData(staff, out);  23.          out.close();  24.  25.          // retrieve all records into a new array  26.          BufferedReader in = new BufferedReader(new  27.             FileReader("employee.dat"));  28.          Employee[] newStaff = readData(in);  29.          in.close();  30.  31.          // print the newly read employee records  32.          for (int i = 0; i < newStaff.length; i++)  33.             System.out.println(newStaff[i]);  34.       }  35.       catch(IOException exception)  36.       {  37.          exception.printStackTrace();  38.       }  39.    }  40.  41.    /**  42.       Writes all employees in an array to a print writer  43.       @param e an array of employees  44.       @param out a print writer  45.    */  46.    static void writeData(Employee[] e, PrintWriter out)  47.       throws IOException  48.    {  49.       // write number of employees  50.       out.println(e.length);  51.  52.       for (int i = 0; i < e.length; i++)  53.          e[i].writeData(out);  54.    }  55.  56.    /**  57.       Reads an array of employees from a buffered reader  58.       @param in the buffered reader  59.       @return the array of employees  60.    */  61.    static Employee[] readData(BufferedReader in)  62.       throws IOException  63.    {  64.       // retrieve the array size  65.       int n = Integer.parseInt(in.readLine());  66.  67.       Employee[] e = new Employee[n];  68.       for (int i = 0; i < n; i++)  69.       {  70.          e[i] = new Employee();  71.          e[i].readData(in);  72.       }  73.       return e;  74.    }  75. }  76.  77. class Employee  78. {  79.    public Employee() {}  80.  81.    public Employee(String n, double s,  82.       int year, int month, int day)  83.    {  84.       name = n;  85.       salary = s;  86.       GregorianCalendar calendar  87.          = new GregorianCalendar(year, month - 1, day);  88.          // GregorianCalendar uses 0 = January  89.       hireDay = calendar.getTime();  90.    }  91.  92.    public String getName()  93.    {  94.       return name;  95.    }  96.  97.    public double getSalary()  98.    {  99.       return salary; 100.    } 101. 102.    public Date getHireDay() 103.    { 104.       return hireDay; 105.    } 106. 107.    public void raiseSalary(double byPercent) 108.    { 109.       double raise = salary * byPercent / 100; 110.       salary += raise; 111.    } 112. 113.    public String toString() 114.    { 115.       return getClass().getName() 116.          + "[name=" + name 117.          + ",salary=" + salary 118.          + ",hireDay=" + hireDay 119.          + "]"; 120.    } 121. 122.    /** 123.       Writes employee data to a print writer 124.       @param out the print writer 125.    */ 126.    public void writeData(PrintWriter out) throws IOException 127.    { 128.       GregorianCalendar calendar = new GregorianCalendar(); 129.       calendar.setTime(hireDay); 130.       out.println(name + "|" 131.          + salary + "|" 132.          + calendar.get(Calendar.YEAR) + "|" 133.          + (calendar.get(Calendar.MONTH) + 1) + "|" 134.          + calendar.get(Calendar.DAY_OF_MONTH)); 135.    } 136. 137.    /** 138.       Reads employee data from a buffered reader 139.       @param in the buffered reader 140.    */ 141.    public void readData(BufferedReader in) throws IOException 142.    { 143.       String s = in.readLine(); 144.       StringTokenizer t = new StringTokenizer(s, "|"); 145.       name = t.nextToken(); 146.       salary = Double.parseDouble(t.nextToken()); 147.       int y = Integer.parseInt(t.nextToken()); 148.       int m = Integer.parseInt(t.nextToken()); 149.       int d = Integer.parseInt(t.nextToken()); 150.       GregorianCalendar calendar 151.          = new GregorianCalendar(y, m - 1, d); 152.          // GregorianCalendar uses 0 = January 153.       hireDay = calendar.getTime(); 154.    } 155. 156.    private String name; 157.    private double salary; 158.    private Date hireDay; 159. } 

Random-Access Streams

If you have a large number of employee records of variable length, the storage technique used in the preceding section suffers from one limitation: it is not possible to read a record in the middle of the file without first reading all records that come before it. In this section, we will make all records the same length. This lets us implement a random-access method for reading back the information using the RandomAccessFile class that you saw earlier we can use this to get at any record in the same amount of time.

We will store the numbers in the instance fields in our classes in a binary format. This is done with the writeInt and writeDouble methods of the DataOutput interface. (As we mentioned earlier, this is the common interface of the DataOutputStream and the RandomAccessFile classes.)

However, since the size of each record must remain constant, we need to make all the strings the same size when we save them. The variable-size UTF format does not do this, and the rest of the Java library provides no convenient means of accomplishing this. We need to write a bit of code to implement two helper methods to make the strings the same size. We will call the methods writeFixedString and readFixedString. These methods read and write Unicode strings that always have the same length.

The writeFixedString method takes the parameter size. Then, it writes the specified number of characters, starting at the beginning of the string. (If there are too few characters the method pads the string, using characters whose Unicode values are zero.) Here is the code for the writeFixedString method:

 static void writeFixedString(String s, int size, DataOutput out)    throws IOException {    int i;    for (i = 0; i < size; i++)    {       char ch = 0;       if (i < s.length()) ch = s.charAt(i);       out.writeChar(ch);    } } 

The readFixedString method reads characters from the input stream until it has consumed size characters or until it encounters a character with Unicode 0. Then, it should skip past the remaining zero characters in the input field.

For added efficiency, this method uses the StringBuffer class to read in a string. A StringBuffer is an auxiliary class that lets you preallocate a memory block of a given length. In our case, we know that the string is, at most, size bytes long. We make a string buffer in which we reserve size characters. Then we append the characters as we read them in.

graphics/notes_icon.gif

Using the StringBuffer class in this way is more efficient than reading in characters and appending them to an existing string. Every time you append characters to a string, the string object needs to find new memory to hold the larger string: this is time-consuming. Appending even more characters means the string needs to be relocated again and again. Using the StringBuffer class avoids this problem.

Once the string buffer holds the desired string, we need to convert it to an actual String object. This is done with the String(StringBuffer b) constructor or the StringBuffer.toString() method. These methods do not copy the characters from the string buffer to the string. Instead, they freeze the buffer contents. If you later call a method that makes a modification to the StringBuffer object, the buffer object first gets a new copy of the characters and then modifies that copy. The string object keeps the frozen contents.

 static String readFixedString(int size, DataInput in)    throws IOException {    StringBuffer b = new StringBuffer(size);    int i = 0;    boolean more = true;    while (more && i < size)    {       char ch = in.readChar();       i++;       if (ch == 0) more = false;       else b.append(ch);    }    in.skipBytes(2 * (size - i));    return b.toString(); } 

graphics/notes_icon.gif

These two methods are packaged inside the DataIO helper class.

To write a fixed-size record, we simply write all fields in binary.

 public void writeData(DataOutput out) throws IOException {    DataIO.writeFixedString(name, NAME_SIZE, out);    out.writeDouble(salary);    GregorianCalendar calendar = new GregorianCalendar();    calendar.setTime(hireDay);    out.writeInt(calendar.get(Calendar.YEAR));    out.writeInt(calendar.get(Calendar.MONTH) + 1);    out.writeInt(calendar.get(Calendar.DAY_OF_MONTH)); } 

Reading the data back is just as simple.

 public void readData(DataInput in) throws IOException {    name = DataIO.readFixedString(NAME_SIZE, in);    salary = in.readDouble();    int y = in.readInt();    int m = in.readInt();    int d = in.readInt();    GregorianCalendar calendar       = new GregorianCalendar(y, m - 1, d);       // GregorianCalendar uses 0 = January    hireDay = calendar.getTime(); } 

In our example, each employee record is 100 bytes long because we specified that the name field would always be written using 40 characters. This gives us a breakdown as indicated in the following:

40 characters = 80 bytes for the name

1 double = 8 bytes

3 int = 12 bytes

As an example, suppose you want to position the file pointer to the third record. You can use the following version of the seek method:

 long n = 3; int RECORD_SIZE = 100; in.seek((n - 1) * RECORD_SIZE); 

Then you can read a record:

 Employee e = new Employee(); e.readData(in); 

If you want to modify the record and then save it back into the same location, remember to set the file pointer back to the beginning of the record:

 in.seek((n - 1) * RECORD_SIZE); e.writeData(out); 

To determine the total number of bytes in a file, use the length method. The total number of records is the length divided by the size of each record.

 long int nbytes = in.length(); // length in bytes int nrecords = (int)(nbytes / RECORD_SIZE); 

The test program shown in Example 12-3 writes three records into a data file and then reads them from the file in reverse order. To do this efficiently requires random access we need to get at the third record first.

Example 12-3 RandomFileTest.java
   1. import java.io.*;   2. import java.util.*;   3.   4. public class RandomFileTest   5. {   6.    public static void main(String[] args)   7.    {   8.       Employee[] staff = new Employee[3];   9.  10.       staff[0] = new Employee("Carl Cracker", 75000,  11.          1987, 12, 15);  12.       staff[1] = new Employee("Harry Hacker", 50000,  13.          1989, 10, 1);  14.       staff[2] = new Employee("Tony Tester", 40000,  15.          1990, 3, 15);  16.  17.       try  18.       {  19.          // save all employee records to the file employee.dat  20.          DataOutputStream out = new DataOutputStream(new  21.             FileOutputStream("employee.dat"));  22.          for (int i = 0; i < staff.length; i++)  23.             staff[i].writeData(out);  24.          out.close();  25.  26.          // retrieve all records into a new array  27.          RandomAccessFile in  28.             = new RandomAccessFile("employee.dat", "r");  29.          // compute the array size  30.          int n = (int)(in.length() / Employee.RECORD_SIZE);  31.          Employee[] newStaff = new Employee[n];  32.  33.          // read employees in reverse order  34.          for (int i = n - 1; i >= 0; i--)  35.          {  36.             newStaff[i] = new Employee();  37.             in.seek(i * Employee.RECORD_SIZE);  38.             newStaff[i].readData(in);  39.          }  40.          in.close();  41.  42.          // print the newly read employee records  43.          for (int i = 0; i < newStaff.length; i++)  44.             System.out.println(newStaff[i]);  45.       }  46.       catch(IOException e)  47.       {  48.          e.printStackTrace();  49.       }  50.  51.    }  52. }  53.  54. class Employee  55. {  56.    public Employee() {}  57.  58.    public Employee(String n, double s,  59.       int year, int month, int day)  60.    {  61.       name = n;  62.       salary = s;  63.       GregorianCalendar calendar  64.          = new GregorianCalendar(year, month - 1, day);  65.          // GregorianCalendar uses 0 = January  66.       hireDay = calendar.getTime();  67.    }  68.  69.    public String getName()  70.    {  71.       return name;  72.    }  73.  74.    public double getSalary()  75.    {  76.       return salary;  77.    }  78.  79.    public Date getHireDay()  80.    {  81.       return hireDay;  82.    }  83.  84.    public void raiseSalary(double byPercent)  85.    {  86.       double raise = salary * byPercent / 100;  87.       salary += raise;  88.    }  89.  90.    public String toString()  91.    {  92.       return getClass().getName()  93.          + "[name=" + name  94.          + ",salary=" + salary  95.          + ",hireDay=" + hireDay  96.          + "]";  97.    }  98.  99.    /** 100.       Writes employee data to a data output 101.       @param out the data output 102.    */ 103.    public void writeData(DataOutput out) throws IOException 104.    { 105.       DataIO.writeFixedString(name, NAME_SIZE, out); 106.       out.writeDouble(salary); 107. 108.       GregorianCalendar calendar = new GregorianCalendar(); 109.       calendar.setTime(hireDay); 110.       out.writeInt(calendar.get(Calendar.YEAR)); 111.       out.writeInt(calendar.get(Calendar.MONTH) + 1); 112.       out.writeInt(calendar.get(Calendar.DAY_OF_MONTH)); 113.    } 114. 115.    /** 116.       Reads employee data from a data input 117.       @param in the data input 118.    */ 119.    public void readData(DataInput in) throws IOException 120.    { 121.       name = DataIO.readFixedString(NAME_SIZE, in); 122.       salary = in.readDouble(); 123.       int y = in.readInt(); 124.       int m = in.readInt(); 125.       int d = in.readInt(); 126.       GregorianCalendar calendar 127.          = new GregorianCalendar(y, m - 1, d); 128.          // GregorianCalendar uses 0 = January 129.       hireDay = calendar.getTime(); 130.    } 131. 132.    public static final int NAME_SIZE = 40; 133.    public static final int RECORD_SIZE 134.       = 2 * NAME_SIZE + 8 + 4 + 4 + 4; 135. 136.    private String name; 137.    private double salary; 138.    private Date hireDay; 139. } 140. 141. class DataIO 142. {  public static String readFixedString(int size, 143.       DataInput in) throws IOException 144.    { 145.       StringBuffer b = new StringBuffer(size); 146.       int i = 0; 147.       boolean more = true; 148.       while (more && i < size) 149.       { 150.          char ch = in.readChar(); 151.          i++; 152.          if (ch == 0) more = false; 153.          else b.append(ch); 154.       } 155.       in.skipBytes(2 * (size - i)); 156.       return b.toString(); 157.    } 158. 159.    public static void writeFixedString(String s, int size, 160.       DataOutput out) throws IOException 161.    { 162.       int i; 163.       for (i = 0; i < size; i++) 164.       { 165.          char ch = 0; 166.          if (i < s.length()) ch = s.charAt(i); 167.          out.writeChar(ch); 168.       } 169.    } 170. } 

java.lang.StringBuffer 1.0

graphics/api_icon.gif
  • StringBuffer()

    constructs an empty string buffer.

  • StringBuffer(int length)

    constructs an empty string buffer with the initial capacity length.

  • StringBuffer(String str)

    constructs a string buffer with the initial contents str.

  • int length()

    returns the number of characters of the buffer.

  • int capacity()

    returns the current capacity, that is, the number of characters that can be contained in the buffer before it must be relocated.

  • void ensureCapacity(int m)

    enlarges the buffer if the capacity is fewer than m characters.

  • void setLength(int n)

    If n is less than the current length, characters at the end of the string are discarded. If n is larger than the current length, the buffer is padded with '\0' characters.

  • char charAt(int i)

    returns the ith character (i is between 0 and length()-1); throws a StringIndexOutOfBoundsException if the index is invalid.

  • void getChars(int from, int to, char[] a, int offset)

    copies characters from the string buffer into an array.

    Parameters:

    from

    The first character to copy

     

    to

    The first character not to copy

     

    a

    The array to copy into

     

    offset

    The first position in a to copy into

  • void setCharAt(int i, char ch)

    sets the ith character to ch.

  • StringBuffer append(String str)

    appends a string to the end of this buffer (the buffer may be relocated as a result); returns this.

  • StringBuffer append(char c)

    appends a character to the end of this buffer (the buffer may be relocated as a result); returns this.

  • StringBuffer insert(int offset, String str)

    inserts a string at position offset into this buffer (the buffer may be relocated as a result); returns this.

  • StringBuffer insert(int offset, char c)

    inserts a character at position offset into this buffer (the buffer may be relocated as a result); returns this.

  • String toString()

    returns a string pointing to the same data as the buffer contents. (No copy is made.)

java.lang.String 1.0

graphics/api_icon.gif
  • String(StringBuffer buffer)

    makes a string pointing to the same data as the buffer contents. (No copy is made.)


       
    Top
     



    Core Java 2(c) Volume I - Fundamentals
    Building on Your AIX Investment: Moving Forward with IBM eServer pSeries in an On Demand World (MaxFacts Guidebook series)
    ISBN: 193164408X
    EAN: 2147483647
    Year: 2003
    Pages: 110
    Authors: Jim Hoskins

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