October 31, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

The ByteBuffer Class in Java

  • August 20, 2002
  • By Richard G. Baldwin
  • Send Email »
  • More Articles »
... in Java by Richard G Baldwin

Java Programming Notes # 1782


Preface

New features in SDK Version 1.4.0

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
I explained the use of the Buffer class in a previous article entitled Understanding the Buffer class in Java. As shown above, one of the subclasses of Buffer is named ByteBuffer.  The ByteBuffer class is very important because it forms a basis for the use of channels in Java. (The concept of I/O channels is another new feature of version 1.4, which I plan to cover in a future lesson.)

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

The inheritance model

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
Four common capabilities

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.
(In addition, some of the subclasses provide capabilities that are specific to the type involved.  For example, the CharBuffer class provides capabilities that are specific to the use of characters and strings, such as a method that wraps a String into a CharBuffer object.)

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.
In addition, as I mentioned earlier, the ByteBuffer class is very important in understanding I/O channels in Java, which I intend to discuss in a future lesson.

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.
Various methods are provided by the Buffer class to deal with these properties, and these methods are inherited into the ByteBuffer class.  I will use some of those methods in the sample program in this lesson.  However, an earlier lesson (see the link above) that explained the use of the Buffer class also explained how to use all of the property-manipulating methods of the Buffer class.  If you aren't familiar with those methods, you might want to refer back to that lesson.

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
(Note that the property getter methods of the Buffer class do not conform to the JavaBeans design patterns for property getter methods.)

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

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.
Absolute get method

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
This version of the get method copies the specified number of bytes from the buffer into the array, starting at the current position of the buffer and at the given offset in the array. The position of the buffer is then incremented by the specified number of bytes.

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

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



Page 1 of 3



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel