Java ScreenShot
     

Screenshot Core Java 2: Volume I - Fundamentals

Table of Contents
 12.  Streams and Files


Putting Streams to Use

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

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

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

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

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


Java 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:

characters = 80 bytes for the name

double = 8 bytes

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

Java 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

Java graphics api_icon.gif
  • String(StringBuffer buffer)

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

Screenshot

Java ScreenShot
     
Top
 

Comments