18.8. (Optional) Case Study: Address Book |
Now let us use RandomAccessFile to create a useful project for storing and viewing an address book. The user interface of the program is shown in Figure 18.19. The Add button stores a new address at the end of the file. The First , Next , Previous , and Last buttons retrieve the first, next, previous, and last addresses from the file, respectively.
Random access files are often used to process files of records. For convenience, fixed-length records are used in random access files so that a record can be located easily, as shown in Figure 18.20. A record consists of a fixed number of fields. A field can be a string or a primitive data type. A string in a fixed-length record has a maximum size. If a string is smaller than the maximum size , the rest of the string is padded with blanks.
Let address.dat be the file to store addresses. A RandomAccessFile for both read and write can be created using
RandomAccessFile raf = new RandomAccessFile( "address.dat" , "rw" );
Let each address consist of a name (32 characters), street (32 characters), city (20 characters), state (2 characters), and zip (5 characters). If the actual size of a field (e.g., name ) is less than the fixed maximum size, fill it with blank characters. If the actual size of a field is greater than the fixed maximum size, truncate the string. Thus the total size of an address is 32 + 32 + 20 + 2 + 5 = 91 characters . Since each character occupies two bytes, one address takes 2*91 = 182 bytes. After an address record is read, the file pointer is 182 bytes ahead of the previous file pointer.
For convenience, Listing 18.8 contains two methods for reading and writing a fixed-length string.
1 import java.io.*; 2 3 public class FixedLengthStringIO { 4 /** Read fixed number of characters from a DataInput stream */ 5 public static String readFixedLengthString( int size, 6 DataInput in) throws IOException { 7 // Declare an array of characters 8 char [] chars = new char [size]; 9 10 // Read fixed number of characters to the array 11 for ( int i = ; i < size; i++) 12 chars[i] = in.readChar(); 13 14 return new String(chars); 15 } 16 17 /** Write fixed number of characters to a DataOutput stream */ 18 public static void writeFixedLengthString(String s, int size, 19 DataOutput out) throws IOException { 20 char [] chars = new char [size]; 21 22 // Fill in string with characters 23 s.getChars( , Math.min(s.length(), size) , chars, ); 24 25 // Fill in blank characters in the rest of the array 26 for ( int i = Math.min(s.length(), size) ; i < chars.length; i++) 27 chars[i] = ' ' ; 28 29 // Create and write a new string padded with blank characters 30 out.writeChars( new String(chars)); 31 } 32 } |
The writeFixedLengthString(String s, int size, DataOutput out) method writes a string in a fixed size to a DataOutput stream. If the string is longer than the specified size, it is truncated (line 23); if it is shorter than the specified size, blanks are padded into it (lines 26 “27). In any case, a new fixed-length string is written to a specified output stream. Since RandomAccessFile implements DataOutput , this method can be used to write a string to a RandomAccessFile . For example, invoking writeFixedLengthString("John", 2, raf) actually writes "Jo" to the RandomAccessFile raf , since the size is 2 . Invoking writeFixedLengthString("John", 6, raf) actually writes "John" to the RandomAccessFile raf , since the size is 6 .
The readFixedLengthString(int size, InputOutput in) method reads a fixed number of characters from an InputStream and returns as a string. Since RandomAccessFile implements InputOutput , this method can be used to read a string from a writeFixedLengthString(String s, int size, DataOutput out) .
The rest of the work can be summarized in the following steps:
1. | Create the user interface. |
2. | Add a record to the file. |
3. | Read a record from the file. |
4. | Write the code to implement the button actions. |
The program is shown in Listing 18.9.
1 import java.io.*; 2 import java.awt.*; 3 import java.awt.event.*; 4 import javax.swing.*; 5 import javax.swing.border.*; 6 7 public class AddressBook extends JFrame { 8 // Specify the size of five string fields in the record 9 final static int NAME_SIZE = 32 ; 10 final static int STREET_SIZE = 32 ; 11 final static int CITY_SIZE = 20 ; 12 final static int STATE_SIZE = 2 ; 13 final static int ZIP_SIZE = 5 ; 14 final static int RECORD_SIZE = 15 (NAME_SIZE + STREET_SIZE + CITY_SIZE + STATE_SIZE + ZIP_SIZE); 16 17 // Access address.dat using RandomAccessFile 18 private RandomAccessFile raf; 19 20 // Text fields 21 private JTextField jtfName = new JTextField(NAME_SIZE); 22 private JTextField jtfStreet = new JTextField(STREET_SIZE); 23 private JTextField jtfCity = new JTextField(CITY_SIZE); 24 private JTextField jtfState = new JTextField(STATE_SIZE); 25 private JTextField jtfZip = new JTextField(ZIP_SIZE); 26 27 // Buttons 28 private JButton jbtAdd = new JButton( "Add" ); 29 private JButton jbtFirst = new JButton( "First" ); 30 private JButton jbtNext = new JButton( "Next" ); 31 private JButton jbtPrevious = new JButton( "Previous" ); 32 private JButton jbtLast = new JButton( "Last" ); 33 34 public AddressBook() { 35 // Open or create a random access file 36 try { 37 raf = new RandomAccessFile( "address.dat" , "rw" ); 38 } 39 catch (IOException ex) { 40 System.out.print( "Error: " + ex); 41 System.exit( ); 42 } 43 44 // Panel p1 for holding labels Name, Street, and City 45 JPanel p1 = new JPanel(); 46 p1.setLayout( new GridLayout( 3 , 1 )); 47 p1.add( new JLabel( "Name" )); 48 p1.add( new JLabel( "Street" )); 49 p1.add( new JLabel( "City" )); 50 51 // Panel jpState for holding state 52 JPanel jpState = new JPanel(); 53 jpState.setLayout( new BorderLayout()); 54 jpState.add( new JLabel( "State" ), BorderLayout.WEST); 55 jpState.add(jtfState, BorderLayout.CENTER); 56 57 // Panel jpZip for holding zip 58 JPanel jpZip = new JPanel(); 59 jpZip.setLayout( new BorderLayout()); 60 jpZip.add( new JLabel( "Zip" ), BorderLayout.WEST); 61 jpZip.add(jtfZip, BorderLayout.CENTER); 62 63 // Panel p2 for holding jpState and jpZip 64 JPanel p2 = new JPanel(); 65 p2.setLayout( new BorderLayout()); 66 p2.add(jpState, BorderLayout.WEST); 67 p2.add(jpZip, BorderLayout.CENTER); 68 69 // Panel p3 for holding jtfCity and p2 70 JPanel p3 = new JPanel(); 71 p3.setLayout( new BorderLayout()); 72 p3.add(jtfCity, BorderLayout.CENTER); 73 p3.add(p2, BorderLayout.EAST); 74 75 // Panel p4 for holding jtfName, jtfStreet, and p3 76 JPanel p4 = new JPanel(); 77 p4.setLayout( new GridLayout( 3 , 1 )); 78 p4.add(jtfName); 79 p4.add(jtfStreet); 80 p4.add(p3); 81 82 // Place p1 and p4 into jpAddress 83 JPanel jpAddress = new JPanel( new BorderLayout()); 84 jpAddress.add(p1, BorderLayout.WEST); 85 jpAddress.add(p4, BorderLayout.CENTER); 86 87 // Set the panel with line border 88 jpAddress.setBorder( new BevelBorder(BevelBorder.RAISED)); 89 90 // Add buttons to a panel 91 JPanel jpButton = new JPanel(); 92 jpButton.add(jbtAdd); 93 jpButton.add(jbtFirst); 94 jpButton.add(jbtNext); 95 jpButton.add(jbtPrevious); 96 jpButton.add(jbtLast); 97 98 // Add jpAddress and jpButton to the frame 99 add(jpAddress, BorderLayout.CENTER); 100 add(jpButton, BorderLayout.SOUTH); 101 102 jbtAdd.addActionListener( new ActionListener() { 103 public void actionPerformed(ActionEvent e) { 104 writeAddress(); 105 } 106 }); 107 jbtFirst.addActionListener( new ActionListener() { 108 public void actionPerformed(ActionEvent e) { 109 try { 110 if (raf.length() > ) readAddress( ); 111 } 112 catch (IOException ex) { 113 ex.printStackTrace(); 114 } 115 } 116 }); 117 jbtNext.addActionListener( new ActionListener() { 118 public void actionPerformed(ActionEvent e) { 119 try { 120 long currentPosition = raf.getFilePointer(); 121 if (currentPosition < raf.length()) 122 readAddress(currentPosition); 123 } 124 catch (IOException ex) { 125 ex.printStackTrace(); 126 } 127 } 128 }); 129 jbtPrevious.addActionListener( new ActionListener() { 130 public void actionPerformed(ActionEvent e) { 131 try { 132 long currentPosition = raf.getFilePointer(); 133 if (currentPosition - 2 * RECORD_SIZE > ) 134 // Why 2 * 2 * RECORD_SIZE? See the follow-up remarks 135 readAddress(currentPosition - 2 * 2 * RECORD_SIZE); 136 else 137 readAddress( ); 138 } 139 catch (IOException ex) { 140 ex.printStackTrace(); 141 } 142 } 143 }); 144 jbtLast.addActionListener( new ActionListener() { 145 public void actionPerformed(ActionEvent e) { 146 try { 147 long lastPosition = raf.length(); 148 if (lastPosition > ) 149 // Why 2 * RECORD_SIZE? See the follow-up remarks 150 readAddress(lastPosition - 2 * RECORD_SIZE); 151 } 152 catch (IOException ex) { 153 ex.printStackTrace(); 154 } 155 } 156 }); 157 158 // Display the first record if exists 159 try { 160 if (raf.length() > ) readAddress( ); 161 } 162 catch (IOException ex) { 163 ex.printStackTrace(); 164 } 165 } 166 167 /** Write a record at the end of the file */ 168 public void writeAddress() { 169 try { 170 raf.seek(raf.length()); 171 FixedLengthStringIO.writeFixedLengthString( 172 jtfName.getText(), NAME_SIZE, raf); 173 FixedLengthStringIO.writeFixedLengthString( 174 jtfStreet.getText(), STREET_SIZE, raf); 175 FixedLengthStringIO.writeFixedLengthString( 176 jtfCity.getText(), CITY_SIZE, raf); 177 FixedLengthStringIO.writeFixedLengthString( 178 jtfState.getText(), STATE_SIZE, raf); 179 FixedLengthStringIO.writeFixedLengthString( 180 jtfZip.getText(), ZIP_SIZE, raf); 181 } 182 catch (IOException ex) { 183 ex.printStackTrace(); 184 } 185 } 186 187 /** Read a record at the specified position */ 188 public void readAddress( long position) throws IOException { 189 raf.seek(position); 190 string name = FixedLengthStringIO.readFixedLengthString( 191 NAME_SIZE, raf); 192 String street = FixedLengthStringIO.readFixedLengthString( 193 STREET_SIZE, raf); 194 String city = FixedLengthStringIO.readFixedLengthString( 195 CITY_SIZE, raf); 196 String state = FixedLengthStringIO.readFixedLengthString( 197 STATE_SIZE, raf); 198 String zip = FixedLengthStringIO.readFixedLengthString( 199 ZIP_SIZE, raf); 200 201 jtfName.setText(name); 202 jtfStreet.setText(street); 203 jtfCity.setText(city); 204 jtfState.setText(state); 205 jtfZip.setText(zip); 206 } 207 208 public static void main(String[] args) { 209 AddressBook frame = new AddressBook(); 210 frame.pack(); 211 frame.setTitle( "AddressBook" ); 212 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 213 frame.setVisible( true ); 214 } 215 } |
A random access file, address.dat , is created to store address information if the file does not yet exist (line 37). If it already exists, the file is opened. A random file object, raf , is used for both write and read operations. The size of each field in the record is fixed and therefore defined as a constant in lines 9 “15.
The user interface is created in lines 44 “100. The listeners are registered in lines 102 “156. When the program starts, it displays the first record, if it exists, in lines 159 “164.
The writeAddress() method sets the file pointer to the end of the file (line 170) and writes a new record to the file (lines 171 “180).
The readAddress() method sets the file pointer at the specified position (line 189) and reads a record from the file (lines 190 “199). The record is displayed in lines 201 “205.
To add a record, you need to collect the address information from the user interface and write the address into the file (line 104).
The code to process button events is implemented in lines 102 “156. For the First button, read the record from position (line 110). For the Next button, read the record from the current file pointer (line 122). When a record is read, the file pointer is moved 2 * RECORD_SIZE number of bytes ahead of the previous file pointer. For the Previous button, you need to display the record prior to the one being displayed now. So you have to move the file pointer two records before the current file pointer (line 135). For the Last button, read the record from the position at raf.length() - 2 * RECORD_SIZE .