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.)
Six methods in ByteBuffer create view buffers:
public abstract ShortBuffer asShortBuffer( ) public abstract IntBuffer asIntBuffer( ) public abstract LongBuffer asLongBuffer( ) public abstract FloatBuffer asFloatBuffer( ) public abstract DoubleBuffer asDoubleBuffer( ) public abstract CharBuffer asCharBuffer( )
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.
Example 14-3. Writing doubles with a view buffer
import java.io.*; import java.nio.*; import java.nio.channels.*; public class RootsChannel { final static int SIZE_OF_DOUBLE = 8; final static int LENGTH = 1001; public static void main(String[] args) throws IOException { // Put 1001 roots into a ByteBuffer via a double view buffer ByteBuffer data = ByteBuffer.allocate(SIZE_OF_DOUBLE * LENGTH); DoubleBuffer roots = data.asDoubleBuffer( ); while (roots.hasRemaining( )) { roots.put(Math.sqrt(roots.position( ))); } // Open a channel to the file where we'll store the data FileOutputStream fout = new FileOutputStream("roots.dat"); FileChannel outChannel = fout.getChannel( ); outChannel.write(data); outChannel.close( ); } } |
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:
public abstract ByteBuffer putChar(char c) public abstract ByteBuffer putShort(short s) public abstract ByteBuffer putInt(int i) public abstract ByteBuffer putLong(long l) public abstract ByteBuffer putFloat(float f) public abstract ByteBuffer putDouble(double d)
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.
Of course, there are corresponding get methods:
public abstract char getChar( ) public abstract short getShort( ) public abstract int getInt( ) public abstract long getLong( ) public abstract float getFloat( ) public abstract double getDouble( )
These all get from the current position. Each of these methods has an absolute variant that allows you to specify the position at which to put or get a value:
public abstract ByteBuffer putChar(int index , char c) public abstract ByteBuffer putShort(int index , short s) public abstract ByteBuffer putInt(int index , int i) public abstract ByteBuffer putLong(int index , long l) public abstract ByteBuffer putFloat(int index , float f) public abstract ByteBuffer putDouble(int index , double d) public abstract char getChar(int index) public abstract short getShort(int index) public abstract int getInt(int index) public abstract long getLong(int index) public abstract float getFloat(int index) public abstract double getDouble(int index)
The earlier PNG example read the size of a data chunk by getting four bytes and then combining them into an int using the bitwise operators:
int i1 = buffer.get( ); int i2 = buffer.get( ); int i3 = buffer.get( ); int i4 = buffer.get( ); int size = i1 << 24 | i2 << 16 | i3 << 8 | i4
This can now be compressed into a single call to getInt( ):
int size = buffer.getInt( );
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:
ByteBuffer buffer = buffer.allocate(8008); for (int i = 0; i <= 1000; i++) { roots.putDouble(i, Math.sqrt(i)); }
It actually stores only 1,007 bytes in the buffer. Each double overwrites the seven low-order bytes of the previous double. The proper way to write this code is like this:
final int DOUBLE_SIZE = 8; ByteBuffer buffer = buffer.allocate(1001 * DOUBLE_SIZE); for (int i = 0; i <= 1000; i++) { roots.putDouble(i* DOUBLE_SIZE, Math.sqrt(i)); }
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:
String s = "Laissez les bon temps roulez!"; for (int i = 0; i < s.length( ); i++) { buffer.putChar(s.charAt(i)); }
Alternately, you could create a CharBuffer view of the ByteBuffer, and then use the write(String) methods in CharBuffer:
CharBuffer cb = buffer.asCharBuffer( ); cb.put("Laissez les bon temps roulez!");
In both cases it might be helpful to precede the string with its length, since buffers have no notions of boundaries between subsequent puts. For example:
String s = "Laissez les bon temps roulez!"; buffer.putInt(s.length( )); CharBuffer cb = buffer.asCharBuffer( ); cb.put(s);
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.
|
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:
public static ByteOrder nativeOrder( )