Java FileChannel Objects in Java, Records with Mixed Types

FileChannel Objects in Java, Records with Mixed Types

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:

  • Create records consisting of sequences of data values of mixed primitive
    types
  • Manipulate those records under program control
  • Transfer those records between the computer’s memory and a physical disk
    file

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.

  • putChar(char value)
  • put(byte value)
  • putFloat(float value)

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:

  • putChar(char value)
  • putChar(int index, char value)

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):

  • put
  • putChar
  • putDouble
  • putFloat
  • putInt
  • putLong
  • putShort

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:

  • get
  • getChar
  • getDouble
  • getFloat
  • getInt
  • getLong
  • getShort

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:

  • Write the data from the ByteBuffer to the disk file
  • Close the output stream and the FileChannel object
  • Display the number of bytes written to the file
  • Display the size of 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();

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:

  • Gets a FileChannel object for input for the disk file named
    junk.txt
  • Reads the data from the disk file into the ByteBuffer object
  • Closes the input stream and the FileChannel object
  • Displays the number of bytes read, along with the size of the disk file
  • Displays the records read into the ByteBuffer object
      //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:

  • Creates a ByteBuffer object
  • Creates two data records each consisting of a char value, a byte
    value, and a float value and stores the two records in the
    ByteBuffer
    object
  • Uses a FileChannel object to write the records in the ByteBuffer
    object to a disk file
  • Clears the ByteBuffer object
  • Uses a FileChannel object to read the records from the disk file
    back into the ByteBuffer object
  • Displays the two records in the ByteBuffer object

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:

  • Create records consisting of sequences of data values of mixed primitive
    types
  • Manipulate those records under program control
  • Transfer those records between the computer’s memory and a physical file

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:

  • File locks
  • Character-set encoders and decoders
  • Pattern matching on files using regular expressions
  • Socket channels for both clients and servers
  • Non-blocking reads
  • Non-blocking servers

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.

[email protected]

-end-

 

Latest Posts

Related Stories