WELCOME to the Java Developer ConnectionSM (JDC)
Tech Tips, May 09, 2000.
This issue of the JDC Tech Tips is written by Glen McCluskey.
These tips were developed using JavaTM 2 SDK, Standard Edition,
v 1.2.2, and are not guaranteed to work with other versions.
RANDOM ACCESS FOR FILES
If you've used the standard Java package java.io
, you're probably
familiar with I/O classes such as InputStream
and BufferedReader
.
These classes support sequential I/O on files. That is, you read
a file starting at the beginning, or write a file from its
beginning or by appending to it.
The class java.io.RandomAccessFile
operates a little differently.
It supports random access, that is, access where you can set
a file pointer to an arbitrary offset (represented as a 64-bit
long value), and then perform I/O from that position.
Unlike classes such as FileInputStream, RandomAccessFile
is not
part of the InputStream/OutputStream
hierarchy; you can't say:
InputStream istr = new RandomAccessFile(...);
RandomAccessFile
identifies file locations according to byte
offsets. In the JavaTM language, bytes and characters are
distinct. A character is made up of two bytes, so a particular
byte offset in a file of characters doesn't necessarily represent
a location of a character.
The RandomAccessFile
class implements the DataInput
and DataOutput
interfaces. These support methods like readInt
and writeUTF
to read
and write standard data types in a uniform way. For example, if you
use writeInt
to write an integer to a file, you can then use readInt
to read the integer back from the file, with byte-ordering issues
automatically handled for you.
To see how RandomAccessFile
might be useful, consider an application
with a large legacy database of fixed-length records. These records
are accessed in random order. For example, the records might
represent a hash table on disk, or some type of complex linked data
structure. The application needs to read a specific record in the
database without having to read all the records before it.
Here is an illustration of this application using two programs, the
first is a C program that writes a database. The database represents
the legacy part of the application. The format of records in the
database is:
number of records as a two-byte short
name of a person, up to 25 bytes,
unused bytes 0 filled
birthdate month as a two-byte short
birthdate day as a two-byte short
birthdate year as a two-byte short
...
The C program looks like this:
#include
#include
/* structure of a record */
struct Rec {
char* name;
short month;
short day;
short year;
};
/* birthdate/name record */
struct Rec data[] = {
{"Jane Jones", 3, 17, 1959},
{"Bill Smith", 2, 27, 1947},
{"Maria Thomas", 12, 23, 1954},
{"Mortimer Smedley Williams",
9, 24, 1957},
{"Jennifer Garcia Throckmorton",
11, 9, 1963}
};
short NUMVALUES = sizeof(data) /
sizeof(struct Rec);
/* write a short to a file */
void writeShort(FILE* fp, short s) {
fputc((s >> 8) & 0xff, fp);
fputc(s & 0xff, fp);
}
/* write a sequence of
bytes to a file */
void writeBytes(FILE* fp, char* buf, size_t n) {
fwrite(buf, 1, n, fp);
}
int main() {
int i;
/* open the output file */
FILE* fpout = fopen("out.data", "wb");
if (fpout == NULL) {
fprintf(stderr, "Cannot open
output file\n");
return 1;
}
/* write out the number of values */
writeShort(fpout, NUMVALUES);
/* write the data to the file */
for (i = 0; i < NUMVALUES; i++) {
struct Rec* p = &data[i];
char outbuf[25];
/* write the name, truncating
if necessary */
strncpy(outbuf, p->name,
sizeof outbuf);
writeBytes(fpout, outbuf,
sizeof outbuf);
/* write month/day/year */
writeShort(fpout, p->month);
writeShort(fpout, p->day);
writeShort(fpout, p->year);
}
fclose(fpout);
return 0;
}
You need to be careful about byte ordering when you write data to
a file for later reading by another application. For example, when
the above program writes out short values to the file, it must
ensure that the two bytes of the short are written in the order
that RandomAccessFile.readShort
expects them--high byte first,
then low byte. You can't use readShort to read a legacy database
that has values whose bytes are reversed--low byte first, then
high byte. You'd need to read raw bytes and assemble the short
value yourself.
The Java program that reads the file looks like this:
import java.io.RandomAccessFile;
import java.io.IOException;
public class RAFDemo {
// starting offset in data file,
// past the count of records
static final int STARTING_OFFSET = 2;
// length of a name
static final int NAME_LENGTH = 25;
// bytes in a record (name length +
three shorts)
static final int BYTES_IN_RECORD =
NAME_LENGTH + 2 + 2 + 2;
public static void main(String args[])
throws IOException {
RandomAccessFile raf =
new RandomAccessFile("
out.data", "r");
// read the number of data records
short numvalues = raf.readShort();
byte namebuf[] = new byte[NAME_LENGTH];
// read each record, going backwards
// through the file
for (int i = numvalues - 1; i >= 0; i--) {
// seek to the record
raf.seek(STARTING_OFFSET + i *
BYTES_IN_RECORD);
// read the name as a vector of bytes
raf.read(namebuf);
// convert the name to a string
StringBuffer namesb = new StringBuffer();
for (int j = 0; j < NAME_LENGTH; j++) {
if (namebuf[j] == 0) {
break;
}
else {
namesb.append((char)namebuf[j]);
}
}
// read month/day/year
short month = raf.readShort();
short day = raf.readShort();
short year = raf.readShort();
// display the results
System.out.println(
namesb.toString() + " " +
month + " " + day + "
" + year);
}
}
}
To demonstrate that it can access parts of the database at
random, the program reads the file records in backwards order
and then displays the records. If you run the program, output is:
Jennifer Garcia Throckmor 11 9 1963
Mortimer Smedley Williams 9 24 1957
Maria Thomas 12 23 1954
Bill Smith 2 27 1947
Jane Jones 3 17 1959
The first name has been truncated to 25 characters, to fit the
requirements of record layout stated above.
One issue related to RandomAccessFile
concerns strings. The
RAFDemo
example just presented reads a legacy database. Strings
in a database record, such as "Jane Jones", are represented
in RAFDemo
as a sequence of bytes. To convert the bytes to
a string, the program converts each byte to a character and then
appends it to a StringBuffer
.
Suppose, however, that you want to use RandomAccessFile
with full
16-bit Unicode characters. How can you do this? One way is to use
the readUTF
and writeUTF
methods found in DataInput
and
DataOutput
. These represent strings as UTF sequences; 7-bit ASCII
characters are represented as themselves, and other characters as
two or three bytes. The methods read or write two bytes of length
information, followed by the UTF representation as a stream of
bytes. Because the length is stored as two bytes, you cannot write
extremely long strings this way.
Another approach to writing strings is to use the writeChars
method. This method writes a sequence of characters (there's no
readChars
method). If you use writeChars
, you need to write out
the string length first using writeInt
.
If you use RandomAccessFile
to access fixed-length records
containing strings, you need to determine how you're going to
represent the records. The complication is that strings are
typically variable in length. You need to either truncate strings,
so that all are a fixed length (as in the example above), or
represent the strings in a separate file and record string numbers
in the actual records. For example, you might use an integer field
in a fixed-length record to store a value "37", where 37 represents
the 37th string in a separate file.
For further information about java.io.RandomAccessFile, see "The
JavaTM Language Specification" section 22.23. Also see
"Java 2TM Platform, Standard Edition, v 1.2.2 API: Specification: Class RandomAccessFile
"
USING ADAPTERS
Suppose that you're doing some programming in the Java programming language,
and you have a class that implements an interface. Let's call the
interface A:
interface A {
public void f1();
public void f2();
public void f3();
public void f4();
public void f5();
}
The interface has five methods, but you're only interested in the
first one, f1. So you say:
class B implements A {
public void f1() {/*...*/}
}
Unfortunately, this usage is invalid, and results in a compile
error. For a class to implement an interface, it must define all
the interface's methods. If you make B abstract, the error goes
away, but you can't create objects of an abstract class. If you
extend B to a non-abstract class C, you're presented with the
error again.
To deal with this problem, use "adapter" classes. An adapter
class for A looks like this:
abstract class A_ADAPTER implements A {
public void f1() {}
public void f2() {}
public void f3() {}
public void f4() {}
public void f5() {}
}
Notice that the adapter class implements all the methods of the
interface as dummy methods that do nothing. If you want to extend
f1 to provide your own functionality, you then say:
class B extends A_ADAPTER {
public void f1() {/*...*/}
}
In this case, a user of class B gets the overriding version of the
f1 method, and the dummy version of the f2-f5 methods found in the
abstract class A_ADAPTER.
One place where adapters are used is AWT event handling. The
WindowListener
interface specifies seven events that relate to
window handling, and WindowAdapter
defines dummy methods for these
events:
public void windowOpened(WindowEvent e) {}
public void windowClosing(WindowEvent e) {}
public void windowClosed(WindowEvent e) {}
public void windowIconified(WindowEvent e) {}
public void windowDeiconified(WindowEvent e) {}
public void windowActivated(WindowEvent e) {}
public void windowDeactivated(WindowEvent e) {}
WindowAdapter
provides dummy implementations of all the methods
that are specified by WindowListener
. So if you extend the
WindowAdapter
class, you only need to provide implementations
of methods whose default functionality you want to override.
Here's an example that extends WindowAdapter
:
import java.awt.event.*;
import javax.swing.*;
public class AdapterDemo {
public static void main(String args[]) {
// set up a frame
JFrame frame =
new JFrame("AdapterDemo");
// set up listeners for
// window iconification
// and for window closing
frame.addWindowListener(
new WindowAdapter() {
public void windowIconified(
WindowEvent e) {
System.out.println(e);
}
public void windowClosing(
WindowEvent e) {
System.exit(0);
}
});
// set up panels and labels
JPanel panel = new JPanel();
JLabel label =
new JLabel("This is a test");
panel.add(label);
// position and display frame
frame.getContentPane().add(panel);
frame.setSize(300, 200);
frame.setLocation(200, 200);
frame.setVisible(true);
}
}
This particular example sets up listeners for the WindowClosing
and WindowIconified
(minimized) events, and ignores the others.
Notation like:
frame.addWindowListener(new WindowAdapter() {...});
defines an anonymous inner class that extends WindowAdapter
.
Similar adapter classes are used for keyboard and mouse event
handling.
For further information about adapters, see the section "AWT
Adapters" in Chapter 9 of the book Graphic Java--Mastering the
JFC, 3rd Edition, Volume 1, AWT, by David Geary. Also see
"Java 2(tm) Platform, Standard Edition, v 1.2.2 API: Specification:
Class WindowAdapter".
The names on the JDCSM
mailing list
are used for internal Sun MicrosystemsTM
purposes only. To remove your name from the list, see
Subscribe/Unsubscribe
below.
Feedback
Comments? Send your feedback on the JDC Tech Tips to: jdc-webmaster
Subscribe/Unsubscribe
The JDC Tech Tips are sent to you because you elected to
subscribe when you
registered as a JDC member. To unsubscribe from JDC email, go
to the following
address and enter the email address you wish to remove from
the mailing list:
http://developer.java.sun.com/unsubscribe.html
To become a JDC member and subscribe to this newsletter go to:
http://java.sun.com/jdc/