I/O与NIO对比
I/O或者输入/输出:指的是计算机与外部世界或者一个程序与计算机的其余部分的之间的接口。它对于任何计算机系统都非常关键,因而所有I/O的主体实际上是内置在操作系统中的。单独的程序一般是让系统为它们完成大部分的工作。
在Java编程中,直到最近一直使用流的方式完成I/O。所有I/O都被视为单个的字节的移动,通过一个称为Stream的对象一次移动一个字节。流I/O用于与外部世界接触。它也在内部使用,用于将对象转换为字节,然后再转换回对象。
NIO与原来的I/O有同样的作用和目的,但是它使用不同的方式块I/O。正如您将在本教程中学到的,块I/O的效率可以比流I/O高许多。
NIO的创建目的是为了让Java程序员可以实现高速I/O而无需编写自定义的本机代码。NIO将最耗时的I/O操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度。
原来的I/O库(在java.io.*中)与NIO最重要的区别是数据打包和传输的方式。正如前面提到的,原来的I/O以流的方式处理数据,而NIO以块的方式处理数据。
面向流的I/O系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的I/O通常相当慢。
一个面向块的I/O系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的I/O缺少一些面向流的I/O所具有的优雅性和简单性。
通道和缓冲区
通道是对原I/O包中的流的模拟。到任何目的地(或来自任何地方)的所有数据都必须通过一个Channel对象。一个Buffer实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区中。
Buffer是一个对象,它包含一些要写入或者刚读出的数据。在NIO中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中,您将数据直接写入或者将数据直接读到Stream对象中。
在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问NIO中的数据,您都是将它放到缓冲区中。
缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
最常用的缓冲区类型是ByteBuffer。一个ByteBuffer可以在其底层字节数组上进行get/set操作(即字节的获取和设置)。
ByteBuffer不是NIO中唯一的缓冲区类型。事实上,对于每一种基本Java类型都有一种缓冲区类型:
l ByteBuffer
l CharBuffer
l ShortBuffer
l IntBuffer
l LongBuffer
l FloatBuffer
l DoubleBuffer
每一个Buffer类都是Buffer接口的一个实例。除了ByteBuffer,每一个Buffer类都有完全一样的操作,只是它们所处理的数据类型不一样。因为大多数标准I/O操作都使用ByteBuffer,所以它具有所有共享的缓冲区操作以及一些特有的操作。
下面是一个缓冲区的例子:
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);
}
Channel是一个对象,可以通过它读取和写入数据。拿NIO与原来的I/O做个比较,通道就像是流。
正如前面提到的,所有数据都通过Buffer对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者同时用于读写。
因为它们是双向的,所以通道可以比流更好地反映底层操作系统的真实情况。特别是在UNIX模型中,底层操作系统通道是双向的。
compact()方法可以对缓冲区进行压缩,作用是丢弃已经释放的数据,保留未释放的数据,并使缓冲区对重新填充容量准备就绪。
NIO的读和写
读和写是I/O的基本过程。从一个通道中读取很简单:只需创建一个缓冲区,然后让通道将数据读到这个缓冲区中。写入也相当简单:创建一个缓冲区,用数据填充它,然后让通道用这些数据来执行写入操作。
从文件中读取
在我们第一个练习中,我们将从一个文件中读取一些数据。如果使用原来的I/O,那么我们只需创建一个FileInputStream并从它那里读取。而在NIO中,情况稍有不同:我们首先从FileInputStream获取一个FileChannel对象,然后使用这个通道来读取数据。
在NIO系统中,任何时候执行一个读操作,您都是从通道中读取,但是您不是直接从通道读取。因为所有数据最终都驻留在缓冲区中,所以您是从通道读到缓冲区中。
因此读取文件涉及三个步骤:(1)从FileInputStream获取Channel,(2)创建Buffer,(3)将数据从Channel读到Buffer中。
第一步是获取通道。我们从FileInputStream获取通道:
FileInputStream fin = new FileInputStream("readandshow.txt");
FileChannel fc = fin.getChannel();
下一步是创建缓冲区:
ByteBuffer buffer = ByteBuffer.allocate(1024);
最后,需要将数据从通道读到缓冲区中,如下所示:
fc.read(buffer);
您会注意到,我们不需要告诉通道要读多少数据到缓冲区中。每一个缓冲区都有复杂的内部统计机制,它会跟踪已经读了多少数据以及还有多少空间可以容纳更多的数据。
写入文件
在NIO中写入文件类似于从文件中读取。首先从FileOutputStream获取一个通道:
FileOutputStream fout = new FileOutputStream("writesomebytes.txt");
FileChannel fc = fout.getChannel();
下一步是创建一个缓冲区并在其中放入一些数据-在这里,数据将从一个名为message的数组中取出,这个数组包含字符串"Somebytes"的ASCII字节。
ByteBuffer buffer = ByteBuffer.allocate(1024);
for(int i=0;i<message.length;++i){
buffer.put(message[i]);
}
buffer.flip();
最后一步是写入缓冲区中:
fc.write(buffer);
注意在这里同样不需要告诉通道要写入多数据。缓冲区的内部统计机制会跟踪它包含多少数据以及还有多少数据要写入。
读写结合
下面我们将看一下在结合读和写时会有什么情况。我们以一个名为CopyFile.java的简单程序作为这个练习的基础,它将一个文件的所有内容拷贝到另一个文件中。CopyFile.java执行三个基本操作:首先创建一个Buffer,然后从源文件中将数据读到这个缓冲区中,然后将缓冲区写入目标文件。这个程序不断重复—读、写、读、写—直到源文件结束。
CopyFile程序让您看到我们如何检查操作的状态,以及如何使用clear()和flip()方法重设缓冲区,并准备缓冲区以便将新读取的数据写到另一个通道中。
String infile="infile.txt";
String outfile="outfile.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){
buffer.clear();
//把内容读入buffer
int r = fcin.read(buffer);
//检查是否完成
if(r == -1){
break;
}
buffer.flip();
//把buffer中内容写出到fcout
fcout.write(buffer);
}
fcout.close();
fcin.close();
fout.close();
fin.close();
flip()方法让缓冲区可以将新读入的数据写入另一个通道。
After a sequence of channel read or put operations, in.read(buffer), buffer.put(data) invoke this method to prepare for a sequence of channel write or relative get operations.
即:经过channel的read(buffer)(相当于向channel把读入的内容写入到buffer中)或buffer.put(data)之后,当准备调用Channel的write方法,即把buffer中的数据取出来向channel中写,或者buffer.get()方法之前调用flip()方法。
clear()方法重设缓冲区,使它可以接受读入的数据。
Invoke this method before using a sequence of channel-read or put operations to fill this buffer.
即:经过channel的write(buffer)方法,或者buffer.get()之后,在调用channel的read(buffer)或者buffer.put(data)之前调用该方法
填充
我们将代表“hello”的字符串的ASCII码载入一个ByteBuffer中,执行如下:
buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');
注意:每个字符必须强制转换为byte,而不能buffer.put(‘H’);因为这样就会存进字符。
因为buffer是ByteBuffer类型,只能存储字节而不是字符,字符内部是以Unicode编码的,每个字符占2个字节。强制转换会中取后八位,只适合字母类型。如下:
ByteBuffer buffer = ByteBuffer.allocate(8);buffer.put((byte)'你');
buffer.flip();
System.out.println(buffer.getChar());
会抛出java.nio.BufferUnderflowException,因为第二个字节实际上没有存储任何东西,如果改为get,则会返回96,是你的后8位的ASCII。
当然ByteBuffer也可以有putChar,putDouble等方法,但是要注意,他们所占用的字节数。比如分配了一个长度为8的ByteBuffer,最多只能put8个字节。超出的话会抛出异常java.nio.BufferOverflowException。
注意:只有ByteBuffer有类似putInteger、putChar等方法,而CharBuffer,IntBuffer等都没有。