【NIO】java的NIO包中与文件操作相关常用类的详细介绍


前言:原文十分详细,本文内容经过自身理解的加工与整理,包括自己在源码的求证,仅作为学习笔记记录。

部分引用:

【Java NIO】一文了解NIO - puyangsky - 博客园 (cnblogs.com)

1. 了解NIO

原版IO是BIO,阻塞io,而jdk1.4后的是NIO,non-blocking IO,非阻塞io。应用《Java NIO》中的一段话来解释一下NIO出现的原因:

操作系统与java基于流的IO模型不匹配。操作系统要移动的是大块数据(缓冲区),这往往是在硬件直接存储器存储DMA的协助下完成,而JVM的原生IO喜欢操作小数据库(面向流,单个字节,几行文本等)。结果就是操作系统送来整个缓冲区的数据,java.io的流数据再花了大量时间把它们拆成了小数据块,往往拷贝一个小数据块就要往返几层对象。操作系统喜欢整卡车似的去运来数据,而Java.io类喜欢一铲子一铲子去加工数据。有了NIO后,就可以轻松的把一卡车的数据备份到可以直接使用的ByteBuffer对象。Java的RandomAccesssFile类是比较接近操作系统的方式。

因此java原生的IO模型之所以慢,是因为与操作系统的操作方式不匹配造成,那么NIO比BIO快最主要就是用到了缓冲区的技术了。

总结就是IO是面向流的,NIO是面向缓冲区的。IO面向流是指每次只能从流中读取一个或者多个字节,直到读完,也就是对应操作小数据块,被读取到的内容没有被缓存起来。而NIO能直接提前缓存大数据块,再进行进程内部的缓冲区的流读取,效率更快。

2. 了解缓冲区

如图描述的是操作系统是如何把磁盘空间与进程缓冲区建立通道的:

  • 进程使用read()向操作系统内核调用缓冲区
  • 内核向磁盘控制器发送指令要求调出目标文件所在的数据块
  • 磁盘再调出数据块后返回给内核空间,内核对数据块进行拆分提取出有效内容
  • 内核通过与进程建立的通道进行通讯,进程可以对操作系统内核的缓冲区的大数据块进行读写

不那么恰当的理解:内核缓冲区可以理解为cpu缓存,磁盘就是硬盘这样。

在这里插入图片描述

扫描二维码关注公众号,回复: 15644771 查看本文章

而因为用户是无法直接操作硬件的,因此需要通过系统调用让操作系统的内核去操作磁盘。另外磁盘这种块存储设备操作的是固定大小的数据块,而用户请求的则是非规则大小的数据,内核空间在这里的作用就是分解、重组的作用。

3. NIO的基本组件

最主要的三个依赖组件为:缓冲区Buffer,通道Channel和选择器Selector

nio流的相关类都放在java.nio包中,大体如下:

  • java.nio 包:Buffer(缓冲区)相关类
  • java.nio.channels包:Channel(管道) 和Selector(选择器)相关类
  • java.nio.charset包:处理字符集的相关类

3.1 缓冲区Buffer

Buffer是一个抽象类,其子类实现类有如下,如名字所说,具体的Buffer就是以具体的为单位的缓冲区。

**其中ByteBuffer使用最多,即以字节为单位的缓冲区。**需要用的时候查文档即可。目前掌握ByteBuffer就够了

在这里插入图片描述

其子类也都是抽象类:如图在这里插入图片描述

3.1.1 创建缓冲区

缓冲区对象会有一系列属性:【即创建缓冲区对象后可以访问和操作的属性】

  • 容量capacity:缓冲区最大的大小
  • 上界limit:缓冲区当前大小
  • 位置position:下一个要读些的位置,由get()和put()更新
  • 标记mark:备忘位置,可以通过mark()方法指定mark = position,通过reset()方法让position = mark
  • 0 <= mark <= position <=limit <=capacity

属性都在Buffer总抽象类中。

