Java中,最常用的IO方式是基于流的,比如FileInputStream。流式IO被视为单个字节的流动,而NIO(New IO)则是基于块的,每次读写的不是一个字节而是一个缓冲区。因此,通常NIO的读写性能更高,但没有流式IO使用简单。流式IO位于java.io.*
包中,NIO位于java.nio.*
中。
注意:实际上java.io.*
下一些带缓冲区的IO实现也已经是基于NIO的实现的,这样IO性能更好,但是NIO还具有一些java.io.*
中没有的好处,你可以把NIO看成一种更底层,功能更强大的API。
Channel相当于流式IO中的流,读写文件到内存缓冲区(Buffer),要经过Channel。
缓冲区就是内存中的一块区域,IO过程中数据会在这块区域停留,最常用的的缓冲区是ByteBuffer(字节缓冲区),实际上对于Java每种基本类型都有对应的缓冲区,例如IntBuffer。
复制文件
public static void main(String[] args) throws Exception
{
File srcFile = new File("src/1.png");
File targetFile = new File("src/2.png");
FileInputStream fileInputStream = new FileInputStream(srcFile);
FileOutputStream fileOutputStream = new FileOutputStream(targetFile);
//获得Channel
FileChannel channelIn = fileInputStream.getChannel();
FileChannel channelOut = fileOutputStream.getChannel();
//分配缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while(true)
{
//清空缓冲区
byteBuffer.clear();
int n = channelIn.read(byteBuffer);
if(n == -1)
{
channelIn.close();
channelOut.close();
break;
}
//将缓冲区从读取模式转换为输出模式
byteBuffer.flip();
channelOut.write(byteBuffer);
}
}
上述代码中,首先从流对象中获得Channel对象,然后分配缓冲区,从channelIn读入缓冲区,从channelOut写出,最终完成文件的复制。
将数组中内容写入文件
public static void main(String[] args) throws Exception
{
File file = new File("src/test.txt");
FileOutputStream fileOutputStream = new FileOutputStream(file);
FileChannel fileChannel = fileOutputStream.getChannel();
String str = "hello, world!";
byte[] bytes = str.getBytes();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(bytes);
byteBuffer.flip();
fileChannel.write(byteBuffer);
fileChannel.close();
}
上述代码中,使用put()将字节数组的内容存入缓冲区,并写入文件。
上面两个例子中,有几个需要解释的地方:
我们需要了解Buffer的内部实现。
Buffer有三个状态变量:
这些状态变量都有对应的方法可以获取,如buffer.limit()
。position和limit还能手动指定。
read()和put()类似,一个是从通道中写入缓冲区,一个是从字节数组中写入缓冲区。
例如调用put()时,buffer是初始的读状态,因此limit和capacity都表示缓冲区容量,随着读操作执行,position向前移动。
当调用flip(),buffer从读取模式转换为输出模式,limit值变为position,表示已写入数据长度,然后position归为0,随着后续写入操作执行,position重新向前移动。
上述代码中,还使用了clear()操作,它将缓冲区恢复为最初始的状态,即调用flip()和读取数据之前。
实际上,缓冲区有一组get()/put()方法,用来读写缓冲区。这些方法中,要注意的是有的方法会改变缓冲区的状态变量,有的则不会。
例如:put(int index, byte b)
是修改缓冲区指定位置的字节,不会改变缓冲区的状态变量,而put(byte b)
则是向缓冲区写入一个字节,这个操作会改变缓冲区的状态变量,这实际上很好理解,不需要解释。具体的用法,还请参考JDK文档。
上面代码中的Bytebuffer.allocate(1024)
表示分配一个1024B的缓冲区。实际上,缓冲区还可以从byte[]数组包装而来,使用wrap函数进行包装:Bytebuffer.wrap(byteArray)
。
我们可以在一个大缓冲区上创建一个子缓冲区窗口,他们共享底层的内存。分配一个子缓冲区通常用于较复杂的缓冲区操作,它比多次指定position更加简洁。
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.position(3);
buffer.limit(7);
ByteBuffer slice = buffer.slice();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
ByteBuffer readOnlyBuffer = byteBuffer.asReadOnlyBuffer();
使用buffer.asReadOnlyBuffer()
可以获得一个只读缓冲区,尝试向只读缓冲区写入会引发运行时异常。
填充C语言结构体时,有一种方便的写法:直接将结将构体应用于某一段内存缓冲区,便于格式化读取这段缓冲区的数据。例如接收若干字节的网络协议数据包,然后直接将其应用于该协议结构体,直接读取该结构体字段即可。Java也可以实现这种功能,但是用的不是结构体,而是分散/聚集IO。
long read(ByteBuffer[] dsts);
long read(ByteBuffer[] dsts, int offset, int length);
long write(ByteBuffer[] srcs);
long write(ByteBuffer[] srcs, int offset, int length);
实际使用时,将各个缓冲区的引用保存在一个类属性中,通常更加方便。
上面介绍的代码中都是使用ByteBuffer.allocate(int opacity)
分配的缓冲区,实际上可以使用ByteBuffer.allocateDirect(int opacity)
分配一个“直接缓冲区”。
Java抽象了底层实现,“非直接缓冲区”实际上是在JVM堆中分配一个数组,IO时数据从硬件读入内核缓冲区,内核缓冲区中可能拷贝多次才到达我们分配的缓冲区的底层数组,而“直接缓冲区”则是JVM根据具体平台,尽力减少了这种拷贝过程,因此性能更高。但是脱离了JVM,就一定要记得,用完后将缓冲区的数据回收。
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
((DirectBuffer)byteBuffer).cleaner().clean();
内存映射文件是操作系统提供的,它可以将文件映射到一个内存地址,像读写内存一样读写文件。在LinuxC编程相关章节中也有介绍。Java中我们也可以利用这种高级的IO功能实现高性能的文件IO。
MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, 0, 1024 );
上述代码中,将文件的前1024B通过FileChannel对象映射到了缓冲区。
NIO提供了Charset类进行文本的编码处理,使用起来十分简单。
例子
public static void main(String[] args) throws Exception
{
File file = new File("src/test.txt");
FileChannel fileChannel = (new FileInputStream(file)).getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
fileChannel.read(byteBuffer);
fileChannel.close();
Charset charset = Charset.forName("utf-8");
CharsetDecoder decoder = charset.newDecoder();
byteBuffer.flip();
CharBuffer charBuffer = decoder.decode(byteBuffer);
System.out.println(charBuffer.toString());
}
Charset类将ByteBuffer转换为对应编码的CharBuffer。注意:ByteBuffer读取前一定要flip(),而生成的CharBuffer已经处于输出状态。其position已经归零,limit则为数据长度。