Use of Streams

   


In the next four sections, we show you how to put some of the creatures in the stream zoo to good use. For these examples, we assume you are working with the Employee class and some of its subclasses, such as Manager. (See Chapters 4 and 5 for more on these example classes.) We 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 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.

NOTE

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. Because 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 tokenizer = new StringTokenizer(line, "|"); 

You can specify multiple delimiters in the string, for example:

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

This means that any of the characters in the string can serve as delimiters.

If you don't specify a delimiter set, the default is " \t\n\r", that is, all whitespace characters (space, tab, newline, and carriage return)

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. The following loop processes all tokens:


while (tokenizer.hasMoreTokens())
{
   String token = tokenizer.nextToken();
   process token
}


NOTE

An alternative to the StringTokenizer is the split method of the String class. The call line.split("[|,;]") returns a String[] array consisting of all tokens, using the delimiters inside the brackets. You can use any regular expression to describe delimiters we will discuss regular expression on page 697.



 java.util.StringTokenizer 1.0 

  • StringTokenizer(String str, String delim)

    constructs a string tokenizer with the given delimiter set.

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

The StringBuilder Class

When you process input, you often need to construct strings from individual characters or Unicode code units. It would be inefficient to use string concatenation for this purpose. 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 StringBuilder class avoids this problem.

In contrast, a StringBuilder works much like an ArrayList. It manages a char[] array that can grow and shrink on demand. You can append, insert, or remove code units until the string builder holds the desired string. Then you use the toString method to convert the contents to an actual String object.

NOTE

The StringBuilder class was introduced in JDK 5.0. Its predecessor, StringBuffer, is slightly less efficient, but it allows multiple threads to add or remove characters. If all string editing happens in a single thread, you should use StringBuilder instead. The APIs of both classes are identical.


The following API notes contain the most important methods for the StringBuilder and StringBuffer classes.


 java.lang.StringBuilder 5.0 


 java.lang.StringBuffer 1.0 

  • StringBuilder/StringBuffer()

    constructs an empty string builder or string buffer.

  • StringBuilder/StringBuffer(int length)

    constructs an empty string builder or string buffer with the initial capacity length.

  • StringBuilder/StringBuffer(String str)

    constructs a string builder or string buffer with the initial contents str.

  • int length()

    returns the number of code units of the builder or buffer.

  • StringBuilder/StringBuffer append(String str)

    appends a string and returns this.

  • StringBuilder/StringBuffer append(char c)

    appends a code unit and returns this.

  • StringBuilder/StringBuffer appendCodePoint(int cp) 5.0

    appends a code point, converting it into one or two code units, and returns this.

  • void setCharAt(int i, char c)

    sets the ith code unit to c.

  • StringBuilder/StringBuffer insert(int offset, String str)

    inserts a string at position offset and returns this.

  • StringBuilder/StringBuffer insert(int offset, char c)

    inserts a code unit at position offset and returns this.

  • StringBuilder/StringBuffer delete(int startIndex, int endIndex)

    deletes the code units with offsets startIndex to endIndex - 1 and returns this.

  • String toString()

    returns a string with the same data as the builder or buffer contents.

Working with 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 make all records the same length. This lets us implement a random-access method for reading back the information by 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. We do that 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, because 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 code units, starting at the beginning of the string. (If there are too few code units, the method pads the string, using zero values.) 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 code units or until it encounters a character with a zero value. Then, it should skip past the remaining zero values in the input field. For added efficiency, this method uses the StringBuilder class to read in a string.

 static String readFixedString(int size, DataInput in)    throws IOException {    StringBuilder b = new StringBuilder(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(); } 

NOTE

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


       
    top



    Core Java 2 Volume I - Fundamentals
    Core Java(TM) 2, Volume I--Fundamentals (7th Edition) (Core Series) (Core Series)
    ISBN: 0131482025
    EAN: 2147483647
    Year: 2003
    Pages: 132

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