ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.


AddThis Social Bookmark Button

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.

Buffer Family Tree
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).

Short View of ByteBuffer
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).

Big Endian
Figure 5: Big-endian.

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

Pages: 1, 2, 3, 4

Next Pagearrow