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.
-end-