31 July 2008

Java ArrayWrapperList

I have some Java networking code that uses the Java NIO (java.nio) package and the Java Concurrency utilities (java.util.concurrent) package. My code is structured to enqueue pending output on a write buffer queue. The write buffer queue is a sequence of ByteBuffers implemented as a java.util.concurrent.LinkedBlockingDeque<ByteBuffer>.

Now, I want to perform a gathering write operation of pending ByteBuffers in the write buffer queue. The BlockingDeque has a drainTo(Collection<? super E>) method that removes elements from the Deque and places them in a specified Collection of the appropriate type. Here's the snag: the java.nio.channels.SocketChannel write() method requires a ByteBuffer[], not a Collection<ByteBuffer>.

Converting from a Collection<ByteBuffer> to a ByteBuffer[] seems as simple as calling the java.util.Arrays.asList(T...) method. Just wrap the target array as a List then call write() with the filled array:

ByteBuffer[] writeBufferArray = new ByteBuffer[BUFFER_COUNT]; // write() needs this
List<ByteBuffer> writeBufferList = Arrays.asList(writeBufferArray); // drainTo() needs this
int writeBufferCount = writeBufferDeque.drainTo(writeBufferList); // fills writeBufferArray?
socketChannel.write(writeBufferArray, 0, writeBufferCount);

Unfortunately this does not work because an UnsupportedOperationExcepion is raised on the call to drainTo(). This happens because the List produced by Arrays.asList(T...) does not permit append operations. It enforces this restriction by not implementing the java.util.AbstractList.add(int, Object) method.

It would be easy enough to copy the ByteBuffers to a "temporary" Collection<ByteBuffer>, then call the toArray() method, but that results in a "double copy." Each ByteBuffer reference would be copied from the BlockingDeque to the temporary Collection<ByteBuffer>, then copied from the temporary Collection<ByteBuffer> to the array produced by toArray().

Avoiding the double copy means that some way must be found to write directly to a ByteBuffer[] from drainTo(). Since drainTo() demands a Collection, some alternative array wrapper must be used.

One potential candidate is the venerable java.util.ArrayList. An ArrayList properly encapsulates an array by creating an internal copy of any array given to it and producing a defensive copy when toArray() is invoked on it. ArrayList does its job well, but it's not suited to this task because it performs copy operations to and from its own array, not a client provided array.

My solution for a List that writes-through to an externally accessible array is called ArrayWrapperList. It works like this:

ByteBuffer[] writeBufferArray = new ByteBuffer[BUFFER_COUNT];
List<ByteBuffer> writeBufferList = new ArrayWrapperList<ByteBuffer>();
writeBufferDeque.drainTo(writeBufferList); // writes-through directly to writeBufferArray
socketChannel.write(writeBufferArray, 0, writeBufferCount);

Conceptually, the code behaves as though writeBufferList.toArray(writeBufferArray) is invoked immediately after the call to drainTo(). It should be noted that in my production version I invoke drainTo(Collection<? super E>, int) not drainTo(Collection<? super E>). The upper bound on the drained elements ensures that the size of writeBufferArray is not exceeded.

ArrayWrapperList does not encapsulate the array it manages like java.util.ArrayList. Rather ArrayWrapperList trades-off encapsulation for the ability to write directly to a client accessible array.

ArrayWrapperList source code is available as part of the Virtual Team Tools project.