FileChannel Objects in Java, Records with Mixed Types

Baldwin shows you how to create records consisting of mixed primitive types, how to manipulate those records under program control, and how to transfer those records between the computer's memory and a physical disk file.

Published:  November 19, 2002
By Richard G. Baldwin

Java Programming Notes # 1792


Preface

New features in SDK Version 1.4.0

The recently released JavaTM 2 SDK, Standard Edition Version 1.4 contains a large number of new features, including the concept of IO channels.  The first lesson in this miniseries, entitled FileChannel Objects in Java, Background Information, introduced you to the concept of channels from a read/write IO viewpoint. 

Mixed primitive types

The previous lesson, entitled FileChannel Objects in Java, ByteBuffer Type, showed you how to use view objects to read and write different primitive types into disk files, where all of the data was of the same type.

In this lesson, I will show you how to use the FileChannel class along with the ByteBuffer class to:

Memory-mapped IO

Future lessons will teach you how to do memory-mapped IO using channels.

Viewing tip

You may find it useful to open another copy of this lesson in a separate browser window.  That will make it easier for you to scroll back and forth among the different listings and figures while you are reading about them.

Supplementary material

I recommend that you also study the other lessons in my extensive collection of online Java tutorials.  You will find those lessons published at Gamelan.com.  However, as of the date of this writing, Gamelan doesn't maintain a consolidated index of my Java tutorial lessons, and sometimes they are difficult to locate there.  You will find a consolidated index at www.DickBaldwin.com.

What is a FileChannel?

Sun describes an object of the FileChannel class simply as "A channel for reading, writing, mapping, and manipulating a file."

Discussion and Sample Code

Will discuss sample program in fragments

I will illustrate the FileChannel class using the sample program named Channel03.  As is my normal approach, I will discuss this program in fragments.  You will find a complete listing of the program in Listing 12 near the end of the lesson.

Reading and writing records

This program, which was tested using Java SDK version 1.4.0 under Win2000, illustrates the use of FileChannel objects to write, read, and manipulate records containing data of mixed primitive types.

The program writes and then reads several data records where each record contains a char, a byte, and a float.

Beginning is similar to previous programs

To keep things simple, the program consists of a main method and several static convenience methods.  The beginning of the main method is shown in Listing 1.

