The Channel Interfaces
Much of the channel functionality is abstracted into a series of different interfaces. Interfaces are used rather than abstract classes because there's frequently a need to mix and match different components. Some channels can be read, some can be written, and some can be both read and written. Some channels are interruptible and some are not. Some channels scatter and some gather. In practice, these capabilities appear in almost any combination.
15.1.1. Channel
The key superinterface is java.nio.channels.Channel. This interface defines the only two methods all channels implement, isOpen( ) and close( ):
public boolean isOpen( ) public void close( ) throws IOException
That is, the only things you know you can do with any channel are find out whether or not it's open and close it. Given this limited functionality, it's rare to work with just a Channel variable instead of using a more detailed type.
15.1.2. ReadableByteChannel and WritableByteChannel
The next most basic interfaces are ReadableByteChannel and WritableByteChannel, which are used for channels that read and write bytes, respectively. Some channels can both read and write, but most channels do one or the other. In theory, channels could work with ints, doubles, strings, and so on. In practice, though, it's always bytes.
ReadableByteChannel declares a single method that fills a ByteBuffer with data read from the channel:
public int read(ByteBuffer target) throws IOException
WritableByteChannel declares a single method that drains data from a ByteBuffer and writes it out to the channel:
public int write(ByteBuffer source) throws IOException
These are the two methods you saw used in the examples in the previous chapter. Each returns the number of bytes read or written, and each advances the position of the buffer argument by the same amount.
15.1.2.1. ByteChannel
Channels that can both read and write sometimes implement the ByteChannel interface. This is simply a convenience interface that implements both ReadableByteChannel and WritableByteChannel. It does not declare any additional methods.
public interface ByteChannel extends ReadableByteChannel, WritableByteChannel
In the core library, this is implemented by SocketChannel, DatagramChannel, and FileChannel. Other channels implement one or the other.
15.1.2.2. Exceptions
The read( ) and write( ) methods are declared to throw IOException, which they do for all the same reasons a stream might throw an IOException: the disk you're writing to fills up, the remote network server you're talking to crashes, your cat dislodges the Ethernet cable from the back of the computer, and so on.
You cannot write to or read from a closed channel; if you try, the method will throw a ClosedChannelException, a subclass of IOException. If the channel is closed by another thread while the write or read is in progress, AsynchronousCloseException, a subclass of ClosedChannelException, is thrown. If another thread interrupts the thread's read or write operation, ClosedByInterruptException, a subclass of AsynchronousCloseException, is thrown.
The read( ) and write( ) methods can also throw runtime exceptions, which usually result from logic errors in the program. The read( ) method throws a NonReadableChannelException if you try to read from a channel that has been opened only for writing. (You'd think such a channel would not be an instance of ReadableByteChannel in the first place, but sometimes the strictures of API design require Java to act as if it were weakly typed, static type checking notwithstanding.) Similarly, the write( ) method throws a NonWritableChannelException if you try to write to a channel that was opened only for reading.
15.1.3. Gathering and Scattering Channels
Most classes that implement ReadableByteChannel also implement its subinterface, ScatteringByteChannel . This interface adds two more read( ) methods that can use several buffers:
public long read(ByteBuffer[] dsts) throws IOException public long read(ByteBuffer[] dsts, int offset, int length) throws IOException
After the first buffer fills up, data read from the channel is placed in the second buffer in dsts. After the second buffer fills up, data is then placed in the third buffer, and so on. The second method is the same except that it starts with the buffer at offset and continues through length buffers. That is, offset and length define the subset of buffers to use from the dsts array, not the offset and length inside each individual buffer.
Similarly, most classes that implement WritableByteChannel also implement its subinterface, GatheringByteChannel . This interface adds two more write( ) methods that drain data from an array of buffers:
public long write(ByteBuffer[] srcs) throws IOException public long write(ByteBuffer[] srcs, int offset, int length) throws IOException
After the first buffer empties, the channel starts draining the second buffer. After the second buffer is empty, data is drained from the third buffer, and so on. Again, the three-argument version is the same except that it starts draining the buffer at offset and continues through length buffers.
This is most useful when the data written to a channel consists of several distinct pieces. For instance, an HTTP server might store the HTTP header in one buffer and the HTTP body in another, then write both using a gathering write. If you're writing a file containing individual records, each record could be stored in a separate buffer.
These methods mostly throw the same exceptions for the same reasons as the nongathering/scattering read( ) and write( ) methods do: IOException, ClosedChannelException, NonReadableChannelException, and so on. They can also throw an IndexOutofBoundsException if the offset or the length exceeds the bounds of the array.
As a very simple example, let's suppose you wish to concatenate several files, as you might with the Unix cat utility. You could map each input file into a ByteBuffer and write all the buffers into a new File, as Example 15-1demonstrates.
Example 15-1. Gathering channels
import java.io.*; import java.nio.*; import java.nio.channels.*; public class NIOCat { public static void main(String[] args) throws IOException { if (args.length < 2) { System.err.println("Usage: java NIOCat inFile1 inFile2... outFile"); return; } ByteBuffer[] buffers = new ByteBuffer[args.length-1]; for (int i = 0; i < args.length-1; i++) { RandomAccessFile raf = new RandomAccessFile(args[i], "r"); FileChannel channel = raf.getChannel( ); buffers[i] = channel.map(FileChannel.MapMode.READ_ONLY, 0, raf.length( )); } FileOutputStream outFile = new FileOutputStream(args[args.length-1]); FileChannel out = outFile.getChannel( ); out.write(buffers); out.close( ); } } |
Example 15-1 makes one dangerous assumption, though: it only works if the write( ) method writes every byte from every buffer. For file channels in blocking mode, this is likely the case and will be true most of the time. A gathering write tries to write all the bytes possible, and more often than not it will do so. However, some channels may be limited. For instance, a socket channel operating in nonblocking mode cannot write more bytes than the local TCP buffer will hold. A more robust solution would write continuously in a loop until none of the buffers had any remaining data:
outer: while (true) { out.write(buffers); for (int i = 0; i < buffers.length; i++) { if (buffers[i].hasRemaining( )) continue outer; } break; }
Honestly, this is ugly, and on a nonblocking channel I'd be inclined to just write the buffers individually instead, like so:
for (int i = 0; i < buffers.length; i++) { while (buffers[i].hasRemaining( )) out.write(buffers[i]); }