1、什么是NIO
NIO即New IO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO弥补了原来的I/O的不足,它在标准Java代码中提供了高速的、NIO主要用到的是块,所以NIO的效率要比IO高很多。NIO和IO最大的区别是数据打包和传输方式。IO是以流的方式处理数据,而NIO是以块的方式处理数据。
面向流的IO一次一个字节的处理数据,一个输入流产生一个字节,一个输出流就消费一个字节。
面向块的IO系统以块的形式处理数据。每一个操作都在一步中产生或消费一个数据块。
下面我们从一个简单的使用IO和NIO读取一个文件中的内容为例,来进入NIO的学习之旅。
使用IO来读取指定文件中的前1024字节并打印出来:
<span style="font-size:18px;">/**
* 使用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));
} </span>
使用NIO来读取<span style="font-size:18px;">/**
* 使用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));
} </span>
从上面的例子中可以看出,NIO以通道Channel和缓冲区Buffer为基础来实现面向块的IO数据处理。下面将讨论并学习NIO 库的核心概念以及从高级的特性到底层编程细节的几乎所有方面。2、NIO基础
Buffer和Channel是标准NIO中的核心对象(网络NIO中还有个Selector核心对象),几乎每一个IO操作中都会用到它们。Channel是对原IO中流的模拟,任何来源和目的数据都必须通过一个Channel对象。一个Buffer实质上是一个容器对象,发给Channel的所有对象都必须先放到Buffer中;同样的,从Channel中读取的任何数据都要读到Buffer中。
2.1 Buffer介绍
Buffer是一个对象,它包含一些要写入或读出的数据。在NIO中,数据是放入buffer对象的,而在IO中,数据是直接写入或者读到Stream对象的。应用程序不能直接对 Channel 进行读写操作,而必须通过 Buffer 来进行,即 Channel 是通过 Buffer 来读写数据的。在NIO中,所有的数据都是用Buffer处理的,它是NIO读写数据的中转池。
使用 Buffer 读写数据一般遵循以下四个步骤:
- 写入数据到 Buffer;
- 调用 flip() 方法;
- 从 Buffer 中读取数据;
- 调用 clear() 方法或者 compact() 方法。
Buffer主要有如下几种:ByteBuffer、charBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer。
2.3 Channel介绍
Channel是一个对象,可以通过它读取和写入数据。可以把它看做IO中的流。但是它和流相比还有一些不同:
- Channel是双向的,既可以读又可以写,而流是单向的
- Channel可以进行异步的读写
- 对Channel的读写必须通过buffer对象
在Java NIO中Channel主要有如下几种类型:
- FileChannel:从文件读取数据的
- DatagramChannel:读写UDP网络协议数据
- SocketChannel:读写TCP网络协议数据
- ServerSocketChannel:可以监听TCP连接
3、NIO中的读和写
IO中的读和写,对应的是数据和Stream,NIO中的读和写,则对应的就是通道和缓冲区。NIO中从通道中读取:创建一个缓冲区,然后让通道读取数据到缓冲区。NIO写入数据到通道:创建一个缓冲区,用数据填充它,然后让通道用这些数据来执行写入。
3.1 从文件中读取
我们已经知道,在NIO系统中,任何时候执行一个读操作,您都是从Buffer中读取,而您不是直接从Channel中读取数据,因为所有的数据都必须用Buffer来封装,所以您应该是从Channel读取数据到Buffer。
因此,如果从文件读取数据的话,需要如下三步:
1)从FileInputStream获取Channel
2)创建Buffer
3)从Channel读取数据到Buffer
下面我们看一下具体过程:
第一步:获取通道
<span style="font-size:18px;">FileInputStream fin = new FileInputStream( "readandshow.txt" );
FileChannel fc = fin.getChannel();</span>
第二步:创建缓冲区
<span style="font-size:18px;">ByteBuffer buffer = ByteBuffer.allocate( 1024 );</span>
第三步:将数据从通道读到缓冲区
<span style="font-size:18px;"><pre name="code" class="java"><span style="font-family:SimSun;font-size:18px;">fc.read( buffer );</span></span>
3.2 写入数据到文件
类似于从文件读数据,
第一步:获取一个通道
<span style="font-size:18px;">FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
FileChannel fc = fout.getChannel();</span>
第二步:创建缓冲区,将数据放入缓冲区<span style="font-size:18px;">ByteBuffer buffer = ByteBuffer.allocate( 1024 );
for (int i=0; i<message.length; ++i) {
buffer.put( message[i] );
}
buffer.flip();</span>
第三步:把缓冲区数据写入通道中
<span style="font-size:18px;">fc.write( buffer );</span>
4、需要注意的点
4.1 检查状态
当没有更多的数据时,拷贝就算完成,此时 read() 方法会返回 -1 ,我们可以根据这个方法判断是否读完。
<span style="font-size:18px;">int r= fcin.read( buffer );
if (r==-1) {
break;
}</span>
4.2 Buffer类的flip、clear方法
控制buffer状态的三个变量
- position:跟踪已经写了多少数据或读了多少数据,它指向的是下一个字节来自哪个位置
- limit:代表还有多少数据可以取出或还有多少空间可以写入,它的值小于等于capacity。
- capacity:代表缓冲区的最大容量,一般新建一个缓冲区的时候,limit的值和capacity的值默认是相等的。
flip、clear这两个方法便是用来设置这些值的。
flip方法
我们先看一下flip的源码:
<span style="font-size:18px;">public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}</span>
在上面的FileCopy程序中,写入数据之前我们调用了buffer.flip();
方法,这个方法把当前的指针位置position设置成了limit,再将当前指针position指向数据的最开始端,我们现在可以将数据从缓冲区写入通道了。 position 被设置为 0,这意味着我们得到的下一个字节是第一个字节。 limit 已被设置为原来的 position,这意味着它包括以前读到的所有字节,并且一个字节也不多。clear方法
先看一下clear的源码:
<span style="font-size:18px;"> public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}</span>
在上面的FileCopy程序中,写入数据之后也就是读数据之前,我们调用了 buffer.clear();
方法,这个方法重设缓冲区以便接收更多的字节。上图显示了在调用 clear() 后缓冲区的状态。