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:
Writing Delimited OutputIn 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
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 TextWhen 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:
NOTE
java.util.StringTokenizer 1.0
Reading Delimited InputReading 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.java1. 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 ClassWhen 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 following API notes contain the most important methods for the StringBuilder and StringBuffer classes. java.lang.StringBuilder 5.0 java.lang.StringBuffer 1.0
Working with Random-Access StreamsIf 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
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:
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.java1. 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. } |