携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
NIO 介绍
由于传统的 IO 流在实际运行时效率较低,所以 JDK 4 版本中提出了 NIO 技术,带来了可观的性能提升。
NIO :Non-Blocking IO,指的是非阻塞的 IO 。
传统的流式 IO ,又可以称为 BIO (Blocking IO) 阻塞 IO。
如何理解阻塞与非阻塞呢?
比如私家车和出租车,私家车买回来后只能是车主一家可以使用,其他不相干的人是不能使用这辆车的,这样私家车处于阻塞状态,车门锁上其他人是不能进入的;而出租车整日都是在路上穿行,有客人它就载客,到达目的客人下车,恢复空车状态后继续揽客,这样出租车便处于非阻塞状态,有客人时便可打开车门进行乘坐。显然阻塞状态存在资源极大浪费,而非阻塞状态可以充分发挥物尽其用的原则。
所以 NIO 的提出在文件的传输过程中极大的提高了传输的性能。
为什么提出
我们学习了 IO 流,应该知道分为输入流和输出流,输入与输出都有相对应的类进行处理,如,InputStream 类只能读文件,OutputStream 类只能写文件,两者是不能互换的,它们各司其职。
如果现在打开一个文件,想要一会写一会读,操作起来非常麻烦,读的时候需要 InputStream 的全套操作,写的时候需要 OutputStream 的全套操作,这样就忙坏了输入和输出流了。
因此,我们需要引入 NIO 提供的 文件通道,文件通道中的数据可以双向流动,也就是说流进来便是读操作,流出去便是写操作,这样文件的读写操作可以在文件通道中进行,大大节省了系统资源的开销。
文件通道 FileChannel
Java 中提供的文件通道 FileChannel 类,存放在 java.nio.channels 包中。
🤔 那么如何获取 FileChannel 的实例对象呢?
有两种方式可以获取 FileChannel 实例。
👉 方式一:通过文件字节输入输出流中的 getChannel() 方法来获取。
FileChannel inputchannel = new FileInputStream(文件名).getChannel();
FileChannel outputchannel = new FileOutputStream(文件名).getChannel();
👉 方式二:通过随机访问文件工具 RandomAccessFile 类中的 getChannel() 方法来获取。
// 获取可读的文件通道
FileChannel inputChannel = new RandomAccessFile(文件名, "r").getChannel();
// 获取可写的文件通道
FileChannel outputChannel = new RandomAccessFile(文件名, "rw").getChannel();
文件通道实例获取后,便可以通过类中的方法进行数据交互了。
FileChannel 类常用的方法,如下表:
| 方法名 | 说明 |
|---|---|
| isOpen | 判断文件通道是否打开。 |
| size | 获取文件通道中的文件大小。 |
| truncate | 把文件大小截取到指定长度。 |
| read | 把文件通道中的数据读到字节缓存。 |
| write | 往文件通道写入字节缓存中的数据。 |
| force | 强制写入,类似于缓冲流中的 flush 方法。 |
| close | 关闭文件通道。 |
文件通道中的读写操作,主要是通过 read() 和 write() 方法进行处理,而文件通道中的读写操作需要有存储空间,这个存储空间就是字节缓存。
字节缓存 ByteBuffer
文件通道的读写操作,在通道内需要有内部的存储空间进行存储操作,它是缓存 Buffer 类提供的。
Buffer 类存放在 Java API 的 java.nio 包中。
Buffer 类也是一个抽象类,主要缓存空间的使用,是采用它的子类来进行操作。
Buffer 缓存提供了:字节缓存、字符缓存、双精度缓存、单精度缓存、整型缓存、长整型缓存、短整型缓存,这些子类同样也都是抽象类。
👉 Buffer 类中提供常用方法有 3 个,如下:
- clear() :清空缓冲区的数据。
- flip() :把缓冲区从写模式切换到读模式。读取数据前,需要先调用 flip 方法。
- rewind() :让缓冲区的指针回到开头,重新在读一遍。
ByteBuffer 字节缓存,它是一种特殊的存储空间,在操作中可以被多次读写。
👉 ByteBuffer 提供了几个构建方法:
| 静态方法 | 说明 |
|---|---|
| ByteBuffer.wrap(byte[] array) | 根据输入的字节数组生成对应的缓存对象。 |
| ByteBuffer.wrap(byte[] array, int offset, int length) | 根据输入的字节数组开始和长度生成对应的缓存对象。 |
| ByteBuffer.allocate(int capacity) | 根据输入的容量分配指定大小的新缓存。它将有一个后备数组,其数组偏移量将为零。 |
| ByteBuffer.allocateDirect(int capacity) | 根据输入的容量分配指定大小的新缓存。 |
获取 ByteBuffer 对象实例后,也可以通过 get() 和 put() 方法存储数据。
实验
通过文件通道写入数据
第一步:创建文件输出流对象 FileOutputStream fos = new FileOutputStream(filename);
第二步:通过文件输出流对象获取文件通道对象 FileChannel channel = fos.getChannel();
第三步:确定写入的数据,String str = "xxxx";
第四步:生成字符串对应的字节缓存对象 ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
第五步:往文件通道写入字节缓存 channel.write(buffer);
最后:资源释放 fos.close(); channel.close();
同理可以知道通过文件通道读取文件
第一步:创建文件输入流对象 FileInputStream fis = new FileInputStream(filename);
第二步:通过文件输入流对象获取文件通道对象 FileChannel channel = fis.getChannel();
第三步:获取文件通道中文件大小 int size = (int) channel.size();
第四步:根据文件大小创建新的字节缓存 ByteBuffer buffer = ByteBuffer.allocateDirect(size);
第五步:把文件通道中的数据读到字节缓存 channel.read(buffer);
第六步:把缓冲区切换到读模式,必须调用 flip 方法 buffer.flip();
第七步:创建与文件大小相同长度的字节数组 byte[] bytes = new byte[size];
第八步:把字节缓存中的数据读取到字节数组 buffer.get(bytes);
第九步:把字节数组转换成字符串 String content = new String(bytes);
第十步:打印输出字符串信息 System.out.println(content);
最后:资源释放 fis.close(); channel.close();
总体代码:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class TestChannel{
public void writeChannel(String filename) throws Exception{
FileOutputStream fos=new FileOutputStream(filename);
FileChannel channel=fos.getChannel();
String str = "明月几时有?把酒问青天。不知天上宫阙,今夕是何年。\n我欲乘风归去,又恐琼楼玉宇,高处不胜寒。起舞弄清影,何似在人间。\n转朱阁,低绮户,照无眠。不应有恨,何事长向别时圆?\n人有悲欢离合,月有阴晴圆缺,此事古难全。但愿人长久,千里共婵娟。";
ByteBuffer buffer=ByteBuffer.wrap(str.getBytes());
channel.write(buffer);
fos.close();
channel.close();
}
public void readChannel(String filename) throws Exception{
FileInputStream fis=new FileInputStream(filename);
FileChannel channel=fis.getChannel();
int size=(int)channel.size();
ByteBuffer buffer=ByteBuffer.allocateDirect(size);
channel.read(buffer);
buffer.flip();
byte[] bytes=new byte[size];
buffer.get(bytes);
String content=new String(bytes);
System.out.println(content);
fis.close();
channel.close();
}
public static void main(String[] args) {
TestChannel tc = new TestChannel();
try {
tc.writeChannel("myfile.txt");
tc.readChannel("myfile.txt");
} catch (Exception e) {
e.printStackTrace();
}
}
}
文件通道的性能优势
文件通道的性能优势主要体现在文件复制,而且越大的文件越能体现它的优势。
一般来说,文件传输最好采用字节流,使用字节流可以传输任何类型文件,不会被破坏任何格式,但是以字节为单位进行传输效率非常的低。
在这样的情况下,至少你应该想到采用缓冲流 BufferedInputStream 和 BufferedOutputStream 进行文件复制。
而今,大数据信息时代,高效操作时王道。所以采用文件通道进行文件复制,可以让效率完全体现。
⭐ 注意:当传输文件不大时,也无需使用文件通道,就好比杀鸡用牛刀,也是一种资源浪费。
传统 IO 流和文件通道复制文件的区别。
传统 IO 复制文件流程图如下:
分为 4 步完成复制:
① 磁盘文件 --> 系统内存
② 系统内存 --> 应用内存
③ 应用内存 --> 系统内存
④ 系统内存 --> 磁盘文件
操作系统收到读文件指令后,把磁盘文件数据读到系统内存中,然后由应用程序把系统内存中的数据读到应用内存;写文件指令收到后,应用程序先把应用内存中的数据写到系统内存,再由操作系统把系统内存中的数据写入磁盘文件。
文件通道复制文件流程图如下:
只需 2 步完成复制:
① 磁盘文件 --> 字节缓存
② 字节缓存 --> 磁盘文件
字节缓存是通道内部的存储空间,因此使用文件通道复制文件,无须动用系统内存,也无须使用应用内存,只需将磁盘上的文件内容读到通道中的字节缓存中,再将字节缓存中的数据写入磁盘上的新文件即可。
显然,文件通道复制文件性能优于传统 IO 。
文件复制也可以采用文件通道中的 transferTo 和 transferFrom 方法进行复制操作。