This program begins pretty much the same as the program in the previous lesson.

  public static void main(
                        String[] args){
 
    //Create a ByteBuffer with a 
    // capacity of 56 bytes, and all
    // elements initialized to zero.
    ByteBuffer bBuf = 
        ByteBuffer.wrap(new byte[56]);

    //Declare varables for use later
    FileOutputStream oStr;
    FileInputStream iStr;
    FileChannel fileChan;

    try{
      // Set ByteBuffer limit to 
      // capacity
      bBuf.limit(bBuf.capacity());

Listing 1

Because of the similarity of the code in Listing 1 to the code in the previous lesson, I won't discuss this fragment on a step-by-step basis.  Rather, I will let the comments in the code speak for themselves, and refer you to the previous lesson if you have questions.

The flow of this program departs from the flow of the program in the previous lesson in Listing 2, so that is where I will begin my detailed discussion

Create and store a record

The code in Listing 2 creates a record consisting of three primitive values and stores the record in the ByteBuffer object.  The types of the three values are char, byte, and float respectively.

      char cData = 'A';
      byte bData = 14;
      float fData = (float)1.0/3;
      putRecord(
               bBuf,cData,bData,fData);

Listing 2

How does it work?

After declaring and populating three variables of types char, byte, and float, the code in Listing 2 invokes the method named putRecord to create the record and store it in the ByteBuffer object.

The putRecord method

The method named putRecord is probably the most significant thing in this lesson, so I will explain its behavior at this point in the discussion.  Following that explanation, I will return to the flow of control in the main method.

The entire method named putRecord is shown in Listing 3.

  static void putRecord(
                       ByteBuffer bBuf,
                       char cData,
                       byte bData,
                       float fData){
    bBuf.putChar(cData);
    bBuf.put(bData);
    bBuf.putFloat(fData);
  }//end putRecord

Listing 3

Incoming parameters of putRecord method

The method named putRecord receives four incoming parameters.  The first parameter is a reference to a ByteBuffer object, which is the object into which the record is to be stored.

The following three parameters are the three primitive values that are to make up the record.

The put methods of the ByteBuffer class

The record is created and stored in the buffer by successively invoking the following three methods on the ByteBuffer object, passing one of the three primitive values to each of the three methods.

There are many different put methods

As of version 1.4.0, there are about eighteen different methods in the ByteBuffer class that begin with the word put.  As you might guess, these methods are used to store data in a ByteBuffer object.

The relative put methods

There are two overloaded versions of the following method name:

The first of these is the method used in Listing 3 above, which takes a single parameter.  This version can be described as follows:

Relative put method for writing a char value.  Writes two bytes containing the given char value, in the current byte order, into this buffer at the current position, and then increments the position by two. The incoming parameter is the char value to be written.

The absolute put methods

Although I didn't use it in this program, you may also be interested in a description of the other overloaded version of this method.  This version takes two parameters, where one parameter is a char value and the other parameter is an index.

Absolute put method for writing a char value.  Writes two bytes containing the given char value, in the current byte order, into this buffer at the given index.  The first parameter is the index at which the bytes will be written.  The second parameter is the char value to be written.

The put methods for different primitive types

Two overloaded versions, (relative and absolute), of the following seven method names are provided by the ByteBuffer class (additional overloaded versions of the method named put are also provided for use with type byte):

All primitive types other than boolean ...

Thus, the ByteBuffer class makes it easy for you to store data of all primitive types (other than boolean) in a ByteBuffer object. 

If you use the relative version of the put... method, the value of the position property will be properly incremented for the type of data being stored during each put operation.

(If you mix data types in the buffer, you must be careful to make certain that you retrieve them in the same order that you store them.  Otherwise, you will get the bytes scrambled among the different primitive values.)

The putRecord method stores three primitive types

Each time the method named putRecord in Listing 3 is invoked, it stores three primitive values (in the order char, byte, and float) into the ByteBuffer object.

How many bytes are required for a record?

The char type requires two bytes.  The byte type requires one byte, and the float type requires four bytes.  Thus, each record, consisting of the three values, requires seven bytes for storage. 

Therefore, each time the putRecord method in Listing 3 is invoked, the value of the location property of the ByteBuffer object is advanced by seven bytes. 

Assuming that the location property has a value of zero at the beginning, each group of seven bytes contains a record as defined in this program.

Of course, you could define your records to contain any combination of primitive types (other than boolean) in any order, so your records may have a different length.

Now back to the flow in main ...

Now that you understand the behavior of the putRecord method, let's return to the flow of control in the main method.

Create and store another record

The code in Listing 4 simply creates another record and adds it to the ByteBuffer object.

      cData = 'B';
      bData = 28;
      fData = (float)2.0/3;
      putRecord(
               bBuf,cData,bData,fData);

Listing 4

When the putRecord method returns from this second invocation, two records, consisting of fourteen data bytes will have been stored in the ByteBuffer object.

Prepare properties for a disk write operation

In preparation for writing these two records to a disk file, the code in Listing 5 sets the limit property of the ByteBuffer object to the current value of the position property, and then sets the value of the position property to zero.

      bBuf.limit(bBuf.position());
      bBuf.position(0);

Listing 5

This has the effect of causing the data written to the disk file to start at the beginning of the ByteBuffer object and to be limited to the number of bytes stored in the ByteBuffer object.

(This is as opposed to writing the entire ByteBuffer object to the disk file based on its capacity.)

Display the records

The code in Listing 6 invokes the showRecords method to display the records stored in the ByteBuffer object.

      showRecords(bBuf);

Listing 6

The showRecords method

At this point, I will depart from the main method and explain the behavior of the showRecords method, as shown in Listing 7.

  static void showRecords(
                      ByteBuffer bBuf){
    
    //Save position
    int pos = bBuf.position();
    //Set position to zero
    bBuf.position(0);
    System.out.println(
                   "Records");

    while(bBuf.hasRemaining()){
      System.out.println(
                bBuf.getChar() + " " + 
                bBuf.get() + " " + 
                bBuf.getFloat());
    }//end while loop

    System.out.println();//new line
    //Restore position and return
    bBuf.position(pos);
  }//end showRecords

Listing 7

If you understood the explanation of the relative put methods earlier, the code in Listing 7 should be easy to understand.

The iterative loop

The code in Listing 7 that is new to this lesson is the boldface iterative loop in the center of the listing.  The code before and after the iterative loop is the same as code that I have explained in previous lessons.

The get methods of the ByteBuffer class

As is the case with the put methods, the ByteBuffer class provides the following methods for retrieving primitive data, by type, from a ByteBuffer object:

Relative and absolute versions of the get methods

As with the put methods, there is a relative version and an absolute version of each of these methods.  For example, the relative version of the getChar() method can be described as follows:

Relative get method for reading a char value.  Reads the next two bytes at this buffer's current position, composing them into a char value according to the current byte order, and then increments the position by two.  Returns the char value at the buffer's current position.

Retrieving primitive data from a ByteBuffer object

Thus, the ByteBuffer class makes it easy for you to retrieve data of all primitive types (other than boolean) from a ByteBuffer object. 

If you use the relative version of the appropriate get... method, the value of the position property will be properly incremented for the type of data being retrieved during each get operation.

(If you mix data types in the buffer, you must make certain that you retrieve the primitive values in the same order that you store them.  Otherwise, you will get the bytes scrambled among the different primitive values.)

Output data

Now that you understand the behavior of the showRecords method, I will return to the flow of control in the main method.  First however, I need to tell you that the output produced by the invocation of the showRecords method at this point in the program is shown in Figure 1.

Records
A 14 0.33333334
B 28 0.6666667

Figure 1

Figure 1 displays one record on each line of output, where each record consists of a char, a byte, and a float (Hopefully, this is what you expected each record to look like.)

Get a FileChannel object for output

Returning to the flow of control in the main method, Listing 8 uses code similar to code that I have discussed in previous lessons to get a FileChannel object for output to a disk file named junk.txt.

      oStr = new FileOutputStream(
                           "junk.txt");
      fileChan = oStr.getChannel();

Listing 8

Write the file and display the file size

Similarly, Listing 9 uses code that I have explained in previous lessons to:

      System.out.println(
               "Bytes written = " 
               + fileChan.write(bBuf));
      //Close stream and channel and
      // display file size
      System.out.println(
                      "File length = " 
                    + fileChan.size());
      oStr.close();
      fileChan.close();

Listing 9

(In the programs in previous lessons, I used a File object to get an independent report on the size of the file.  In this program, I used the size() method of the FileChannel class to get the size of the file.)

The file size output

The code in Listing 9 produces the output shown in Figure 2.

Bytes written = 14
File length = 14

Figure 2

Figure 2 shows the number of bytes that we would expect to be written based on our previous analysis of the number of bytes required to store each of the two records.

Clear the buffer and display its contents

The code in Listing 10 sets the value of each byte in the ByteBuffer object to zero, and then displays the ByteBuffer object in terms of records.

      clearByteBufferData(bBuf,"bBuf");

      showRecords(bBuf);

Listing 10

The code in Listing 10 produces the output shown in Figure 3.

Clear bBuf
Records
  0 0.0
  0 0.0

Figure 3

(There is a space at the beginning of each record because a char value of zero doesn't represent a printable character.)

Read the file and display the data

The code in Listing 11:

      //Get FileChannel for input
      iStr = new FileInputStream(
                           "junk.txt");
      fileChan = iStr.getChannel();

      //Read data from disk file into
      // ByteBuffer.  Then display 
      // records in the ByteBuffer
      System.out.println(
                "Bytes read = " 
                + fileChan.read(bBuf));
      //Close stream and channel and
      // display file size
      System.out.println(
                      "File length = " 
                    + fileChan.size());
      iStr.close();
      fileChan.close();
      
      //Display records
      showRecords(bBuf);

Listing 11

The output produced by the code in Listing 11 is shown in Figure 4.

Bytes read = 14
File length = 14
Records
A 14 0.33333334
B 28 0.6666667

Figure 4

Recap

To recap, this program:

Happily, the output shown in Figure 4 matches the original contents of the two records, confirming that there was no data corruption during the round trip.

That's it for now

By now you should understand a quite a lot about the use of the FileChannel class and the ByteBuffer class to create records containing mixed primitive types, to store those records in a disk file, and to manipulate the records after reading them from the disk file.

Run the Program

If you haven't already done so, I encourage you to copy the code from Listing 12 into your text editor, compile it, and execute it.  Experiment with it, making changes, and observing the results of your changes.

Remember, however, that you must be running Java version 1.4.0 or later to compile and execute this program.

Summary

In this lesson, I have taught you how to use the FileChannel class along with the ByteBuffer class to:

What's Next?

In the next lesson, I will teach you how to use the FileChannel class to perform memory-mapped IO.

Future plans

As time goes on, I plan to publish additional lessons that will help you learn to use other new IO features including:

Complete Program Listing

A complete listing of the program discussed in this lesson is shown in Listing 12 below.
 
/* File Channel03.java
Copyright 2002, R.G.Baldwin
Revised 9/23/02

Illustrates use of FileChannel objects
to write and read data of different 
types from a disk file.

Writes and reads data where the
ByteBuffer contains records of mixed
types.  Each record contains a char,
a byte, and a float.

Tested using JDK 1.4.0 under Win2000

The output is:

Records
A 14 0.33333334
B 28 0.6666667

Bytes written = 14
File length = 14
Clear bBuf
Records
  0 0.0
  0 0.0

Bytes read = 14
File length = 14
Records
A 14 0.33333334
B 28 0.6666667
**************************************/

import java.io.*;
import java.nio.channels.*;
import java.nio.*;

class Channel03{
  public static void main(
                        String[] args){
 
    //Create a ByteBuffer with a 
    // capacity of 56 bytes, and all
    // elements initialized to zero.
    ByteBuffer bBuf = 
        ByteBuffer.wrap(new byte[56]);

    //Declare varables for use later
    FileOutputStream oStr;
    FileInputStream iStr;
    FileChannel fileChan;

    try{
      // Set ByteBuffer limit to 
      // capacity
      bBuf.limit(bBuf.capacity());
              
      //Store two records of mixed data
      // types in the ByteBuffer
      char cData = 'A';
      byte bData = 14;
      float fData = (float)1.0/3;
      putRecord(
               bBuf,cData,bData,fData);
      cData = 'B';
      bData = 28;
      fData = (float)2.0/3;
      putRecord(
               bBuf,cData,bData,fData);
      
      //Set limit to position and then
      // set position to 0
      bBuf.limit(bBuf.position());
      bBuf.position(0);
      
      //Display records in ByteBuffer
      showRecords(bBuf);
    
      //Get FileChannel for output
      oStr = new FileOutputStream(
                           "junk.txt");
      fileChan = oStr.getChannel();

      //Write output data from the
      // ByteBuffer to the disk file.
      System.out.println(
               "Bytes written = " 
               + fileChan.write(bBuf));
      //Close stream and channel and
      // display file size
      System.out.println(
                      "File length = " 
                    + fileChan.size());
      oStr.close();
      fileChan.close();

      //Clear the ByteBuffer
      clearByteBufferData(bBuf,"bBuf");

      //Display the ByteBuffer to 
      // confirm that it has been
      // cleared.
      showRecords(bBuf);

      //Get FileChannel for input
      iStr = new FileInputStream(
                           "junk.txt");
      fileChan = iStr.getChannel();

      //Read data from disk file into
      // ByteBuffer.  Then display 
      // records in the ByteBuffer
      System.out.println(
                "Bytes read = " 
                + fileChan.read(bBuf));
      //Close stream and channel and
      // display file size
      System.out.println(
                      "File length = " 
                    + fileChan.size());
      iStr.close();
      fileChan.close();
      
      //Display records
      showRecords(bBuf);

    }catch(Exception e){
      System.out.println(e);}
  }// end main

  //---------------------------------//
  
  static void clearByteBufferData(
          ByteBuffer buf, String name){
    //Stores 0 in each element of a
    // byte buffer.
    
    //Set position to zero
    buf.position(0);
    System.out.println(
                      "Clear " + name);
    while(buf.hasRemaining()){
      buf.put((byte)0);
    }//end while loop
    //Set position to zero and return
    buf.position(0);
  }//end clearByteBufferData
  //---------------------------------//
  
  static void putRecord(
                       ByteBuffer bBuf,
                       char cData,
                       byte bData,
                       float fData){
    bBuf.putChar(cData);
    bBuf.put(bData);
    bBuf.putFloat(fData);
  }//end putRecord
  //---------------------------------//
  static void showRecords(
                      ByteBuffer bBuf){
    
    //Save position
    int pos = bBuf.position();
    //Set position to zero
    bBuf.position(0);
    System.out.println(
                   "Records");
    while(bBuf.hasRemaining()){
      System.out.println(
                bBuf.getChar() + " " + 
                bBuf.get() + " " + 
                bBuf.getFloat());
    }//end while loop
    System.out.println();//new line
    //Restore position and return
    bBuf.position(pos);
  }//end showDoubleBufferData

  //---------------------------------//
}//end class Channel03 definition

Listing 12


Copyright 2002, Richard G. Baldwin.  Reproduction in whole or in part in any form or medium without express written permission from Richard Baldwin is prohibited.

About the author

Richard Baldwin is a college professor (at Austin Community College in Austin, TX) and private consultant whose primary focus is a combination of Java, C#, and XML. In addition to the many platform and/or language independent benefits of Java and C# applications, he believes that a combination of Java, C#, and XML will become the primary driving force in the delivery of structured information on the Web.

Richard has participated in numerous consulting projects and he frequently provides onsite training at the high-tech companies located in and around Austin, Texas.  He is the author of Baldwin's Programming Tutorials, which has gained a worldwide following among experienced and aspiring programmers. He has also published articles in JavaPro magazine.

Richard holds an MSEE degree from Southern Methodist University and has many years of experience in the application of computer technology to real-world problems.

Baldwin@DickBaldwin.com

-end-