Buffer internals
Buffer internals overview
In this section, we'll look at two important components of buffers in NIO: state variables and accessor methods.
State variables are key to the "internal accounting system" mentioned in the previous section. With each read/write operation, the buffer's state changes. By recording and tracking those changes, a buffer is able to internally manage its own resources.
When you read data from a channel, the data is placed in a buffer. In some cases, you can write this buffer directly to another channel, but often, you'll want to look at the data itself. This is accomplished using theaccessor method get(). Likewise, when you want to put raw data in a buffer, you use the accessor methodput().
In this section, you'll learn about state variables and accessor methods in NIO. Each component will be described, and then you'll have the opportunity to see it in action. While NIO's internal accounting system might seem complicated at first, you'll quickly see that most of the real work is done for you. The bookkeeping you're probably accustomed to coding by hand -- using byte arrays and index variables -- is handled internally in NIO.
State variables
Three values can be used to specify the state of a buffer at any given moment in time:
position
limit
capacity
Together, these three variables track the state of the buffer and the data it contains. We'll examine each one in detail, and also see how they fit into a typical read/write (input/output) process. For the sake of the example, we'll assume that we are copying data from an input channel to an output channel.
Position
You will recall that a buffer is really just a glorified array. When you read from a channel, you put the data that you read into an underlying array. Thepositionvariable keeps track of how much data you have written. More precisely, it specifies into which array element the next byte will go. Thus, if you've read three bytes from a channel into a buffer, that buffer'spositionwill be set to 3, referring to the fourth element of the array.
Likewise, when you are writing to a channel, you get the data from a buffer. Thepositionvalue keeps track of how much you have gotten from the buffer. More precisely, it specifies from which array element the next byte will come. Thus, if you've written 5 bytes to a channel from a buffer, that buffer'spositionwill be set to 5, referring to the sixth element of the array.
Limit
Thelimitvariable specifies how much data there is left to get (in the case of writing from a buffer into a channel), or how much room there is left to put data into (in the case of reading from a channel into a buffer).
Thepositionis always less than, or equal to, thelimit.
Capacity
Thecapacityof a buffer specifies the maximum amount of data that can be stored therein. In effect, it specifies the size of the underlying array -- or, at least, the amount of the underlying array that we are permitted to use.
Thelimitcan never be larger than thecapacity.
Observing the variables
We'll start with a newly created buffer. For the sake of the example, let's assume that our buffer has a totalcapacityof eight bytes. TheBuffer's state is shown here:
Recall that thelimitcan never be larger than thecapacity, and in this case both values are set to 8. We show this by pointing them off the end of the array (which is where slot 8 would be if there were a slot 8):
Thepositionis set to 0. If we read some data into the buffer, the next byte read will go into slot 0. If we write from the buffer, the next byte taken from the buffer will be taken from slot 0. Thepositionsetting is shown here:
Because thecapacityis not going to change, we can omit it from the discussion that follows.
The first read
Now we are ready to begin read/write operations on our newly created buffer. We start by reading some data from our input channel into the buffer. The first read gets three bytes. These are put into the array starting at theposition, which was set to 0. After this read, the position is increased to 3, as shown here:
Thelimitis unchanged.
The second read
For our second read, we read two more bytes from the input channel into our buffer. The two bytes are stored at the location pointed to byposition;positionis thus increased by two:
Thelimitis unchanged.
The flip
Now we are ready to write our data to an output channel. Before we can do this, we must call theflip()method. This method does two crucial things:
It sets thelimitto the currentposition.
It sets thepositionto 0.
The figure on the section shows our buffer before the flip. Here is the buffer after the flip:
We are now ready to begin writing data to a channel from the buffer. Thepositionhas been set to 0, which means the next byte we get will be the first one. And thelimithas been set to the oldposition, which means that it just includes all the bytes we read before, and no more.
The first write
In our first write, we take four bytes from the buffer and write them to our output channel. This advances thepositionto 4, and leaves thelimitunchanged, as shown here:
The second write
We only have one byte left to write. Thelimitwas set to 5 when we did ourflip(), and thepositioncannot go past thelimit. So the last write takes one byte from our buffer and writes it to the output channel. This advances thepositionto 5, and leaves thelimitunchanged, as shown here:
The clear
Our final step is to call the buffer'sclear()method. This method resets the buffer in preparation for receiving more bytes.Cleardoes two crucial things:
It sets thelimitto match thecapacity.
It sets thepositionto 0.
This figure shows the state of the buffer afterclear()has been called:
The buffer is now ready to receive fresh data.
Accessor methods
So far, we've only used buffers to move data from one channel to another. Frequently, however, your program will need to deal directly with the data. For example, you might want to save user data to disk. In this case, you'll have to put that data directly into a buffer, and then write the buffer to disk using a channel.
Or, you might want to read user data back in from disk. In this case, you would read the data into a buffer from a channel, and then examine the data in the buffer.
We'll close this section with a detailed look at accessing data directly in the buffer, using theget()andput()methods for theByteBufferclass.
The get() methods
In theByteBufferclass, there are fourget()methods:
byte get();
ByteBuffer get( byte dst[] );
ByteBuffer get( byte dst[], int offset, int length );
byte get( int index );
The first method gets a single byte. The second and third methods read a group of bytes into an array. The fourth method gets the byte from a particular position in the buffer. The methods that return aByteBuffersimply return thethisvalue on which they are called.
Additionally, we say that the first threeget()methods are relative, while the last one is absolute.Relativemeans that thelimitandpositionvalues are respected by theget()operation -- specifically, the byte is read from the currentposition, and thepositionis incremented after theget. Anabsolutemethod, on the other hand, ignores thelimitandpositionvalues, and does not affect them. In effect, it bypasses the buffer's accounting methods entirely.
The methods shown above correspond to theByteBufferclass. The other classes have equivalentget()methods that are identical except that rather than dealing with bytes, they deal with the type appropriate for that buffer class.
The put() methods
In theByteBufferclass, there are fiveput()methods:
ByteBuffer put( byte b );
ByteBuffer put( byte src[] );
ByteBuffer put( byte src[], int offset, int length );
ByteBuffer put( ByteBuffer src );
ByteBuffer put( int index, byte b );
The first methodputs a single byte. The second and third methods write a group of bytes from an array. The fourth method copies data from the given sourceByteBufferinto thisByteBuffer. The fifth method puts the byte into the buffer at a particularposition. The methods that return aByteBuffersimply return thethisvalue on which they are called.
As with theget()methods, we characterize theput()methods as beingrelativeorabsolute. The first four methods are relative, while the fifth one is absolute.
The methods shown above correspond to theByteBufferclass. The other classes have equivalentput()methods that are identical except that rather than dealing with bytes, they deal with the type appropriate for that buffer class.
Typed get() and put() methods
In addition to theget()andput()methods described previously,ByteBufferalso has extra methods for reading and writing values of different *, as follows:
getByte()
getChar()
getShort()
getInt()
getLong()
getFloat()
getDouble()
putByte()
putChar()
putShort()
putInt()
putLong()
putFloat()
putDouble()
Each of these methods, in fact, comes in two varieties -- one relative and one absolute. They are useful for reading formatted binary data, such as the header of an image file.
You can see these methods in action in the example program *InByteBuffer.java.
The buffer at work: An inner loop
The following inner loop summarizes the process of using a buffer to copy data from an input channel to an output channel.
while (true) {
buffer.clear();
int r = fcin.read( buffer );
if (r==-1) {
break;
}
buffer.flip();
fcout.write( buffer );
}
Theread()andwrite()calls are greatly simplified because the buffer takes care of many of the details. Theclear()andflip()methods are used to switch the buffer between reading and writing.
More about buffers
Buffers overview
Thus far, you have learned most of what you need to know about buffers to use them on a day-to-day basis. Our examples haven't strayed much beyond the kind of standard read/write procedures you could just as easily implement in original I/O as in NIO.
In this section, we'll get into some of the more complex aspects of working with buffers, such as buffer allocation, wrapping, and slicing. We'll also talk about some of the new features NIO brings to the Java platform. You'll learn how to create different * of buffers to meet different goals, such asread-onlybuffers, which protect data from modification, anddirectbuffers, which map directly onto the underlying OS buffers. We'll close the section with an introduction to creating memory-mapped files in NIO.
Buffer allocation and wrapping
Before you can read or write, you must have a buffer. To create a buffer, you mustallocateit. We allocate a buffer using the static method ofallocate():
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
Theallocate()method allocates an underlying array of the specified size and wraps it in a buffer object -- in this case aByteBuffer.
You can also turn an existing array into a buffer, as shown here:
byte array[] = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap( array );
In this case, you've used thewrap()method to wrap a buffer around an array. You must be very careful about performing this type of operation. Once you've done it, the underlying data can be accessed through the buffer as well as directly.