File Handling
4.5 Random Access Files
Serial access fi les are simple to handle and are quite widely used in small-scale applications or as a means of providing temporary storage in larger-scale applica- tions. However, they do have two distinct disadvantages, as noted below.
(i) We can’t go directly to a specifi c record. In order to access a particular record, it is necessary to physically read past all the preceding records. For applications containing thousands of records, this is simply not feasible.
(ii) It is not possible to add or modify records within an existing fi le. (The whole fi le would have to be re-created!)
Random access fi les (probably more meaningfully called direct access fi les) overcome both of these problems, but do have some disadvantages of their own…
(i) In common usage, all the (logical) records in a particular fi le must be of the same length.
(ii) Again in common usage, a given string fi eld must be of the same length for all records on the fi le.
(iii) Numeric data is not in human-readable form.
However, the speed and fl exibility of random access fi les often greatly outweigh the above disadvantages. Indeed, for many real-life applications, there is no realistic alternative to some form of direct access.
To create a random access fi le in Java, we create a RandomAccessFile object. The constructor takes two arguments:
• a string or File object identifying the fi le;
• a string specifying the fi le’s access mode.
The latter of these may be either “r” (for read-only access) or “rw” (for read-and- write access). For example:
RandomAccessFile ranFile =
new RandomAccessFile("accounts.dat","rw");
Before reading or writing a record, it is necessary to position the fi le pointer . We do this by calling method seek , which requires a single argument specifying the byte position within the fi le. Note that the fi rst byte in a fi le is byte 0 . For example:
ranFile.seek(500);
//Move to byte 500 (the 501st byte).
In order to move to the correct position for a particular record, we need to know two things:
• the size of records on the fi le;
• the algorithm for calculating the appropriate position.
The second of these two factors will usually involve some kind of hashing func- tion that is applied to the key fi eld. We shall avoid this complexity and assume that
records have keys 1, 2, 3,… and that they are stored sequentially. However, we still need to calculate the record size. Obviously, we can decide upon the size of each String fi eld ourselves. For numeric fi elds, though, the byte allocations are fi xed by Java (in a platform-independent fashion) and are as shown below .
int 4 bytes long 8 bytes fl oat 4 bytes double 8 bytes
Class RandomAccessFile provides the following methods for manipulating the above types:
readInt, readLong, readFloat, readDouble
writeInt, writeLong, writeFloat, writeDouble
It also provides similarly-named methods for manipulating the other primitive types. In addition, it provides a method called writeChars for writing a (variable- length) string. Unfortunately, no methods for reading/writing a string of fi xed size are provided, so we need to write our own code for this. In doing so, we shall need to make use of methods readChar and writeChar for reading/writing the primitive type char .
Example
Suppose we wish to set up an accounts fi le with the following fi elds:
• account number ( long );
• surname ( String );
• initials ( String );
• balance ( fl oat ).
N.B. When calculating the number of bytes for a String fi eld, do not make the mistake of allocating only one byte per character. Remember that Java is based on the unicode character set, in which each character occupies two bytes.
Now let’s suppose that we decide to allocate 15 (unicode) characters to surnames and 3 (unicode) characters to initials. This means that each surname will be allo- cated 30 (i.e., 15 × 2) bytes and each set of initials will be allocated 6 (i.e., 3 × 2) bytes. Since we know that a long occupies precisely 8 bytes and a fl oat occupies precisely 4 bytes, we now know that record size = (8 + 30 + 6 + 4) bytes = 48 bytes.
Consequently, we shall store records starting at byte positions 0, 48, 96, etc. The formula for calculating the position of any record on the fi le is then:
(Record No. –1) × 48
For example, suppose our RandomAccessFile object for the above accounts fi le is called ranAccts . Then the code to locate the record with account number 5 is:
ranAccts.seek(192); //(5–1)x48 = 192
Since method length returns the number of bytes in a fi le, we can always work out the number of records in a random access fi le by dividing the size of the fi le by
the size of an individual record. Consequently, the number of records in fi le ranAccts at any given time = ranAccts.length()/48 .
Now for the code…
import java.io.*;
import java.util.*;
public class RanFile1 {
private static fi nal int REC_SIZE = 48;
private static fi nal int SURNAME_SIZE = 15;
private static fi nal int NUM_INITS = 3;
private static long acctNum = 0;
private static String surname, initials;
private static fl oat balance;
public static void main(String[] args)
throws IOException {
RandomAccessFile ranAccts =
new RandomAccessFile("accounts.dat", "rw");
Scanner input = new Scanner(System.in);
String reply = "y";
do {
acctNum++;
System.out.println(
"\nAccount number " + acctNum + ".\n");
System.out.print("Surname: ");
surname = input.nextLine();
System.out.print("Initial(s): ");
initials = input.nextLine();
System.out.print("Balance: ");
balance = input.nextFloat();
//Now get rid of carriage return(!)…
input.nextLine();
writeRecord(ranAccts); //Method defi ned below.
System.out.print(
"\nDo you wish to do this again (y/n)? ");
reply = input.nextLine();
}while (reply.equals("y")||reply.equals("Y"));
System.out.println();
showRecords(ranAccts); //Method defi ned below.
}
public static void writeRecord(RandomAccessFile fi le) throws IOException
{
//First fi nd starting byte for current record…
long fi lePos = (acctNum-1) * REC_SIZE;
//Position fi le pointer…
fi le.seek(fi lePos);
//Now write the four (fi xed-size) fi elds.
//Note that a defi nition must be provided //for method writeString…
fi le.writeLong(acctNum);
writeString(fi le, surname, SURNAME_SIZE);
writeString(fi le, initials, NUM_INITS);
fi le.writeFloat(balance);
}
public static void writeString(RandomAccessFile fi le, String text, int fi xedSize) throws IOException
{
int size = text.length();
if (size<=fi xedSize) {
fi le.writeChars(text);
//Now 'pad out' the fi eld with spaces…
for (int i=size; i<fi xedSize; i++) fi le.writeChar(' ');
}
else //String is too long!
fi le.writeChars(text.substring(0,fi xedSize));
//Write to fi le the fi rst fi xedSize characters of //string text, starting at byte zero.
}
public static void showRecords(RandomAccessFile fi le) throws IOException
{
long numRecords = fi le.length()/REC_SIZE;
fi le.seek(0); //Go to start of fi le.
for (int i=0; i<numRecords; i++) {
acctNum = fi le.readLong();
surname = readString(fi le, SURNAME_SIZE);
//readString defi ned below.
initials = readString(fi le, NUM_INITS);
balance = fi le.readFloat();
System.out.printf("" + acctNum
+ " " + surname
+ " " + initials + " "
+ "%.2f %n",balance);
} }
public static String readString(
RandomAccessFile fi le, int fi xedSize) throws IOException
{
String value = ""; //Set up empty string.
for (int i=0; i<fi xedSize; i++)
//Read character and concatenate it onto value…
value+=fi le.readChar();
return value;
} }
Note that methods readString and writeString above may be used without modifi - cation in any Java program that needs to transfer strings from/to a random access fi le.
The following screenshot demonstrates the operation of this program (Fig. 4.3 ).
Fig. 4.3 Creating a simple random access fi le and displaying its contents
The above example does not adequately demonstrate the direct access capabilities of a RandomAccessFile object, since we have processed the whole of the fi le from start to fi nish, dealing with records in the order in which they are stored on the fi le.
We should also be able to retrieve individual records from anywhere in the fi le and/
or make modifi cations to those records. The next example shows how this can be done for our accounts fi le.
Example
//Allows the user to retrieve individual account //records and modify their balances.
import java.io.*;
import java.util.*;
public class RanFile2 {
private static fi nal int REC_SIZE=48;
private static fi nal int SURNAME_SIZE=15;
private static fi nal int NUM_INITS=3;
private static long acctNum=0;
private static String surname, initials;
private static fl oat balance;
public static void main(String[] args)
throws IOException {
Scanner input = new Scanner(System.in);
RandomAccessFile ranAccts =
new RandomAccessFile("accounts.dat", "rw");
long numRecords = ranAccts.length()/REC_SIZE;
String reply;
long currentPos; //File pointer position.
do {
System.out.print("\nEnter account number: ");
acctNum = input.nextLong();
while ((acctNum<1) || (acctNum>numRecords)) {
System.out.println(
"\n*** Invalid number! ***\n");
System.out.print(
"\nEnter account number: ");
acctNum = input.nextLong();
}
showRecord(ranAccts); //Defi ned below.
System.out.print("\nEnter new balance: ");
balance = input.nextFloat();
input.nextLine();
//Get rid of carriage return!
currentPos = ranAccts.getFilePointer();
ranAccts.seek(currentPos-4); //Back 4 bytes.
ranAccts.writeFloat(balance);
System.out.print(
"\nModify another balance (y/n)? ");
reply = (input.nextLine()).toLowerCase();
}while (reply.equals("y"));
//(Alternative to method in previous example.) ranAccts.close();
}
public static void showRecord(RandomAccessFile fi le) throws IOException {
fi le.seek((acctNum-1)*REC_SIZE);
acctNum = fi le.readLong();
surname = readString(fi le, SURNAME_SIZE);
initials = readString(fi le, NUM_INITS);
balance = fi le.readFloat();
System.out.println("Surname: " + surname);
System.out.println("Initials: " + initials);
System.out.printf("Balance: %.2f %n",balance);
}
public static String readString(
RandomAccessFile fi le, int fi xedSize) throws IOException {
//Set up empty buffer before reading from fi le…
StringBuffer buffer = new StringBuffer();
for (int i=0; i<fi xedSize; i++)
//Read character from fi le and append to buffer.
buffer.append(fi le.readChar());
return buffer.toString(); //Convert into String.
} }