在这里插入图片描述

另外从源码我们可以看到ByteBuffer中的方法基本都是静态方法,隐藏了很多ByteBuffer的实例实现类,方便了用户的调用。所以我们在得到byteBuffer的对象后,其属性和方法并不是在ByteBuffer类中,而是在其实现类如HeapByteBuffer类中。

方法一:allocate静态方法,直接分配的方式创建:

// 最常用的创建,创建全新缓冲区 
// [其中allocate是分配的意思,其单位默认是类中的byte] 
int capacity = 1024
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

allocate静态方法源码:

在这里插入图片描述

方法二:wrap静态方法,把已存在的字节数组包装到缓冲区:

// 以字节数组为容器创建缓冲区,也就是说这个字节数组就是我们的缓冲区
// 对缓冲区操作 = 对数组操作;对数组操作 = 对缓冲区操作
// [其中wrap是囊、容器的意思,也就是以某字节数组为缓冲区容器]
byte[] array = new byte[1024];
ByteBuffer byteBuffer = ByteBuffer.wrap(array);
// 当然可以限定容器范围,提供offet

wrap静态方法源码:
在这里插入图片描述
在这里插入图片描述

3.1.2 缓冲区工具方法

3.1.2.1 flip翻转

这是什么东西呢?缓冲区ByteBuffer会不断被二进制填充,填充满后就是进程写好或者从内核读好的一批缓冲数据,也就是进行下一步,把缓冲区内容传递到channel通道,这个时候如果我们直接读取Buffer缓冲区,其实是读取不到东西的,因为这个时候的position指针指向的是Buffer末尾,因为已经满了嘛。如果我们要再放入通道之前再次读取Buffer,就需要把指针返回到头部,也就是需要缓冲区翻转。

方法在Buffer抽象类中,是final方法,它对应的实例可以使用。

int capacity = 1024
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
xxxxx
byteBuffer.flip();
xxxxx

flip翻转的源码:用于认为当前缓冲区已满,这个满不是说到capacity,而是主观认为满了

limit:缓冲区当前大小即当前position位置,position在数组末尾。即认为当前缓冲区已满

position:当前位置要回到头

mark:标记点重置为-1

在这里插入图片描述

3.1.2.2 rewind翻转

与flip效果一致,区别在于使用时机,flip用于确定当前buffer已经满了;而如果rewind是用于当前缓冲区还没满的时候使用。

int capacity = 1024
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
xxxxx
byteBuffer.flip();
xxxxx

rewind翻转的源码:用于当前缓冲区还没满

区别是limit没有被操作,也就是认为当前缓冲区未满

在这里插入图片描述

3.1.2.3 clear清空

清空缓冲区内容

int capacity = 1024
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.clear();

clear清空的源码:并不是实际意义上的删除,而是覆盖

实际是把position指针归0,下次写入的时候会覆盖之前的内容。

在这里插入图片描述

3.1.2.4 remaining()

remaining = limit - position

表示我还剩余多长, 一般用在flip之后,表示我剩余多少没有读,因此flip后position = 0;

remaining()源码:
在这里插入图片描述

3.1.2.5 mark() 与 reset()

// mark = position
byteBuffer.mark()

// positon = mark
byteBuffer.reset()

3.1.2.6 limit() 限制缓冲区使用范围

int newLimit = xxx; // limit要小于capacity
// limit = newLimit; 
// if(position > limit) position = newLimit; 如果position超过了limit,限制其位置
byteBuffer.limit(newLimit); 

3.1.2.7 操作index的其他方法

nextGetIndex()

nextPutIndex()

checkIndex(int i)

等等,不常用就不介绍了

Buffer总结:

最常用的:

  • 通过allocate创建ByteBuffer 读
  • 通过wrap创建ByteBuffer 写
  • flip()翻转,每次处理缓冲区buffer需要从头开始
  • clear(),每次处理完当前的缓冲区buffer,清空一下
  • 其他都是按照需要使用

