Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式。很多小伙伴可能和我一样,对于习惯了用IO来操作文件之后,对这个日渐流行的新东西会有不少的疑惑,那么阅读本文吧,和我一起打开NIO的大门,学习NIO操作文件。
下面有一个概要的区别图,图下面会有区别的描述。
IO NIO
面向流 面向缓冲
阻塞IO 非阻塞IO
无 选择器
Java IO中最为核心的一个概念是流(Steam),面向流的编程。流是信息的载体。IO中的一个流要么是输入流,要么是输出流,不可能同时是输入流和输出流。Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
NIO是面向缓冲,数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。
NIO的核心包含三个重要组件:
Channel可以理解为IO的Stream流,每次读取数据,都是从通道中读取,写数据也是写入到通道,直接对接Buffer缓冲区。常见的通道有FileChannel,是文件的通道,用来读取文件,本文着重将这个通道。还有DataChannel,通过UDP读写网络数据,SocketChannel通过TCP读写网络数据,ServerSocketChannel监听新进来的TCP连接,对每一个新进来的TCP连接,都会建立一个SocketChannel。
通道可以实现双向读写,比如说用RandomAccessFile类获取文件读写,调用RandomAccessFile.getChannel()方法,获取的就是读写双向的通道。代码如下两行。
RandomAccessFile randomAccessFile = new RandomAccessFile(path.toFile(),"rw");
FileChannel fileChannel = randomAccessFile.getChannel();
下面这两种情况下获取的通道,只能单向的操作,请看代码。
FileOutputStream fos = new FileOutputStream(path.toFile());
FileChannel fileChannel1 = fos.getChannel();
FileInputStream fis = new FileInputStream(path.toFile());
FileChannel fileChannel2 = fis.getChannel();
这里分别通过字节输出流和字节输入流获取了通道,这两个通道都分别有write()和read()方法,但字节输出流创建的通道调用读取的方法时候就会报错,同理,输入流调用写的方法也会报错,什么原因呢?
原来是打开文件的权限不同导致的。从 FileInputStream 对象的getChannel( )方法获取的 FileChannel 对象是只读的,虽然FileChannel 实现了 ByteChannel 接口,看起来是双向的。但是在这样的通道上调用 write( )方法将抛出未经检查的NonWritableChannelException 异常,因为 FileInputStream 对象总是以 read-only 的权限打开文件。
缓冲区:缓冲区实质上是一个数组。最常用的缓冲区类型是ByteBuffer,对应Java的基本类型都有一种缓冲区区:
缓冲区类型:
类似IO中的BufferedInputStream等,NIO操作文件的核心方式就是Channel+Buffer,下面直接上一段代码,看一下缓冲区如何和通道结合起来使用。
// 1. 获取数据源 和 目标传输地的输入输出流(此处以数据源 = 文件为例)
FileInputStream fin = new FileInputStream(infile);
FileOutputStream fout = new FileOutputStream(outfile);
// 2. 获取数据源的输入输出通道
FileChannel fcin = fin.getChannel();
FileChannel fcout = fout.getChannel();
// 3. 创建 缓冲区 对象:Buffer(共有2种方法)
// 方法1:使用allocate()静态方法
ByteBuffer buff = ByteBuffer.allocate(256);
// 上述方法创建1个容量为256字节的ByteBuffer
// 注:若发现创建的缓冲区容量太小,则重新创建一个大小合适的缓冲区
// 方法2:通过包装一个已有的数组来创建
// 注:通过包装的方法创建的缓冲区保留了被包装数组内保存的数据
ByteBuffer buff = ByteBuffer.wrap(byteArray);
// 额外:若需将1个字符串存入ByteBuffer,则如下
String sendString="你好,服务器. ";
ByteBuffer sendBuff = ByteBuffer.wrap(sendString.getBytes("UTF-16"));
// 4. 从通道读取数据到缓冲区
// 注:若 以读取到该通道数据的末尾,则返回-1
fcin.read(buff);
// 5. 传出数据准备:将缓存区的读模式 转换->> 写模式
buff.flip();
// 6. 从缓冲区中读取数据写入到通道
fcout.write(buff);
// 7. 重置缓冲区
// 目的:重用现在的缓冲区,即 不必为了每次读写都创建新的缓冲区,在再次读取之前要重置缓冲区
// 注:不会改变缓冲区的数据,只是重置缓冲区的主要索引值
buff.clear();
上面方法中,在声明缓冲区的时候,罗列了不同的方式,后面读写操作都是对接的缓冲区。那么中间的flip()和clear()到底是干什么的呢?下面讲解一下Buffer缓冲区的内部原理方便大家理解。
Buffer缓冲区分为三个重要的变量:
在从通道读取时,您将所读取的数据放到底层的数组中。 position
变量跟踪已经写了多少数据。更准确地说,它指定了下一个字节将放到数组的哪一个元素中。因此,如果您从通道中读三个字节到缓冲区中,那么缓冲区的 position
将会设置为3,指向数组中第四个元素。
同样,在写入通道时,您是从缓冲区中获取数据。 position
值跟踪从缓冲区中获取了多少数据。更准确地说,它指定下一个字节来自数组的哪一个元素。因此如果从缓冲区写了5个字节到通道中,那么缓冲区的 position
将被设置为5,指向数组的第六个元素。
limit
变量表明还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。
position
总是小于或者等于 limit
。
缓冲区的 capacity
表明可以储存在缓冲区中的最大数据容量。实际上,它指定了底层数组的大小 ― 或者至少是指定了准许我们使用的底层数组的容量。limit
决不能大于 capacity
。
看了上面对三个变量的描述,在看一下buffer缓冲区操作的三个方法的代码:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
flip():此方法将position的值赋给了limit,将position置为0,常用在读取通道中的数据到Buffer之后,要进行写操作之前调用,此时,缓冲区的数据的长度在position的位置,将position的值赋给limit,position置为0,可以一个字节不多一个字节不少的进行写的操作,保证数据的可靠性。
public final Buffer clear() {
position = 0; //设置为0
limit = capacity; //极限和容量相同
mark = -1; //取消标记
return this;
}
clear():此方法相当于给Buffer复位,但不会清除Buffer中的数据,调用这个方法使缓冲区为为新的通道读取或写入做准备。如果是写入,新的写入会覆盖旧的同一个位置的内容。
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
rewind() :使缓冲区为重新读取已包含的数据做好准备:它使限制保持不变,将位置设置为 0。和clear()类似,只是不改动限制。
以上三个方法都不对buffer内的数据作修改。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
*
*@desc 复制文件
*/
public class Test {
public static void main(String[] args) throws IOException {
// 定义源文件 & 目标文件
String infile = "C:\\copy.sql";
String outfile = "C:\\copy.txt";
// 1. 获取数据源 和 目标传输地的输入输出流
FileInputStream fin = new FileInputStream(infile);
FileOutputStream fout = new FileOutputStream(outfile);
// 2. 获取数据源的输入输出通道
FileChannel fcin = fin.getChannel();
FileChannel fcout = fout.getChannel();
// 3. 创建缓冲区对象,容量初始化为1024
ByteBuffer buff = ByteBuffer.allocate(1024);
while (true) {
// 4. 从通道读取数据 & 写入到缓冲区
int r = fcin.read(buff);
// 返回-1代表已读取到该通道数据的末尾,循环可以结束
if (r == -1) {
break;
}
// 5. 传出数据准备:调用flip()方法
buff.flip();
// 6. 从 Buffer 中读取数据 & 传出数据到目标的输入通道
fcout.write(buff);
// 7. 复位缓冲区
buff.clear();
}
}
}
以上是对NIO操作文件的一些原理和实战的讲解,对NIO感兴趣的可以看看,如有疑问,下方留言我会随时回复哦。