15.9.3 使用Channel

15.9.3 使用Channel

Channel与流对象的区别

Channel类似于传统的流对象,但与传统的流对象有两个主要区别。

  1. Channel可以直接将指定文件的部分或全部直接映射成Buffer
  2. 程序不能直接访问Channel中的数据,包括读取、写入都不行,Channel只能与Buffer进行交互。也就是说:
    • 如果要从Channel中取得数据,必须先用BufferChannel中取出一些数据,然后让程序从Buffer中取出这些数据;
    • 如果要将程序中的数据写入Channel,一样先让程序将数据放入Buffer中,程序再将Buffer里的数据写入Channe

Channel分类

JavaChannel接口提供了DatagramChannelFileChannelPipe.SinkChannelPipe.SourceChannelSelectableChannelServerSocketChannelSocketChannel等实现类,本节主要介绍FileChannel的用法。
根据这些Channel的名字不难发现,新IO里的Channel是按功能来划分的,例如

  • Pipe.SinkChannelPipe.SourceChannel是用于支持线程之间通信的管道Channel;
  • ServerSocketChannelSocketChannel是用于支持**TCP网络通信**的Channel;
  • DatagramChannel则是用于支持**UDP网络通信**的Channel

本书将会在第17章介绍网络通信编程的详细内容,如果需要掌握ServerSocketChannel,SocketChannelChannel的用法,可以参考本书第17章。

如何创建Channel

所有的Channel都不应该通过构造器来直接创建,而是通过传统的节点InputStreamOutputStreamgetChannel()方法来返回对应的Channel,不同的节点流获得的Channel不一样。例如:

  • FileInputStreamFileOutputStreamgetChannel()方法返回的是FileChannel,
  • PipedInputStreamPipedOutputStreamgetChannel()方法返回的是Pipe.SinkChannelPipe.SourceChannel

Channel常用方法

Channel中最常用的三类方法是map()read()write(),其中

  • map()方法用于将Channel对应的部分或全部数据映射成ByteBuffer;
  • readwrite方法都有一系列重载形式,这些方法用于从Buffer中读取数据或向Buffer中写入数据。

FileChannel的map方法

map()方法的方法签名为:

1
abstract MappedByteBuffer map(FileChannel.MapMode mode, long position, long size)
  • 第一个参数执行映射时的模式,分别有只读、读写等模式
  • 而第二个、第三个参数用于控制将Channel的哪些数据映射成ByteBuffer

程序 FileChannel示例

下面程序示范了直接将FileChannel的全部数据映射成ByteBuffer的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;

public class FileChannelTest {
public static void main(String[] args) {
File f = new File("FileChannelTest.java");
try (
// 创建FileInputStream,以该文件输入流创建FileChannel
FileChannel inChannel = new FileInputStream(f).getChannel();
// 以文件输出流创建FileBuffer,用以控制输出
FileChannel outChannel = new FileOutputStream("a.txt").getChannel()) {
// 将FileChannel里的全部数据映射成ByteBuffer
MappedByteBuffer buffer =
inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length()); // ①
// 使用GBK的字符集来创建解码器
// Charset charset = Charset.forName("GBK");
Charset charset = Charset.forName("utf-8");
// 直接将buffer里的数据全部输出到outChannel中
outChannel.write(buffer); // ②

// 再次调用buffer的clear()方法,复原limit、position的位置,准备再次读取Buffer
buffer.clear();
// 创建解码器(CharsetDecoder)对象
CharsetDecoder decoder = charset.newDecoder();
// 使用解码器将ByteBuffer转换成CharBuffer
CharBuffer charBuffer = decoder.decode(buffer);
// CharBuffer的toString方法可以获取对应的字符串
System.out.println(charBuffer);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

上面程序中分别使用FileInputStreamFileOutputStream来获取FileChannel,虽然FileChannel既可以读取也可以写入,

  • 但**FileInputStream获取的FileChannel只能读**,
  • 而**FileOutputStream获取的FileChannel只能写**。

程序中①号代码:

1
2
MappedByteBuffer buffer = 
inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length()); // ①

直接将指定Channel中的全部数据映射成ByteBuffer.
然后程序中②号代码:

1
outChannel.write(buffer); // ②

直接将整个ByteBuffer的全部数据写入一个输出FileChannel中,这就完成了文件的复制.
程序后面部分为了能将FileChannelTest.java文件里的内容打印出来,使用了Charset类和CharsetDecoder类将ByteBuffer转换成CharBuffer关于CharsetCharsetDecoder下一节将会有更详细的介绍。

程序 RandomAccessFile获取的Channel

不仅InputStreamOutputStream包含了getChannel()方法,在RandomAccessFile中也包含了一个getChannel()方法,RandomAccessFile返回的FileChannel是只读的还是读写的,则取决于RandomAccessFile打开文件的模式
例如,下面程序将会对a.txt文件的内容进行复制,追加在该文件后面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.*;
import java.nio.*;
import java.nio.channels.*;

public class RandomFileChannelTest {
public static void main(String[] args) throws IOException {
File f = new File("a.txt");
try (
// 创建一个RandomAccessFile对象
RandomAccessFile raf = new RandomAccessFile(f, "rw");
// 获取RandomAccessFile对应的Channel
FileChannel randomChannel = raf.getChannel()) {
// 将Channel中所有数据映射成ByteBuffer
ByteBuffer buffer = randomChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
// 把Channel的记录指针移动到最后
// 代码1
randomChannel.position(f.length());
// 将buffer中所有数据输出
randomChannel.write(buffer);
}
}
}

上面程序中的代码1:

1
randomChannel.position(f.length());

可以将Channel的记录指针移动到该Channel的最后,从而可以让程序将指定ByteBuffer的数据追加到该Channel的后面。每次运行上面程序,都会把a.txt文件的内容复制份,并将全部内容追加到该文件的后面。

像传统IO那样使用Channel和Buffer读写文件

如果读者习惯了传统IO的“用竹筒多次重复取水”的过程,或者担心Channel对应的文件过大,使用map()方法一次将所有的文件内容映射到内存中引起性能下降,也可以使用ChannelBuffer传统的“用竹筒多次重复取水”的方式。
如下程序所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;

public class ReadFile {
public static void main(String[] args) throws IOException {
try (
// 创建文件输入流
FileInputStream fis = new FileInputStream("ReadFile.java");
// 创建一个FileChannel
FileChannel fcin = fis.getChannel()) {
// 定义一个ByteBuffer对象,用于重复取水
ByteBuffer bbuff = ByteBuffer.allocate(256);
// 将FileChannel中数据读取到ByteBuffer中
while (fcin.read(bbuff) != -1) {
// 准备读取Buffer中的数据
bbuff.flip();

// 创建Charset对象
// Charset charset = Charset.forName("GBK");
Charset charset = Charset.forName("utf-8");
// 创建解码器(CharsetDecoder)对象
CharsetDecoder decoder = charset.newDecoder();
// 将ByteBuffer的内容转码
CharBuffer cbuff = decoder.decode(bbuff);
System.out.print(cbuff);

// 将Buffer初始化,为下一次读取数据做准备
bbuff.clear();
}
}
}
}

上面代码虽然使用FileChannelBuffer来读取文件,但处理方式和使用InputStreambyte[]来读取文件的方式几乎一样,都是采用“用竹筒多次重复取水”的方式。但因为Buffer提供了fip()clear()两个方法,所以程序处理起来比较方便

类似传统方式使用NIO步骤

  • 读取数据到Buffer
  • 每次读取数据后调用fip()方法将没有数据的区域“封印”起来,避免程序从Buffer中取出null值;
  • Buffer中取出数据
  • 数据取出后立即调用clear()方法将Bufferposition设0,为下次读取数据做准备。