背景
JDK1.4的java.nio.*包中引入了新的JavaI/O类库,其目的在于 提高速度。实际上,旧的I/O包已经使用nio重新实现过。因此,即使不显式使用nio编写代码,也能从中受益。
I/O的应用场景分为文件I/O和网络I/O,在这里之研究前者。
组成部分
nio主要有三大组成部分:通道(Channel)、缓冲器(Buffer)、Selector(选择区)。
因为nio的数据传输结构更接近于操作系统执行I/O的方式:通道和缓冲器,而传统的I/O是面向字节流和字符流,所以nio的速度更快。
通道用于存放数据,缓冲器用于传输数据,选择区用于监听多个通道的事件(比如:连接打开,数据到达)。
通道和缓冲器
我们并没有直接和通道交互,我们只是和缓冲器交互,并把缓冲器派送到通道。通道要么从缓冲器获得数据,要么向缓冲器发送数据。
唯一直接与通道交互的缓冲器是ByteBuffer,即可以存储未加工字节的缓冲器。它是一个很基础的类:通过告知分配多少存储空间来创建一个ByteBuffer对象,并且还有一个选择集。用于以原始的字节形式或基本数据类型输出和读取数据。但是它不能直接读取或输出对象,即使是字符串对象也不行。虽然这种处理很低级,但是却更贴合操作系统的处理方式。
旧I/O类库中的FileInputStream、FileOutputStream、RandomAccessFile这三个类被修改了,用以产生FileChannel。这三个类都是字节操纵流,与nio性质一致。像Reader和Writer这种面向字符流的类就不能用于产生通道;但是java.nio.channels.Channels
类中提供了方法,可以在通道中产生Reader和Writer。
读写实例
public class GetChannel {
private static final int BSIZE = 2014;
public static void main(String[] args) throws IOException {
/**
* 写数据,
* 若已有内容,原内容会被覆盖;
* 若不存在文件,则会创建新文件并写入新内容
*/
FileChannel fc = new FileOutputStream("data.txt").getChannel();
fc.write(ByteBuffer.wrap("some text".getBytes()));
fc.close();
/**
* 添加内容至文件末尾
*/
fc = new RandomAccessFile("data.txt", "rw").getChannel();
fc.position(fc.size()); // 移动到文件末尾
fc.write(ByteBuffer.wrap("Some more".getBytes()));
fc.close();
/**
* 读取文件
*/
fc = new FileInputStream("data.txt").getChannel();
ByteBuffer buff = ByteBuffer.allocate(BSIZE);
fc.read(buff);
buff.flip();
while(buff.hasRemaining()){
System.out.print((char)buff.get());
}
}
}
channel
:对于上述三种流类,getChannel()
方法都可以获得一个FileChannel
。通道是一种相当基础的东西:可以向它传送用于读写的ByteBuffer,并且可以锁定文件的某些区域用于独占式访问ByteBuffer
:将字节存放于ByteBuffer
的方法之一是:使用一种“put”方法直接对它们进行填充,填入一个或多个字节,或基本数据类型的值。也可以使用wrap()
方法将已存在的字节数组“包装”到ByteBuffer
中,这样就把一个数组包装为ByteBuffer
缓冲器,一旦完成包装,底层数据就可以通过缓冲区或者直接访问。我们称第二种为数组支持的ByteBuffer
position
:可以使用FileChannel
的position
方法来在文件中移动FileChannel
。在上例中是将其移动到文件末尾,方便内容的增加。allocate
:对于只读访问,我们要显式使用静态的allocate()
方法来分配ByteBuffer
,nio的目标就是快速移动大量数据,因此ByteBuffer
的大小就显得尤为重要——事实上,这里使用的1K可能比我们通常要使用的小一点(必须通过实际运行程序来找到最佳尺寸)。使用allocateDirect
替代allocate
,以产生一个与操作系统有更高耦合性的“直接”缓冲器还有可能达到更高的速度。但是,这种分配会增加开支,并且具体的实现也随操作系统的不同而不同。read()
和flip()
:一旦调用read()
来告知FileChannel
向ByteBuffer
存储字节,就必须调用缓冲器上的flip()
,让它做好让别人读取字节的准备。如果打算使用缓冲器执行进一步的read()
操作,我们也必须调用clear()
来为每个read()
做好准备。如下例:
public class ChannelCopy {
private static final int BSIZE = 2014;
public static void main(String[] args) throws IOException {
FileChannel in = new FileInputStream("src/main/resources/in.txt").getChannel();
FileChannel out = new FileOutputStream("src/main/resources/out.txt").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
while(in.read(buffer) != -1){
buffer.flip(); // 准备写
out.write(buffer);
buffer.clear(); // 准备读
}
/**
* 常用的是直接使用transferTo 或者 transferFrom
*/
// in.transferTo(0, in.size(), out);
// 或者out.transferFrom(in, 0, in.size());
}
}
这里有两个FileChannel
,一个用于读,一个用于写。每次read()
操作之后,就会将数据输入到缓冲器中,flip()
则是准备缓冲器以便它的信息可以由write()
提取。write()
操作之后,信息仍在缓冲区中,接着clear()
操作则对所有的内部指针重新安排,以便缓冲器在另一个read()
操作期间能够做好接收数据的准备。
文件内容复制之类的操作,一般使用transferTo
或者transferFrom
方法