Java Technology Home Page
A-Z Index

Java Developer Connection(SM)
Technical Tips

Downloads, APIs, Documentation
Java Developer Connection
Tutorials, Tech Articles, Training
Online Support
Community Discussion
News & Events from Everywhere
Products from Everywhere
How Java Technology is Used Worldwide
Print Button
 
Tech Tips archive

Tech Tips
May 09, 2000

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/


Print Button
[ This page was updated: 21-Sep-2000 ]
Products & APIs | Developer Connection | Docs & Training | Online Support
Community Discussion | Industry News | Solutions Marketplace | Case Studies
Glossary | Feedback | A-Z Index
For more information on Java technology
and other software from Sun Microsystems, call:
(800) 786-7638
Outside the U.S. and Canada, dial your country's AT&T Direct Access Number first.
Sun Microsystems, Inc.
Copyright © 1995-2000 Sun Microsystems, Inc.
All Rights Reserved. Terms of Use. Privacy Policy.