3.2 通道Channel

进程中Buffer缓冲区为我们装载了数据,但是数据的写入和读取并不能直接进行与内核缓冲区的read()与write()的系统调用,JVM为我们提供了一层对系统调用的封装,Channel可以用最小的开销来访问操作系统本身的IO服务,解耦同时优化开销。

Channel分类:

  • File IO
    • FileChannel
    • SocketChannel
  • Stream IO
    • ServerSocketChannel
    • DatagramChannel

【区别于java.net.socket,socket套接字是用来写服务器通信的,原生的net.socket是阻塞的,而nio的socket是可以实现非阻塞的,可以做比如聊天软件等服务。但java不是我们常用写服务器的语言。做服务器C++更加适合。因此我们只介绍我们要用的FileChannel】

3.2.1 FileChannel

3.2.1.1 获取FileChannel

FileChannel只能通过工厂方法来实例化,即调用RandomAccessFile、FileInputStream和FileOutputStream的getChannel()方法。

【RandomAccessFile支持随机访问文件,程序可以直接跳转到文件的任意地方来读写数据,而另外两个只能从头到尾找到对应位置后读写】

因此RandomAccessFile是读写文件的比较优的解,最重要的场景就是网络请求的多线程下载与断点续传

String mode = "rw"
RandomAccessFile file = new RandomAccessFile("文件路径", mode);
/*其中mode有如下选择:
    "r": 以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。
    "rw": 打开以便读取和写入。
    "rws": 打开以便读取和写入。相对于 "rw","rws" 还要求对“文件的内容”或“元数据”的每个更新都同步写入到基础存储设备。
    "rwd": 打开以便读取和写入,相对于 "rw","rwd" 还要求对“文件的内容”的每个更新都同步写入到基础存储设备。
*/

3.2.1.2 使用FileChannel

FileChannel是能读也能写的双工通道

// FileChannel源码:
// 把通道中数据传到目的缓冲区中,dst是destination的缩写
public abstract int read(ByteBuffer dst) throws IOException;
// 把源缓冲区中的内容写到指定的通道中去
public abstract int write(ByteBuffer src) throws IOException;

读 实际使用流程:

  • 通过目标文件流创建一个FileChannel
  • allcate创建一个buffer
  • 进入循环处理缓冲区,channel.read(buffer)表示 buffer 通过 channel 获取到文件流的内容,会返回成功读到的字节数【如果读完会返回-1】。
  • 每次read后buffer都是满的,position指针在末尾。我们需要对buffer进行flip翻转,position归0从头开始读
  • 每次读 buffer.remaining() = capacity - 0 这么长,因为flip后position = 0;clear后limit = capacity;
  • 处理当前缓冲区有的内容到变量中
  • clear,清空buffer,给下一次内容读取的空间
public static void readFile(String path) throws IOException {
    
    
    FileChannel fc = new RandomAccessFile("文件路径", "r").getChannel();
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    // StringBuilder是用来接收文件文本结果
    StringBuilder sb = new StringBuilder();
    // 每次循环读一波,如果channel连接的文件流没有内容读了,就返回 -1
    // 每次读完,limit会自动到有内容的最后一个位置,而不是每次都 = capacity
    while ((fc.read(buffer)) >= 0) {
    
    
        // 每次读完,buffer是满的,翻转指针,保证从头开始读
        buffer.flip();
        
        // remaining = limit - position 表示剩下多长
        // 一定要注意,这个时候position经过了flip后是等于0的
        // 所以一般这个时候buffer.remaining() = capacity - 0; 就是buffer的整长
        // 也表示剩下多少没读
        // 这样写主要是为了最后一波的读,不一定是满的,最后一次读就是limit - 0 就是有内容的,不去读无内容的
        byte[] bytes = new byte[buffer.remaining()];
        
        // 从buffer中获取内容放到bytes数组中
        buffer.get(bytes);
        // 依据bytes数组转换当前数据
        String string = new String(bytes, "UTF-8");
        // 把当前数据加入到总结果中
        sb.append(string);

        // 清空buffer,给下一次内容读取的空间
        buffer.clear();
    }
    System.out.println(sb.toString());
}

