Typed Data

I/O is really about bytesnot ints, not text, not doublesbytes. The bytes that are read and written can be interpreted in various ways, but as far as the filesystem, the network socket, or almost anything else knows, they're just bytes. The detailed interpretation is left up to the program that reads and writes those bytes. Thus, it shouldn't come as any surprise in the next chapter when you discover that different kinds of channelsTCP channels, UDP channels, file channels, and the likedeal almost exclusively with byte buffers and almost never with int buffers, char buffers, or anything else.

However, sometimes it's convenient to be able to pretend that I/O is about something else. If a program were dealing in ints, it would be nice to be able to read and write ints, not bytes. In traditional I/O, DataInputStream and DataOutputStream fill this gap. In new I/O, view buffers meet this need.

14.10.1. View Buffers

The ByteBuffer class, and only the ByteBuffer class, can present a view of itself as a buffer of a different type: an IntBuffer, CharBuffer, ShortBuffer, LongBuffer, FloatBuffer, or DoubleBuffer. A view buffer is backed by a ByteBuffer. When you write an int such as 1,789,554 into the view buffer, the buffer writes the four bytes corresponding to that int into the underlying buffer. The encoding used is the same as that used by DataOutputStream, except for a possible byte order adjustment. The view buffer has a position, mark, limit, and capacity defined in terms of its type. The underlying ByteBuffer has a position, mark, limit, and capacity defined in terms of bytes. If the view buffer is an IntBuffer, the underlying ByteBuffer's position, mark, limit, and capacity will be four times the position, mark, limit, and capacity of the view buffer, because there are four bytes in an int. If the view buffer is a DoubleBuffer, the underlying ByteBuffer's position, mark, limit, and capacity will be eight times the position, mark, limit, and capacity of the view buffer, because there are eight bytes in a double. (If the buffer's size isn't an exact multiple of the view type's size, excess bytes at the end are ignored.)

In Example 8-3, you saw how a DataOutputStream could write square roots in a file as doubles. Example 14-3 repeats this example using the new I/O API instead of streams. First, a ByteBuffer big enough to hold 1001 doubles is allocated. Next, a DoubleBuffer is created as a view of the ByteBuffer. The double roots are put into this view buffer. Finally, the underlying ByteBuffer is written into the file.

Interestingly, the ByteBuffer in this example does not need to be flipped. Because the original buffer and the view buffer have separate positions and limits, writing data into the view buffer doesn't change the original's position; it only changes its data. When we're ready to write data from the original buffer onto the channel, the original buffer's position and limit still have their default values of 0 and the capacity, respectively.

14.10.2. Put Type Methods

View buffers work as long as you want to write only one type of data (for example, all doubles, as in Example 14-4). However, very often files need to contain multiple types of data: doubles, ints, chars, and more. For instance, a PNG file contains unsigned integers, ASCII strings, and raw bytes. For this purpose, ByteBuffer has a series of put methods that take the other primitive types:

The formats used to write these types are the same as for DataOutput (modulo byte order).

Each of these advances the position by the size of the corresponding type. For instance, putChar and putShort increment the position by 2, putInt and putFloat increment the position by 4, and putLong and putDouble increment the position by 8.

Be careful, though. These methods are not quite the same as the equivalent methods in CharBuffer, ShortBuffer, and so forth. The difference is that the index is into the byte range, not the double range. For example, consider this code fragment:

Despite these methods, a ByteBuffer still just stores bytes. It doesn't know which elements hold a piece of an int, which hold pieces of doubles, and which hold plain bytes. Your code is responsible for keeping track of the boundaries. If a buffer doesn't contain fixed types in fixed positions, you'll need to design some sort of meta-protocol using length and type codes to figure out where the relevant boundaries are.

Unlike DataOutputStream, there aren't any methods to write strings into a ByteBuffer. However, it's straightforward to write each char in the string. For example, this code writes the string "Laissez les bon temps roulez!" into a buffer:

Remember, the CharBuffer view starts at the position of the underlying buffer when the view was created. Here, this is immediately after the int containing the string's length.

14.10.3. Byte Order

The DataInputStream and DataOutputStream classes in java.io only handle big-endian data. The buffers in new I/O are a little more flexible. By default, they're configured for big-endian data. However, they can be changed to little-endian if that's what you need. Usually, you need to specify byte order. For example, if you're reading or writing astronomy data in the FITS format, you have to use big-endian. It doesn't matter what platform you're on; FITS files are always big-endian.

Well, not quite always. The FITS spec says numbers are supposed to be big-endian, but you can in fact find FITS files and software that use little-endian representations. Either way, given a file in big- or little-endian format, you have to read it in the order it was written for the data to make sense, regardless of the native byte order of the host platform.

The order( ) method lets you specify the required byte order:

public final ByteBuffer order(ByteOrder order)

The current byte order is returned by the no-args version of the method:

public final ByteOrder order( )

ByteOrder has exactly two possible values:

ByteOrder.BIG_ENDIAN
ByteOrder.LITTLE_ENDIAN

Sometimes what you want is the native byte order of the host platform. The static ByteOrder.nativeOrder( ) method tells you this: