The ByteBuffer Class in Java
Java Programming Notes # 1782
Preface
The recently released JavaTM 2 SDK, Standard Edition Version 1.4.0 contains a number of new features. This article explains how to use some of those new features.
Among the new features is a new I/O API. Here is how Sun describes that API and the new features that it provides:
"The new I/O (NIO) APIs introduced in v 1.4 provide new features and improved performance in the areas of buffer management, scalable network and file I/O, character-set support, and regular-expression matching. The NIO APIs supplement the I/O facilities in the java.io package."Basic classes
The abstract Buffer class, and its subclasses, are basic to many of the new features in the NIO. The Sun documentation lists the following known subclasses of Buffer:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
The purpose of this lesson is to help you understand how to use the features of the ByteBuffer class. I will describe many of those features, and will illustrate the use of those features by explaining the code in a sample program.
Caution: Dangerous Curves Ahead
A caution is in order regarding the capabilities discussed in this lesson. Once you enter the domain of the ByteBuffer class, you have left the type-safe world normally associated with Java behind. You have entered a domain more akin to that normally enjoyed by adventuresome C and C++ programmers.
For example, there is nothing to prevent you from creating a buffer for byte data, populating it with double data, and then erroneously viewing and interpreting it as type int. Also, there is nothing to prevent you from interpreting LITTLE_ENDIAN data as BIG_ENDIAN, and vice versa. There are many other ways that you can go astray as well, and neither the compiler nor the virtual machine are of much help in preventing such programming errors.
I won't spend a lot of time discussing these matters, but I have provided a short sample program that illustrates the above sequence of events in Listing 38 near the end of the lesson.
Viewing tip
You may find it useful to open another copy of this lesson in a separate browser window. That will make it easier for you to scroll back and forth among the different listings and figures while you are reading about them.
Supplementary material
I recommend that you also study the other lessons in my extensive collection of online Java tutorials. You will find those lessons published at Gamelan.com. However, as of the date of this writing, Gamelan doesn't maintain a consolidated index of my Java tutorial lessons, and sometimes they are difficult to locate there. You will find a consolidated index at www.DickBaldwin.com.
Discussion and Sample Code
As mentioned above, the class named ByteBuffer extends the abstract class named Buffer. The Sun documentation lists the following known subclasses of Buffer:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
As you can see from the names of the subclasses, there is one subclass of the Buffer class for each non-boolean primitive type. If you compare the documentation for the seven subclasses of Buffer, you will find that they are very similar. In fact, each of the subclasses provides the following four capabilities for data of the type whose name appears in the subclass name.
- Absolute and relative get and put methods that read and write single elements.
- Relative get methods that transfer contiguous sequences of elements from the buffer into an array.
- Relative put methods that transfer contiguous sequences of elements from an array (of the same type) or some other buffer (of the same type) into a buffer.
- Methods for compacting, duplicating, and slicing a buffer.
Additional capabilities of ByteBuffer class
The ByteBuffer class also provides the following two additional capabilities. This makes the ByteBuffer class more general than the other five subclasses of Buffer.
- Absolute and relative get and put methods that read and write values of primitive types other than byte.
- Methods for creating view buffers, which allow a byte buffer to be viewed as a buffer containing values of some other primitive type.
Will concentrate on the ByteBuffer class
Therefore, I have singled out the ByteBuffer class for a detailed discussion in this lesson. Once you understand how to use objects of the ByteBuffer class, you should also understand how to use objects instantiated from the other subclasses of Buffer.
A container for primitive data
None of the container classes in the Java Collections Framework are designed to contain primitive data. Those containers are all designed to contain references to objects. Objects instantiated from subclasses of Buffer are containers for primitive data.
Three important properties
Every object instantiated from Buffer has the following three important properties:
- capacity: The number of elements the buffer contains.
- limit: The index of the first element that should not be read or written.
- position: The next element to be read or written.
Writing and reading data
Subclasses of Buffer use put and get operations to store data into a buffer and to read data from the buffer (to transfer data into and out of the buffer). Each subclass defines two categories of put and get operations: relative and absolute.
Relative put and get operations
Relative data transfer operations store or read one or more elements starting at the current position. The position is automatically incremented based on the number of items transferred and the type of data transferred.
Absolute put and get operations
Absolute data transfer operations take an element index as a parameter and use that index to store or retrieve data. These operations do not affect the value of the position property.
Method chaining
Some of the methods of the ByteBuffer class return a reference
to the buffer. This makes it possible to use method invocation
chaining syntax such as that shown in Figure 1.
buf5.putDouble(1.0/3.0). putFloat((float)(1.0/6.0)). putLong(Long.MAX_VALUE); Figure 1 |
Read-only buffers
It is possible to create read-only buffers, and I will do so in the sample program in this lesson. Methods that normally change the contents of a buffer will throw a ReadOnlyBufferException when invoked on a read-only buffer.
While a read-only buffer does not allow its content to be changed, its mark, position, and limit values may be changed. You can determine if a buffer is read-only by invoking its isReadOnly method.
The sample program named ByteBuffer01
The features of the ByteBuffer class are illustrated in the program named ByteBuffer01, which I will discuss in fragments. A complete listing of the program is provided in Listing 37 near the end of the lesson.
Displaying buffer properties
Listing 1 shows a convenience method that I wrote, whose purpose is
to display the properties of a buffer. The reference to the buffer
of interest and a String to identify the buffer are passed as parameters
to the method.
static void showBufferProperties(
Buffer buf,String name){
System.out.println(
"Buffer Properties for " + name
+"\n capacity="
+ buf.capacity()
+ " limit="
+ buf.limit()
+ " position="
+ buf.position());
}//end showBufferProperties
Listing 1
|
The method uses the three getter methods of the Buffer class to get and display the values of the following properties:
- capacity
- limit
- position
The format of the output produced by Listing 1 is illustrated in Figure
2.
Buffer Properties for buf5 capacity=25 limit=25 position=0 Figure 2 |
Display byte data in the buffer
Listing 2 shows a convenience method designed to display the byte data
stored in the buffer, from the current value of the position property
to the value of the limit property. The byte data is displayed
12 bytes per row of output.
static void showBufferData(
ByteBuffer buf, String name){
System.out.println(
"Buffer data for " + name);
int cnt = 0;
while(buf.hasRemaining()){
System.out.print(
buf.get() + " ");
cnt++;
if(cnt%12 == 0)
System.out.println();//line
}//end while loop
System.out.println();//blank line
}//end showBufferData
Listing 2
|
The code in Listing 2 invokes two important methods that are new to version 1.4.0 of the SDK:
- The hasRemaining method of the Buffer class
- The relative get method of the ByteBuffer class
The hasRemaining method is much like one of the methods of the Iterator and Enumeration interfaces, which are used to iterate on objects instantiated from the concrete classes of the Java Collections Framework.
The hasRemaining method tells whether there are any elements remaining between the current position and the limit. The method returns a boolean, which is true only if there is at least one element remaining in the buffer. Thus, this method works very nicely in the conditional clause of a while loop for the purpose of iterating on a buffer.
The relative get method
The relative get method of the ByteBuffer class reads and returns the byte at the buffer's current position, and then increments the position. Thus, it also works quite well in an iterator loop for a buffer (provided you have exercised proper control over the values of the position and limit properties beforehand).
The format of the output produced by the code in Listing 2 is illustrated
in Figure 3. Each numeric value in figure three is the value of a
single byte, and the values for twelve bytes are displayed on each row
of output.
Buffer data for buf5 0 0 0 1 0 0 0 2 0 0 0 4 0 0 0 8 0 0 0 16 0 0 0 32 0 Figure 3 |
Display array data
Listing 3 is a convenience method designed simply to display the data
in an array object of type byte.
static void showArrayData(
byte[] array){
System.out.println(
"Show array data");
for(int cnt = 0;
cnt < array.length; cnt++){
System.out.print(
array[cnt] + " ");
if((cnt+1)%12 == 0)
System.out.println();//line
}//end for loop
System.out.println();//blank line
}//end showArrayData
Listing 3
|
I am assuming that you are already familiar with the use of array objects in Java, and therefore, I won't discuss this code in detail. If that is not the case, you can learn about array objects at www.DickBaldwin.com.
Create an array object
There are several ways to create a buffer object in Java. One of those ways is to wrap an existing array object in a buffer object. To do that, we need an array object, which I will create using the code in Listing 4. (I will discuss two other ways to create a buffer object later in this lesson.)
Listing 4 shows the beginning of the main method. The code
in Listing 4 creates, populates, and displays an eight-element array object
containing data of type byte.
public static void main(
String[] args){
//Wrap a byte array into a buffer
System.out.println(
"Create and populate array");
byte[] a1 = {0,1,2,3,4,5,6,7};
showArrayData(a1);
Listing 4
|
Again, I am assuming that you are already familiar with the use of array
objects in Java, and therefore, I won't discuss this code in detail.
The code in Listing 4 produces the output shown in Figure 4.
Create and populate array Show array data 0 1 2 3 4 5 6 7 Figure 4 |
I show this here because we will want to compare it with the data stored in our buffer object later.
Create a ByteBuffer object
As mentioned above, there are several ways to create a buffer, and one
of them is shown in Listing 5.
System.out.println( "Wrap byte array in buffer"); ByteBuffer buf1 = ByteBuffer.wrap(a1); System.out.println( "Buffer is direct: " + buf1.isDirect()); showBufferData(buf1, "buf1"); Listing 5 |
Listing 5 invokes the static wrap method of the ByteBuffer class to create a buffer that wraps the existing array object referred to by the reference variable named a1.
Wrapping an array object
There are two overloaded versions of the wrap method, one that requires incoming offset and length parameters, and one that doesn't. (Both versions require an incoming reference to an array object.) I used the simpler of the two versions, which does not require offset and length.
A backing array
For both versions, the new buffer is backed up by, or connected to, the byte array, which it wraps. Modifications to the buffer cause the array contents to be modified, and modifications to the array cause the buffer contents to be modified. (It appears as though they are really the same set of data. Note that this is a common theme that will arise more than once in this lesson.)
Buffer property values
For the version of the wrap method that I used, the capacity and limit properties of the new buffer are the same as array.length. The initial value of the position property of the new buffer is zero, and its mark is undefined.
For the more complex version, the initial values of the buffer properties are determined by the values of the offset and length parameters passed to the wrap method.
Direct vs. non-direct buffers
The code in Listing 5 also introduces you to the fact that a byte buffer
is either direct or non-direct. I will have more to say about
this later. For now, suffice it to say that a buffer created by wrapping
an array object is non-direct, as indicated by the program output
shown in Figure 5.
Wrap byte array in buffer Buffer is direct: false Buffer data for buf1 0 1 2 3 4 5 6 7 Figure 5 |
Figure 5 also shows the byte contents of the buffer. If you compare this with the array contents shown in Figure 4, you will see that the buffer and its backing array contain the same values.
Modifications are reflected ...
Modifications to the buffer cause the array contents to be modified,
and modifications to the array cause the buffer contents to be modified.
This is partially illustrated in Listing 6.
System.out.println( "Modify first array element"); a1[0] = 10; showArrayData(a1); buf1.position(0); showBufferData(buf1, "buf1"); Listing 6 |
The code in Listing 6 changes the value in the first array element from 0 to 10, and then displays the modified contents of the array object and the buffer. You will see that this causes the value of the first element in the buffer to change accordingly.
Set the position to zero
Recall that the showBufferData method described earlier (and invoked in Listing 6) displays the contents of the buffer from the element specified by the value of the position property, to the element specified by the value of the limit property.
At this point, (and at numerous other points in this program), it is necessary to set the value of the position property to zero before invoking showBufferData. Otherwise, the contents of the entire buffer would not be displayed. This is accomplished by invoking the position method inherited from the Buffer class and passing zero as a parameter.
Figure 6 shows the output produced by the code in Listing 6.
Modify first array element Show array data 10 1 2 3 4 5 6 7 Buffer data for buf1 10 1 2 3 4 5 6 7 Figure 6 |
The important thing to note in Figure 6 is that the value in the first element of the buffer was changed when the value in the first element of the backing array was changed.
Absolute and relative put methods
As I explained earlier, the ByteBuffer class provides both absolute
and relative versions of the put and get methods.
Listing 7 illustrates both categories of put methods. In addition,
Listing 7 also illustrates the fact that modifications to the buffer cause
the array contents to be modified accordingly.
System.out.println( "Modify the buffer"); buf1.put(3,(byte)20); buf1.position(4); buf1.put((byte)21); buf1.put((byte)22); buf1.position(0); showBufferData(buf1, "buf1"); showArrayData(a1); Listing 7 |
Invoke absolute put method
The code in Listing 7 begins by using the absolute version of the put method to write the byte value 20 into the buffer at position 3. This overwrites the value previously stored at that position. (Note that the invocation of the absolute version of the put method has no effect on the value of the position property.)
Invoke relative put method
Then the code in Listing 7 sets the value of the position property to 4, and invokes the relative version of the put method twice in succession. This causes the values 21 and 22 to overwrite the values previously stored in positions 4 and 5.
Display the data
Then Listing 7 sets the position to 0 and displays the contents
of the buffer, followed by the contents of the array. The output
produced by Listing 7 is shown in Figure 7.
Modify the buffer Buffer data for buf1 10 1 2 20 21 22 6 7 Show array data 10 1 2 20 21 22 6 7 Figure 7 |
Perhaps the most important things to observe in this output are:
- The values of the elements at positions 3, 4, and 5 in the buffer are changed to 20, 21, and 22 respectively.
- The values of the corresponding elements in the array are also changed accordingly.
The relative get method is illustrated in the showBufferData
method discussed earlier. The absolute get method is
illustrated in Listing 8.
System.out.println("Get absolute");
System.out.println("Element 3 = "
+ buf1.get(3));
System.out.println("Element 5 = "
+ buf1.get(5));
System.out.println("Element 7 = "
+ buf1.get(7));
Listing 8
|
The code in Listing 8 gets and displays the values stored in positions
3, 5, and 7. The output produced is shown in Figure 8.
Get absolute Element 3 = 20 Element 5 = 22 Element 7 = 7 Figure 8 |
You can verify these results by comparing them back against the buffer contents shown in Figure 7.
Contiguous get and put operations
In addition to reading and writing single elements from the buffer, the subclasses of the Buffer class (including ByteBuffer) allow for transferring contiguous blocks of elements into and out of the buffer.
The ByteBuffer class provides for the transfer of a block of bytes from the buffer into an array object of type byte (contiguous get). The class also provides for the transfer of a block of bytes from an array object (of type byte), or from another ByteBuffer object, into contiguous elements of a ByteBuffer object (contiguous put).
A contiguous get to an array
Listing 9 illustrates the use of the contiguous get method to
transfer a block of contiguous bytes from the buffer to a new empty array
object of type byte.
System.out.println( "Contiguous get"); buf1.position(0); showBufferData(buf1, "buf1"); byte[] a2 = new byte[10]; showArrayData(a2); buf1.position(1); buf1.get(a2, 3, 5); showArrayData(a2); Listing 9 |
Create a new byte array
The code in Listing 9 begins by displaying the contents of the buffer for later comparison with the array contents. Then it creates a new array object of type byte whose length is 10. This is larger than the capacity of the buffer. (Recall that the initial value of each of the elements in a new byte array is zero unless purposely initialized using the syntax shown in Listing 4.)
Invoke the contiguous get method
Then the code in Listing 9 sets the position of the buffer to 1, and invokes the get method, passing the following parameters:
- The array referred to by a2 as the destination array into which bytes are to be written
- A value of 3 for the offset within the array of the first byte to be written
- A value of 5 for the maximum number of bytes to be written to the given array
Throwing exceptions
As you have probably figured out already, various conditions involving the specified number of bytes, the remaining number of bytes in the buffer, and the length of the array can conflict, causing exceptions to be thrown. I won't attempt to explain those conditions here, but will simply refer you to the Sun documentation for those details.
Display the data
Finally, the code in Listing 9 displays the contents of the array into which the bytes were transferred.
The output produced by this section of the program is shown in Figure
9.
Contiguous get Buffer data for buf1 10 1 2 20 21 22 6 7 Show array data 0 0 0 0 0 0 0 0 0 0 Show array data 0 0 0 1 2 20 21 22 0 0 Figure 9 |
As you can see in Figure 9, after the transfer takes place, there are five non-zero values in the array, beginning at index 3. This matches the array offset value of 3 and the specified length of 5 passed as parameters to the contiguous get method.
Also, as you can see in Figure 9, the five values transferred from the buffer to the array began at position 1 in the buffer, corresponding to the fact that the position was set to 1 immediately prior to the invocation of the get method.
Contiguous put from array as source of data
Listing 10 illustrates the transfer of a block of bytes from an array
into contiguous elements in a buffer.
System.out.println( "Contiguous put from array"); showArrayData(a2); buf1.position(0); buf1.put(a2, 1, 8); buf1.position(0); showBufferData(buf1, "buf1"); Listing 10 |
The code in Listing 10 begins by displaying the contents of the array for later comparison with the contents of the buffer.
Invoke contiguous put method
Then the code in Listing 10 sets the position of the buffer to 0, and invokes the put method, passing the following parameters:
- The array referred to by a2 as the source array from which the bytes are to be read
- A value of 1 for the offset within the array of the first byte to be read
- A value of 8 for the maximum number of bytes to be read from the array
The output produced by the code in Listing 10 is shown in Figure 10.
Contiguous put from array Show array data 0 0 0 1 2 20 21 22 0 0 Buffer data for buf1 0 0 1 2 20 21 22 0 Figure 10 |
Figure 10 shows that 8 bytes were transferred from the array to the buffer, beginning with the value at array index 1. These eight bytes were written into the buffer beginning at position 0, overwriting the eight values that previously existed in the buffer.
Another non-direct buffer
Here are some of the details from Sun regarding direct and non-direct buffers:
"A byte buffer is either direct or non-direct. ... A direct byte buffer may be created by invoking the allocateDirect factory method of this class. ... Whether a byte buffer is direct or non-direct may be determined by invoking its isDirect method. This method is provided so that explicit buffer management can be done in performance-critical code."I will have more to say about direct buffers later on.
Listing 11 illustrates the creation of a non-direct buffer through
allocation.
ByteBuffer buf2 = ByteBuffer.allocate(10); System.out.println( "Buffer is direct: " + buf2.isDirect()); Listing 11 |
Allocate the buffer
Note that the code in Listing 11 invokes the allocate factory method of the ByteBuffer class (and not the allocateDirect factory method, which would create a direct buffer).
Then, for purpose of illustration, the code in Listing 11 invokes the isDirect method on the new buffer to determine if it is direct or non-direct.
The output
Figure 11 shows the output produced by the code in Listing 11.
Buffer is direct: false Figure 11 |
As you can see from the output, this buffer is non-direct.
Not a wrap
This approach to creating a new buffer is different from the wrapping approach illustrated earlier in the program. This buffer is initially empty, and its capacity is 10 elements (the value passed as a parameter to the factory method). As an empty buffer of type byte, the initial value of each of its elements is zero.
The new buffer's initial position is 0, its limit is the same as its capacity, and its mark is undefined.
Although this buffer was not created by wrapping an existing array, the buffer does have a backing array, and the offset of the backing array is zero (I will have more to say about the backing array later).
Contiguous put from buffer as source of data
This new empty buffer is referred to by the reference variable named
buf2.
Listing 12 illustrates the transfer of a contiguous block of bytes from
the buffer referred to by buf1 to this new buffer.
showBufferData(buf2, "buf2"); buf2.position(1); buf1.position(0); buf2.put(buf1); buf1.position(0); showBufferData(buf1, "buf1"); buf2.position(0); showBufferData(buf2, "buf2"); Listing 12 |
The code in Listing 12 begins by displaying the contents of the new buffer. We will see shortly that each of the elements in the new buffer is initialized to a value of zero.
Set positions and invoke contiguous put method
Then the code in Listing 12 sets position values for each of the buffers and invokes the contiguous put method on buf2, passing buf1 as a parameter. The parameter specifies the buffer that acts as a source for the data to be transferred. The position values of each of the buffers control the data that is actually transferred.
What data is actually transferred?
In particular, this version of the put method transfers the bytes remaining in the source buffer (between position and limit) into the destination buffer on which the method is invoked. The data is transferred into the destination buffer beginning at the current position for the destination buffer. Then the position properties of both buffers are incremented by the number of bytes transferred.
Exceptions can be thrown
Several conditions can cause exceptions to be thrown, but I will simply refer you to the Sun documentation for the details in that regard.
The output
After the data has been transferred, the contents of both buffers are
displayed. The output produced by the code in Listing 12 is shown
in Figure 12.
Buffer data for buf2 0 0 0 0 0 0 0 0 0 0 Buffer data for buf1 0 0 1 2 20 21 22 0 Buffer data for buf2 0 0 0 1 2 20 21 22 0 0 Figure 12 |
As you can see, the initial values of each of the ten elements in the new empty buffer are zero.
All eight bytes of data are transferred from buf1 (beginning at position 0 in buf1) to buf2 (beginning at position 1 in buf2).
(Note that the boldface values don't mean anything special here.
I will refer back to them later.)