写 实际使用流程

  • 通过目标文件流创建一个FileChannel
  • allcate创建一个buffer
  • 进入循环处理缓冲区,使用指针遍历完要写入的内容的字节数组。
  • 把目标字节数组写满到buffer的数组中,指针跳转
  • buffer翻转,position = 0,让channel写入
  • channel写入buffer
  • clear,清空buffer,给下一次缓冲的空间
public static void writeFile(String path, String string) throws IOException {
    
    
    FileChannel fc = new RandomAccessFile("文件路径", "rw").getChannel();
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    // 指针,表示写入进度
    int current = 0;
    // 要写的内容的字节数组的长度,为目标字节数组
    int len = string.getBytes().length;
    while (current < len) {
    
    
        // 每次写入1024字节到ByteBuffer中
        // 如果剩余内容不足1024,则提前break
        for (int i=0;i<1024;i++) {
    
    
            if (current+i>=len) break;
            buffer.put(string.getBytes()[current+i]);
        }
        //指针一次性跳转1024。如果是最后一次的缓冲,则跳转小于1024
        current += buffer.position();
		
        // buffer翻转,从头开始写
        buffer.flip();
        // 通过channel通道写入
        fc.write(buffer);
        // 清空buffer数组提供下一次缓冲的空间
        buffer.clear();
    }
}

总结:

FileChannel固然用法简单,但是要注意,FileChannel是阻塞的!!!!并且无法切换到非阻塞状态,这和NIO的non-blocking理念有所冲突。但是能够满足大部分小文件和少文件的场景使用。

如果要实现NIO的非阻塞模式,需要使用套接字通道+选择器。

经过了解,从socket开始往后的内容是属于使用nio编写服务器进行socket通信了,而原来的java.net.socket是阻塞的。目前这并不是我们在对文件操作中需要用到的。因此不再展示后面的内容了。而且代码复杂度太高了。

3.3 Selector

选择器其实是一种多路复用机制在Java语言中的应用,在学习Selector之前有必要学习一下I/O多路复用的概念。【多路复用是非阻塞同步的】

多路复用:

linux的select模型,poll模型和epoll模型就是多路复用的经典。一个socket通道会监听多个资源,只要某个资源准备好了read或者write了,那当前通道就提供给对应需要该资源的请求。而不需要一个请求开一个通道。poll模型是通过轮询去监听,而epoll是通过资源响应来实现高效监听。如果资源没准备好,那么请求所在的线程就先做其他事情,没必要挂起阻塞。

IO多路复用就是通过某种机制可以监视多个文件描述符,一旦某个文件可以执行IO操作,能够通知应用程序去进行相应的读写操作。

因此多路复用也可以理解为一个线程去监听多个网络连接,线程定时轮询所有的网络连接,某个准备好了,该线程就会给这个连接提供服务。而对于还没有准备好网络连接所在的请求,先不进行IO服务,先去做其他事情【因此是不阻塞的,不是说做不了IO就挂起】,等该请求对应的网络连接准备好了,再通知应用程序去使用该线程提供的服务进行IO。

因此要使用Selector,必须要使用非阻塞的Channel。【Socket通道】

Selector是要在使用java编写服务器进行非阻塞通信的时候才会使用,我们暂时不需要使用。

4. Paths和Files

nio包中还有Files类和Paths类也是我们在操作文件的时候经常使用的

4.1 Paths

一般就是通过get()方法返回一个Path类型的,代表当前资源的路径,提供给其他一些的nio相关类使用。

Path path = Path.get("xxx/xxx/xx.jpg");

在这里插入图片描述

4.2 Files

!!下面每个使用都附带方法对应的源码!!

