NIO(New IO)顾名思义,即使高级的输入/输出处理API。
NIO概述
InputStream和OutputStream的输入/输出,基本上是以字节为单位进行处理的,而且是整块数据读入后又整块数据写出,比如:
public void dump(InputStream src, OutputStream dest) throws IOException{
try(InputStream input = src; OutputStream output = dest){
byte[] data = new byte[1024];
int length;
while((length = input.read(data)) != -1){
output.write(data, 0, length);
}
}
}
相对于用InputStream和OutputStream来衔接数据源与目的地,NIO使用频道(channel)来衔接数据的节点,在处理数据的时候,NIO可以让我们设定缓冲区的容量,在缓冲区中对感兴趣的区块进行标记,像是标记读取位置,数据有效位置,对于这些区块标记,提供了clear(), rewind(), flip(), compact()等高级操作。我们使用NIO来将上面的代码改写一下:
public static void dump(ReadableByteChannel src, WritableByteChannel dest)
throws IOException{
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
try (ReadableByteChannel readableByteChannel = src;
WritableByteChannel writableByteChannel = dest){
while(readableByteChannel.read(byteBuffer) != -1){
//标记byteBuffer读入数据所在的区域
byteBuffer.flip();
writableByteChannel.write(byteBuffer);
//清除byteBuffer中的标记
byteBuffer.clear();
}
}
}
对于上面的代码,我们在了解相关API之后就会明白。
Channel架构与操作
Channel是AutoCloseable的子接口,我们先来看一下Channel的继承架构:
ByteChannel中没有定义任何方法,单纯的继承了ReadableByteChannel与WritableByteChannel的行为,ByteChannel的子接口SeekableByteChannel可以读取与改变下一个要存取数据的位置。
Channel的操作类都是抽象类,不能直接实例化,想要取得Channel的操作对象,可以使用Channels类,前面定义了静态方法newChannel(),可以让你从InputStream和OutputStream分别建立ReadableByteChannel与WritableByteChannel,有些InputStream和OutputStream实例本身也有方法可以取得Channel实例,举例来说,FileInputStream,FileOutputStream都有个getChannel方法可以分别取得FileChannel实例。
如果你已经有了相关的Channel实例,也可以通过Channels上其他的new×××()静态方法,得到InputStream,OutputStream,Reader,Writer实例。
Buffer架构与操作
在NIO设计中,数据都是在java.nio.Buffer中处理,Buffer是个抽象类,定义了clear(), flip(), reset(), rewind()等对数据区块的高级操作。
容量,界限与存取位置
根据不同的数据类型处理需求,你可以选择不同的buffer子类,他们都是抽象类,不能直接实例化,然而Buffer的直接子类们都有一个allocate()静态方法,可以让我们指定Buffer的容量,如果是ByteBuffer,容量是指内部操作时使用的byte[]长度,如果是Charbuffer,则是char[]长度,FloatBuffer则是float[]长度,以此类推。Buffer容量的大小可以使用capacity方法取得,如果想取的Buffer内部的数组,可以使用array方法,如果我们有一个数组想要转为某个Buffer子类实例,每个Buffer子类实例都有wrap静态方法可以提供这项服务。
对于ByteBuffer还有一个allocateDirect方法,相对于allocate方法配置的内存是由JVM管理,它会利用操作系统的原生I/O操作,具体差异大家有兴趣可以去学习,想要知道Buffer是否为直接配置,可以通过isDirect方法得知。
Buffer是个容器,你填装的数据不会超过它的容量,实际可读取或写入的数据界限索引值可以由limit()方法得知或设定,举例来说,容量为1024字节的ByteBuffer,ReadableByteChannel对其写入了512字节,那么limit方法应该设定512,至于下一个可读取数据的位置索引值,可以使用position方法得知或设定。
clear(), flip()与rewind()
Buffer的操作可以先从clear(), flip()与rewind()开始认识,当一个缓冲区刚被配置好或调用clear方法之后,limit()等于capacity(), position()会是0,如果ReadableByteChannel对ByteBuffer写入了16字节,那么position()就是16.
现在如果要对ByteBuffer中已写入的16字节进行读取,position必须为0,limit必须为16,所以我们可以使用buffer.position(0), buffer.limit(16)来完成这项任务,但是我们直接调用flip方法,它可以将limit的值设为position的目前值,而position的值设为0.
现在我相信大家对于之前的程序已经完全明白了。
顺便给大家贴上完整的范例:
public class NIOUtil {
public static void dump(ReadableByteChannel src, WritableByteChannel dest)
throws IOException{
try (ReadableByteChannel readableByteChannel = src;
WritableByteChannel writableByteChannel = dest){
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while(readableByteChannel.read(byteBuffer) != -1){
byteBuffer.flip();
writableByteChannel.write(byteBuffer);
byteBuffer.clear();
}
}
}
public static void main(String[] args) throws Exception{
URL url = new URL("https://www.baidu.com");
//得到输入源,使用Channels的静态方法创建输入流
ReadableByteChannel src = Channels.newChannel(url.openStream());
//得到输出源,使用Channels的静态方法创建输出流
WritableByteChannel dest = new FileOutputStream("index.html").getChannel();
//FileChannel dest = new FileOutputStream("index.html").getChannel();
NIOUtil.dump(src, dest);
}
}
调用rewind(),会将position设置为0,但limit不变,这通常用在想要重复读取Buffer中某段数据时使用,作用相当于单独调用Buffer的position(0)方法。
mark(), reset(), remaining()
Buffer上还有个mark方法,可以在目前position上标记,在存取Buffer之后,若调用了reset方法,会将那个position设回被mark标记的位置。position与limit之间为剩余可存取的资料,可以使用remaining得知还有多少长度,使用hasRemaining可以测试是否还有剩余可存取的数据。