Compaction
Buffers are often used for sequential reading and writing. That is, first some data is read from a file, a network connection, or some other source and stored in the buffer. Next, data is drained from the buffer and written into a different file, network connection, or some other destination. However, the output that drains data from the buffer may not move as quickly as the input that fills the buffer. For instance, if data is being read from a file and written onto a network connection, input is likely to substantially outpace output.
To assist in such scenarios, many buffers can be compacted by invoking their compact( ) methods. This is the compact( ) method for ByteBuffer:
public abstract ByteBuffer compact( )
Each of the seven buffer classes has its own compact method that differs only in return type. For example, this is the compact method for DoubleBuffer:
public abstract DoubleBuffer compact( )
Compacting removes all the data from the buffer before the current position, then shifts the remaining data backwards in the buffer to the beginning. Finally, the limit is set to the capacity, and the position is set to the first empty space. For example, suppose we put five ints into a buffer, like this:
IntBuffer buffer = IntBuffer.allocate(8); buffer.put(10).put(20).put(30).put(40).put(50);
The buffer is now in the state shown in Figure 14-15.
We now flip the buffer to prepare it for draining and read three ints from it using a bulk get:
buffer.flip( ); int[] data = new int[3] buffer.get(data);
Figure 14-15. A partially filled int buffer
Now the buffer is in the state shown in Figure 14-16.
Figure 14-16. The buffer when three ints have been read
If we just want to continue draining from this point, we're good to go. However, if instead we now want to fill the buffer with more data, we have several problems. First, the position is set to 3, not 5. If we start putting now, we'll overwrite data that hasn't been processed. We could move the position to 5 and the limit to 8, but we'd still only have three empty slots left, and we may have more data than that. We could clear the buffer, but then we'd lose the unread data. Any manipulation of the position and the limit really isn't going to solve these problems. Instead, we call compact( ):
buffer.compact( );
This places the buffer in the state shown in Figure 14-17. As you can see, the two remaining ints are still available, and the position has been updated to allow as much data to be put in the buffer as possible without losing any unprocessed elements.
Figure 14-17. A compacted buffer
Example 14-2 demonstrates one possible use of the compact( ) method. This example copies a file, like the earlier Example 14-1. However, it uses only a single loop. One read and one write are performed in each pass through the loop. The flip( ) method makes the buffer ready for output and the compact( ) method makes it ready for input.
Example 14-2. Copying files using NIO
import java.io.*; import java.nio.*; import java.nio.channels.*; public class NIODuplicator { public static void main(String[] args) throws IOException { FileInputStream inFile = new FileInputStream(args[0]); FileOutputStream outFile = new FileOutputStream(args[1]); FileChannel inChannel = inFile.getChannel( ); FileChannel outChannel = outFile.getChannel( ); ByteBuffer buffer = ByteBuffer.allocate(1024*1024); int bytesRead = 0; while (bytesRead >= 0 || buffer.hasRemaining( )) { if (bytesRead != -1) bytesRead = inChannel.read(buffer); buffer.flip( ); outChannel.write(buffer); buffer.compact( ); } inChannel.close( ); outChannel.close( ); } } |
If the output tends to block and the input doesn't, this program might be somewhat faster than Example 14-1, but then again, it might not be. As with any detailed performance analysis, actual results vary from one system and platform to the next. A better, more reliable solution to this problem would involve nonblocking I/O, which I'll take up in Chapter 16.