java.nio.ByteBuffer
is the cornerstone of the nio new I/O package. It is also used for high
performance conversions of byte[] to char[]
and back.
What ByteBuffer is Not
The biggest problem in understanding ByteBuffer is
presuming that it is cleverer than it really is.
- ByteBuffer is a baffling class. It is a bit like a
RAM-based RandomAccessFile. It is also a bit like a ByteArrayList
without the autogrow feature, to let you deal with partly filled byte[]
in a consistent way.
- It looks at first as if it might be a traditional circular squirrel cage buffer
but it is not. There is no circularity. Nor is it some kind of virtual moving
window on a file.
- It is not like a pipe, designed to simulate an giant stream with a finite buffer.
There is no queuing of any kind.
- It is not even as clever as COBOL double buffering, which you might suspect by
it having a flip method.
- It has no asynchronous look-ahead.
- It is extremely low level. nio should probably have been call llio (low level io).
You must explicitly clear and fill the buffer and explicitly read/or write it.
It is up to you to avoid overfilling the buffer.
- It is a very mundane buffer. The only thing that makes it much different from a
raw byte[] is the way may be backed something that
only looks like a byte[] but is not really, e.g. an entire
memory mapped file or direct I/O buffer containg part of a file.
For most purposes, you might as well use the traditional IO stream methods which
use nio under the covers.
How It Works
Operations
- To get started you can clear. This sets the limit
to the capacity and the position to zero, thus freeing up all the buffer space
and making the buffer logically empty. It does not actually zero the backing
array.
- You can add data to the buffer with put. When the
buffer is full, you get a BufferOverflowException.
The call does not block until space is freed up. put
has several variants to put just one or multiple bytes, to put the next bytes,
or to specify the offset either absolutely (relative to the beginning of the
buffer) or relatively (relative to the current position). It works analogously
to the seek pointer manipulations in file I/O.
- The way to prepare to read is to use flip rather
than rewind. It sets the limit to the current
position and then sets the position to zero. Using flip
will prevent you reading with get out past where
you have written. You must call flip
exactly once. If you fail to call it or call it twice, the ByteBuffer
will appear to be empty. To make things worse, ByteBuffer
often calls flip for you automatically e.g. on FileChannel.
map( FileChannel. MapMode.
READ_ONLY,...) and on ByteBuffer.
wrap. I consider this design grossly incompetent.
The design maliciously attempts to trip up programmers.
- You then read data out of the buffer with get. When
you bang into the limit, you get a BufferUnderflowException.
Normally, you count your reads with a for loop up to the limit. The get
call does not block until more data are available. get
has several variants to get just one or multiple bytes, to get the next bytes,
or to specify the offset either absolutely (relative to the beginning of the
buffer) or relatively (relative to the current position). It works analogously
to the seek pointer manipulations in file I/O.
- After you have read data, you can go back to the beginning and start reading
again with rewind, which leaves the limit unchanged
and sets the position to zero. You must use flip
before the first read pass and rewind before
subsequent ones. If you screw this up you will either see an empty buffer or
read gibberish out past the end of the data, all without error messages or
exceptions!
Sample Code
Creating a ByteBuffer from scratch:
Creating a ByteBuffer by wrapping an existing byte[]:
Using a MappedByteBuffer to read a file:
Sequentially Reading a File
If your file is small enough to fit in your virtual address space all at once,
then you could memory map it, using a FileChannel
and MappedByteBuffer and leave the OS to figure out
how to do the I/O to read it as needed, or possibly even preemptively read it.
If you don’t want to allocate large hunks of your virtual address space,
you could allocate a smaller MappedByteBuffer at
some offset in the file other than 0, and read a decently large chunk of it.
When done, allocate a new MappedByteBuffer. You can
be considerably more generous in your chunk size than when allocating buffers.
Alternatively, you could do your I/O in a more conventional way using FileChannel.
read( ByteBuffer dst
), to read the next chunk of the file into a pre-allocated ByteBuffer.
This approach is clumbsier than traditional stream I/O, but can be more
efficient, especially when you slew over most of the data, or access it via the
backing array. It will pay off if for example you were processing just a 4-byte
field in a 512-byte record, since only the bytes you need are copied from the
buffer, not the entire record.
The effect is even more pronounced with MappedBuffers
and large records where pages of records you don’t need are not even read
in to RAM.
Handling Little Endian Files
You can use ByteBuffer.order
( ByteOrder.LITTLE_ENDIAN
) to set the endian byte-sex of the buffer to little
endian. Then when you use ByteBuffer. getInt
( int offset ), it will
collect the bytes least significant first. Note that the offset is specified in bytes,
not ints.
Learning More
Sun’s Javadoc on the
ByteBuffer class : available:
Sun’s Javadoc on the
Buffer class : available:
Sun’s Javadoc on the
FileInputStream class : available:
Sun’s Javadoc on the
FileChannel class : available:
Sun’s Javadoc on the
MappedByteBuffer class : available: