1. 引言
I/O流或者输入/输出流指的是计算机与外部世界或者一个程序与计算机的其余部分的之间的接口。新的输入/输出(NIO)库是在JDK 1.4中引入的。NIO弥补了原来的I/O的不足,它在标准Java代码中提供了高速的、面向块的I/O。
原来的I/O库与NIO最重要的区别是数据打包和传输的方式的不同,原来的 I/O 以流 的方式处理数据,而 NIO 以块 的方式处理数据。
面向流的I/O系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的I/O通常相当慢。
NIO与原来的I/O有同样的作用和目的,但是它使用块I/O的处理方式。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的I/O缺少一些面向流的I/O所具有的优雅性和简单性。
2. 从一个例子开始
下面我们从一个简单的使用IO和NIO读取一个文件中的内容为例,来进入NIO的学习之旅。
使用IO来读取指定文件中的前1024字节并打印出来:
Java代码
- /**
- * 使用IO读取指定文件的前1024个字节的内容。
- * @param file 指定文件名称。
- * @throws java.io.IOException IO异常。
- */
- public void ioRead(String file) throws IOException {
- FileInputStream in = new FileInputStream(file);
- byte[] b = new byte[1024];
- in.read(b);
- System.out.println(new String(b));
- }
- /**
- * 使用NIO读取指定文件的前1024个字节的内容。
- * @param file 指定文件名称。
- * @throws java.io.IOException IO异常。
- */
- public void nioRead(String file) throws IOException {
- FileInputStream in = new FileInputStream(file);
- FileChannel channel = in.getChannel();
- ByteBuffer buffer = ByteBuffer.allocate(1024);
- channel.read(buffer);
- byte[] b = buffer.array();
- System.out.println(new String(b));
- }
从上面的例子中可以看出,NIO以通道Channel和缓冲区Buffer为基础来实现面向块的IO数据处理。下面将讨论并学习NIO 库的核心概念以及从高级的特性到底层编程细节的几乎所有方面。
3. 核心概念:通道和缓冲区
1) 概述:
通道和缓冲区是NIO中的核心对象,几乎在每一个I/O操作中都要使用它们。
通道Channel是对原I/O包中的流的模拟。到任何目的地(或来自任何地方)的所有数据都必须通过一个Channel对象。
缓冲区Buffer实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区中。
2) 缓冲区:
Buffer是一个容器对象,它包含一些要写入或者刚读出的数据。在NIO中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中,您将数据直接写入或者将数据直接读到Stream对象中。
在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问NIO中的数据,您都是将它放到缓冲区中。
缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
最常用的缓冲区类型是ByteBuffer。 一个ByteBuffer可以在其底层字节数组上进行get/set操作(即字节的获取和设置)。
ByteBuffer不是NIO中唯一的缓冲区类型。事实上,对于每一种基本Java类型都有一种缓冲区类型:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
每一个Buffer类都是Buffer接口的一个实例。 除了ByteBuffer, 每一个Buffer类都有完全一样的操作,只是它们所处理的数据类型不一样。因为大多数标准I/O操作都使用ByteBuffer,所以它具有所有共享的缓冲区操作以及一些特有的操作。
下面的UseFloatBuffer列举了使用类型化的缓冲区FloatBuffer的一个应用例子:
Java代码
- /**
- * 使用 float 缓冲区。
- * @version 1.00 2010-5-19, 10:30:59
- * @since 1.5
- * @author ZhangShixi
- */
- public class UseFloatBuffer {
- public static void main(String[] args) {
- // 分配一个容量为10的新的 float 缓冲区
- FloatBuffer buffer = FloatBuffer.allocate(10);
- for (int i = 0; i < buffer.capacity(); i++) {
- float f = (float) Math.sin((((float) i) / 10) * (2 * Math.PI));
- buffer.put(f);
- }
- // 反转此缓冲区
- buffer.flip();
- // 告知在当前位置和限制之间是否有元素
- while (buffer.hasRemaining()) {
- float f = buffer.get();
- System.out.println(f);
- }
- }
- }
3) 通道:
Channel是对原I/O包中的流的模拟,可以通过它读取和写入数据。拿NIO与原来的I/O做个比较,通道就像是流。
正如前面提到的,所有数据都通过Buffer对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道 读入缓冲区,再从缓冲区获取这个字节。
通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类), 而通道可以用于读、写或者同时用于读写。
因为它们是双向的,所以通道可以比流更好地反映底层操作系统的真实情况。特别是在UNIX模型中,底层操作系统通道是双向的。
4. 从理论到实践:NIO中的读和写
1) 概述:
读和写是I/O的基本过程。从一个通道中读取很简单:只需创建一个缓冲区,然后让通道将数据读到这个缓冲区中。写入也相当简单:创建一个缓冲区,用数据填充它,然后让通 道用这些数据来执行写入操作。
2) 从文件中读取:
如果使用原来的I/O,那么我们只需创建一个FileInputStream并从它那里读取。而在NIO中,情况稍有不同:我们首先从FileInputStream获取一个FileChannel对象,然后使用这个通道来读取数据。
在NIO系统中,任何时候执行一个读操作,您都是从通道中读取,但是您不是直接从通道读取。因为所有数据最终都驻留在缓冲区中,所以您是从通道读到缓冲区中。
因此读取文件涉及三个步骤:
(1) 从FileInputStream获取Channel。
(2) 创建Buffer。
(3) 将数据从Channel读到Buffer 中。
现在,让我们看一下这个过程。
Java代码
- // 第一步是获取通道。我们从 FileInputStream 获取通道:
- FileInputStream fin = new FileInputStream( "readandshow.txt" );
- FileChannel fc = fin.getChannel();
- // 下一步是创建缓冲区:
- ByteBuffer buffer = ByteBuffer.allocate( 1024 );
- // 最后,需要将数据从通道读到缓冲区中:
- fc.read( buffer );
您会注意到,我们不需要告诉通道要读多少数据到缓冲区中。每一个缓冲区都有复杂的内部统计机制,它会跟踪已经读了多少数据以及还有多少空间可以容纳更多的数据。我们将在缓冲区内部细节中介绍更多关于缓冲区统计机制的内容。
3) 写入文件:
在 NIO 中写入文件类似于从文件中读取。
Java代码
- // 首先从 FileOutputStream 获取一个通道:
- FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
- FileChannel fc = fout.getChannel();
- // 下一步是创建一个缓冲区并在其中放入一些数据,这里,用message来表示一个持有数据的数组。
- ByteBuffer buffer = ByteBuffer.allocate( 1024 );
- for (int i=0; i<message.length; ++i) {
- buffer.put( message[i] );
- }
- buffer.flip();
- // 最后一步是写入缓冲区中:
- fc.write( buffer );
注意在这里同样不需要告诉通道要写入多数据。缓冲区的内部统计机制会跟踪它包含多少数据以及还有多少数据要写入。
4) 读写结合:
下面的示例将展示使用读写结合,将一个文件的所有内容拷贝到另一个文件中。
Java代码
- /**
- * 将一个文件的所有内容拷贝到另一个文件中。
- *
- * CopyFile.java 执行三个基本操作:
- * 首先创建一个 Buffer,然后从源文件中将数据读到这个缓冲区中,然后将缓冲区写入目标文件。
- * 程序不断重复 — 读、写、读、写 — 直到源文件结束。
- *
- * @version 1.00 2010-5-19, 10:49:46
- * @since 1.5
- * @author ZhangShixi
- */
- public class CopyFile {
- public static void main(String[] args) throws Exception {
- String infile = "C:\\copy.sql";
- String outfile = "C:\\copy.txt";
- // 获取源文件和目标文件的输入输出流
- FileInputStream fin = new FileInputStream(infile);
- FileOutputStream fout = new FileOutputStream(outfile);
- // 获取输入输出通道
- FileChannel fcin = fin.getChannel();
- FileChannel fcout = fout.getChannel();
- // 创建缓冲区
- ByteBuffer buffer = ByteBuffer.allocate(1024);
- while (true) {
- // clear方法重设缓冲区,使它可以接受读入的数据
- buffer.clear();
- // 从输入通道中将数据读到缓冲区
- int r = fcin.read(buffer);
- // read方法返回读取的字节数,可能为零,如果该通道已到达流的末尾,则返回-1
- if (r == -1) {
- break;
- }
- // flip方法让缓冲区可以将新读入的数据写入另一个通道
- buffer.flip();
- // 从输出通道中将数据写入缓冲区
- fcout.write(buffer);
- }
- }
- }