Top Ten New Things You Can Do with NIO
Pages: 1, 2, 3, 4
8: Buffer Views
NIO introduces buffers, a gaggle of related classes in the
java.nio package (see Figure 3). Upon first impression,
buffers seem like something you were assigned in Computer Science 101
class. They're very simple objects that encapsulate a fixed-size
array of primitive values, along with some state information about the
array. That's pretty much it.

Figure 3: Buffer class family tree.
Buffers were created primarily to act as containers for data being
sent to or received from channels. Channels are conduits to low-level
I/O services and are always byte-oriented; they only know how to use
ByteBuffer objects.
So what do we use the other buffers types for? Instances of the non-byte
buffers can be created from scratch, or wrapped around an array of the
appropriate type. They can be useful that way, but such a buffer
cannot be used for I/O. However, there's a third way to create
non-byte buffers, as a view of an existing
ByteBuffer.
For example, let's say you have a file containing Unicode
characters stored as 16-bit values (this is the UTF-16 encoding, not
the UTF-8 encoding used for common text files). If you were to read a
chunk of this file into the byte buffer, you could then create a
CharBuffer view of those bytes, like this:
CharBuffer charBuffer = byteBuffer.asCharBuffer();
This creates a view of the original ByteBuffer, which
behaves like a CharBuffer, combining each pair of bytes
in the buffer into a 16-bit char value, as represented by
Figure 4 (this figure shows that odd remaining bytes are not included
in the view; let's assume here that you started with an even-size byte
buffer).

Figure 4: CharBuffer view of a ByteBuffer.
You can then use the CharBuffer object to iterate over
the data (using relative get() calls), access it randomly
with absolute get()s, or copy the data to a char[]
array and pass it along to another object that knows nothing about
buffers.
The ByteBuffer class also has methods to do ad hoc
accesses of individual primitive values. For example, to access four
bytes of a buffer as an int, you could do the following:
int fileSize = byteBuffer.getInt();
This extracts four bytes from the buffer, beginning at the current
position (there are also absolute versions of these methods) and
combines them to form a 32-bit int value. A very cool
thing about this is that the four bytes need not be aligned on any
particular address boundaries. The ByteBuffer
implementation will do whatever it needs to do to assemble the bytes
(or disassemble, for put()) if the underlying hardware
does not permit misaligned memory accesses.
7: Byte Swabbing
If you've ever had to deal with cross-platform issues,
you're probably wondering at this point about byte order in the
previous example. The CharBuffer view groups pairs of
bytes together to form 16-bit values, but which byte is high and which
one is low? The order in which bytes are combined to form larger
numeric values is known as endian-ness. When the numerically-most-significant byte is stored first in memory (at the lower
address), this is big-endian byte order (see Figure 5). The
opposite, where the least significant byte occurs first, is little-endian
(see Figure 6).

Figure 5: Big-endian.

Figure 6: Little-endian.
In the example in the previous section, are the 16-bit Unicode chars stored as little-endian (UTF-16LE) or big-endian (UTF-16BE)? They could be stored in the file either way, so we need a means of controlling how the view buffer maps the bytes to chars.
Every buffer object has a byte order setting. For all but
ByteBuffer, this is a read-only property and cannot be
changed. The byte order setting of ByteBuffer objects can
be changed at any time. Doing so affects the resulting byte order of
any views created of that ByteBuffer object. So, if we
knew that the Unicode data in our file was encoded as UTF-16LE
(little-endian), we'd set the ByteBuffer's byte
order prior to creating the view CharBuffer, thusly:
byteBuffer.order (ByteOrder.LITTLE_ENDIAN);
CharBuffer charBuffer = byteBuffer.asCharBuffer();
The new view buffer inherits the byte order setting of the
ByteBuffer. Subsequent changes to the ByteBuffer's
byte order will not affect that of the view. The initial byte order
setting of a ByteBuffer object is always big-endian,
regardless of the native byte ordering of the hardware it's
running on.
What if we didn't know the byte order of the Unicode data in
the file? If the file was encoded with the portable UTF-16 encoding,
the first two bytes of the file would contain a byte order marker
value (if it's directly encoded as UTF-16LE or UTF-16BE, then you
need prior knowledge of the byte order). If you were to test that byte
order marker, you could set the byte order appropriately before
creating the CharBuffer view.
A ByteBuffer object's current byte order setting
also affects byte swabbing for data element views (getInt(),
getLong(), getFloat(), etc.). The
buffer's byte order setting at the time of the call affects how
bytes are combined to form the return value or broken out for storage
in the buffer.
6: Direct Buffers
The data elements encapsulated by a buffer can be stored in one of
several different ways: in a private array created by the buffer
object (allocation), in an array you provide (wrapping), or, in the
case of direct buffers, in native memory space outside of the JVM's
memory heap. When you create a direct buffer (by invoking
ByteBuffer.allocateDirect()), native system memory is
allocated and a buffer object is wrapped around it.
The primary purpose of direct buffers is for doing I/O on channels. Channel implementations can set up OS-level I/O operations to act directly upon a direct buffer's native memory space. That alone is a powerful new capability and a key to NIO's efficiency. But those I/O operations occur under the hood; they're not something you can use directly. But there is an aspect of direct buffers that you can exploit to great advantage.
The ability to use native memory to hold buffer data is enabled by some new JNI methods, which make it possible, for the first time, for a Java object to access memory space allocated in native code. Prior to 1.4, native code could access data in the JVM heap (if it was very careful -- there were severe restrictions), but Java code could not reach memory allocated by native code.
Now, not only can JNI code discover the address of the native
memory space inside of a buffer created with ByteBuffer.allocateDirect()
on the Java side, but it can allocate its own memory (with malloc(),
for example) and then call back to the JVM to wrap that memory space
in a new ByteBuffer object (the JNI method to do this is
NewDirectByteBuffer()).
The really exciting part is that a ByteBuffer object
can be wrapped around any memory address the native code can
obtain, even memory outside the JVM's own address space. One
example is creating a direct ByteBuffer object that
encapsulates the memory on a video card. Such an buffer enables pure
Java code to read and write directly to video memory with no system
calls or buffer copies. Pure Java video drivers! All you need is a
tiny bit of JNI glue to grab the video memory and return a
ByteBuffer object. You couldn't do that before NIO
came along.