4.2.0 获取文件大小

long size = Files.size(Path.get("/xxxx/xxx.jpg"));//得到的结果是B,Byte字节

//提供一个转换单位的方法
String fileSize = "";
double len = Double.valueOf(file.length());
if(len < 1024) {
    
    
    fileSize = "" + String.format("%.2f", len) + "b";
}else if(len >= 1024 && len < 1048576) {
    
    
    fileSize = "" + String.format("%.2f", len/1024) + "kb";
}else if(len >= 1048576 && len < 1073741824) {
    
    
    fileSize = "" + String.format("%.2f", len/1048576) + "mb";
}

在这里插入图片描述

4.2.1 建立一个对文件某资源的input流

InputStream inputStream = Files.newInputStream(Path.get("/xxxx/xxx.jpg"));

在这里插入图片描述

4.2.2 建立一个对某文件资源的output流

OutputStream outputStream = Files.newOutputStream(Path.get("/xxxx/xxx.jpg"));

在这里插入图片描述

4.2.3 建立一个对某文件夹的流

DirectoryStream directoryStream = Files.newDirectory(Path.get("/xxxx"));

在这里插入图片描述

4.2.4 创建一个文件

Files.createFile(Path.get("/xxxx/xxx.jpg"));

在这里插入图片描述

4.2.5 创建一个文件夹

Files.createFile(Path.get("/xxxx"));

在这里插入图片描述

4.2.6 删除文件/文件夹

Files.delete(Path.get("/xxxx/xxx.jpg"));

在这里插入图片描述

4.2.7 复制文件

Files.copy(Path.get("/xxxx/xxx1.jpg"), Path.get("/xxxx/xxx2.jpg"));

在这里插入图片描述

4.2.8 移动文件

Files.move(Path.get("/xxxx/xxx1.jpg"), Path.get("/xxxx/xxx2.jpg"));

在这里插入图片描述

4.2.9 判断是否为文件夹

Files.isDirectory(Path.get("/xxxx/xxx"));

在这里插入图片描述

4.2.10 还有包括下面常用的

//判断两个文件是否相同
public static boolean isSameFile(Path path, Path path2)
//判断该文件是否被隐藏
public static boolean isHidden(Path path)
//获取当前文件最后被修改的时间
public static FileTime getLastModifiedTime(Path path, LinkOption... options)
//设置当前文件最后被修改的时间
public static Path setLastModifiedTime(Path path, FileTime time)
//当前文件是否存在
public static boolean exists(Path path, LinkOption... options)
//当前文件是否可读
public static boolean isReadable(Path path)
//当前文件是否可写
public static boolean isWritable(Path path)
//当前文件是否可执行
public static boolean isExecutable(Path path)
//建立一个缓冲流
public static BufferedReader newBufferedReader(Path path)
public static BufferedWriter newBufferedWriter(Path path)
//读一个文件,需要我们提供一个绑定文件的流,initialSize是缓冲字节数组的初始化大小
private static byte[] read(InputStream source, int initialSize)
//读所有的行,返回的是一个数组列表,可以用来处理数据
public static List<String> readAllLines(Path path)
//把字节数组写入一个文件
public static Path write(Path path, byte[] bytes)
//获取某目录下的所有文件和目录(Path)
public static Stream<Path> list(Path dir)
//求文件的行数
public static Stream<String> lines(Path path)

5. 写NIO工具类

**总结一句话:**NIO相比于原生IO,如果仅用于对文件的操作【只使用FileChannel】而不使用socket通信,那么它还是阻塞的,但是因为在IO线程中引入了Buffer空间与Channel的封装,使得我们在读写文件的时候效率更高【原生IO需要操作5000ms,NIO可能就只需要500ms】。

**最后整理NIO文本读写的工具类。**直接复制使用即可。

public class NioUtil {
    
    
    /**
     * NIO读取文件
     * @throws IOException
     */
    public static String read(String url) throws IOException {
    
    
        RandomAccessFile access = new RandomAccessFile(url, "r");
        FileChannel channel = access.getChannel();
        int allocate = 1024;
        ByteBuffer byteBuffer = ByteBuffer.allocate(allocate);
        // 接收结果的容器
        StringBuilder sb = new StringBuilder();
        while ((channel.read(byteBuffer)) >= 0) {
    
    
            // 每次读完,buffer是满的,翻转指针,保证从头开始读
            byteBuffer.flip();

            // remaining = limit - position 表示剩下多长
            // 一定要注意,这个时候position经过了flip后是等于0的
            // 所以一般这个时候buffer.remaining() = capacity - 0; 就是buffer的整长
            // 也表示剩下多少没读
            // 这样写主要是为了最后一波的读,不一定是满的,最后一次读就是limit - 0 就是有内容的,不去读无内容的
            byte[] bytes = new byte[byteBuffer.remaining()];

            // 从buffer中获取内容放到bytes数组中
            byteBuffer.get(bytes);
            // 依据bytes数组转换当前数据
            String string = new String(bytes, "UTF-8");
            // 把当前数据加入到总结果中
            sb.append(string);

            // 清空buffer,给下一次内容读取的空间
            byteBuffer.clear();
        }

        channel.close();
        if (access != null) {
    
    
            access.close();
        }
        return sb.toString();
    }

    /**
     * NIO写文件, 默认覆盖
     * @param text 要写入的文本
     * @param url 绝对路径
     * @throws IOException
     */
    public static void write(String url, String text) throws IOException{
    
    
        RandomAccessFile access = new RandomAccessFile(url, "w");
        FileChannel fc = access.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 指针,表示写入进度
        int cur = 0;
        // 要写的内容的字节数组的长度,为目标字节数组
        int len = text.getBytes().length;
        while (cur < len) {
    
    
            // 每次写入1024字节到ByteBuffer中
            // 如果剩余内容不足1024,则提前break
            for (int i = 0; i < 1024; i++) {
    
    
                if (cur + i >= len) break;
                buffer.put(text.getBytes()[cur+i]);
            }
            //指针一次性跳转1024。如果是最后一次的缓冲,则跳转小于1024
            cur += buffer.position();
            // buffer翻转,从头开始写
            buffer.flip();
            // 通过channel通道写入
            fc.write(buffer);
            // 清空buffer数组提供下一次缓冲的空间
            buffer.clear();
        }
    }

    /**
     * NIO写文件,可追加
     * @param text 要写入的文本
     * @param url 绝对路径
     * @throws IOException
     */
    public static void write(String url, String text, String mode) throws IOException{
    
    
        RandomAccessFile access = new RandomAccessFile(url, "rw");
        FileChannel fc = access.getChannel();
        if("a".equals(mode)) {
    
    
            //把文件指针指向末尾进行添加
            access.seek(access.length());
        }
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 指针,表示写入进度
        int cur = 0;
        // 要写的内容的字节数组的长度,为目标字节数组
        int len = text.getBytes().length;
        while (cur < len) {
    
    
            // 每次写入1024字节到ByteBuffer中
            // 如果剩余内容不足1024,则提前break
            for (int i = 0; i < 1024; i++) {
    
    
                if (cur + i >= len) break;
                buffer.put(text.getBytes()[cur+i]);
            }
            //指针一次性跳转1024。如果是最后一次的缓冲,则跳转小于1024
            cur += buffer.position();
            // buffer翻转,从头开始写
            buffer.flip();
            // 通过channel通道写入
            fc.write(buffer);
            // 清空buffer数组提供下一次缓冲的空间
            buffer.clear();
        }
    }

    public static void main(String[] args) throws Exception {
    
    
        String url ="C:\\xxx.text";
        write(url, "123", "a");
        System.out.println(read(url));

    }
}

猜你喜欢

转载自blog.csdn.net/NineWaited/article/details/126589314