2023-09-23
原文作者:李林超 原文地址: https://www.lilinchao.com/archives/2091.html

[TOC]

一、概念

FileChannel是一个连接到文件的 通道 ,使用FileChannel可以从文件读数据,也可以向文件中写入数据。Java NIO的FileChannel是标准 Java IO 读写文件的替代方案。

FileChannel的主要作用是读取、写入、映射、操作文件。

FileChannel 只能工作在阻塞模式下。

FileChannel 和 标准Java IO对比

FileInputStream/FileOutputStream FileChannel
单向 双向
面向字节的读写 面向Buffer读写
不支持 支持内存文件映射
不支持 支持转入或转出其他通道
不支持 支持文件锁
不支持操作文件元信息 不支持操作文件元信息

FileChannel的优点

  • 在文件中的特定位置读取和写入
  • 将文件的一部分直接加载到内存中,这样可以更高效
  • 可以以更快的速度将文件数据从一个通道传输到另一个通道
  • 可以锁定文件的一部分以限制其他线程的访问
  • 为避免数据丢失,可以强制将文件更新立即写入存储

二、FileChannel 类结构

    package java.nio.channels;
     
    publicabstractclass FileChannel
        extends AbstractInterruptibleChannel
        implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel{
        /**
         * 初始化一个无参构造器.
         */
        protected FileChannel() { }
     
        //打开或创建一个文件,返回一个文件通道来访问文件
        public static FileChannel open(Path path,
                                       Set<? extends OpenOption> options,
                                       FileAttribute<?>... attrs)
            throws IOException
        {
            FileSystemProvider provider = path.getFileSystem().provider();
            return provider.newFileChannel(path, options, attrs);
        }
     
        privatestaticfinal FileAttribute<?>[] NO_ATTRIBUTES = new FileAttribute[0];
     
        //打开或创建一个文件,返回一个文件通道来访问文件
        public static FileChannel open(Path path, OpenOption... options)
            throws IOException
        {
            Set<OpenOption> set = new HashSet<OpenOption>(options.length);
            Collections.addAll(set, options);
            return open(path, set, NO_ATTRIBUTES);
        }
     
      //从这个通道读入一个字节序列到给定的缓冲区
        public abstract int read(ByteBuffer dst) throws IOException;
     
        //从这个通道读入指定开始位置和长度的字节序列到给定的缓冲区
        public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException;
     
        /**
         * 从这个通道读入一个字节序列到给定的缓冲区
         */
        public final long read(ByteBuffer[] dsts) throws IOException {
            return read(dsts, 0, dsts.length);
        }
     
        /**
         * 从给定的缓冲区写入字节序列到这个通道
         */
        public abstract int write(ByteBuffer src) throws IOException;
     
        /**
         * 从给定缓冲区的子序列向该信道写入字节序列
         */
        public abstract long write(ByteBuffer[] srcs, int offset, int length)
            throws IOException;
     
        /**
         * 从给定的缓冲区写入字节序列到这个通道
         */
        public final long write(ByteBuffer[] srcs) throws IOException {
            return write(srcs, 0, srcs.length);
        }
     
        /**
         * 返回通道读写缓冲区中的开始位置
         */
        public abstract long position() throws IOException;
     
        /**
         * 设置通道读写缓冲区中的开始位置
         */
        public abstract FileChannel position(long newPosition) throws IOException;
     
        /**
         * 返回此通道文件的当前大小
         */
        public abstract long size() throws IOException;
     
        /**
         * 通过指定的参数size来截取通道的大小
         */
        public abstract FileChannel truncate(long size) throws IOException;
     
        /**
         * 强制将通道中的更新文件写入到存储设备(磁盘等)中
         */
        public abstract void force(boolean metaData) throws IOException;
     
        /**
         * 将当前通道中的文件写入到可写字节通道中
       * position就是开始写的位置,long就是写的长度
         */
        public abstract long transferTo(long position, long count,WritableByteChannel target)throws IOException;
     
        /**
         * 将当前通道中的文件写入可读字节通道中
       * position就是开始写的位置,long就是写的长度
         */
        public abstract long transferFrom(ReadableByteChannel src,long position, long count) throws IOException;
     
        /**
         * 从通道中读取一系列字节到给定的缓冲区中
       * 从指定的读取开始位置position处读取
         */
        public abstract int read(ByteBuffer dst, long position) throws IOException;
     
        /**
         * 从给定的缓冲区写入字节序列到这个通道
         * 从指定的读取开始位置position处开始写
         */
        public abstract int write(ByteBuffer src, long position) throws IOException;
     
     
        // -- Memory-mapped buffers --
     
        /**
         * 一个文件映射模式类型安全枚举
         */
        publicstaticclass MapMode {
     
            //只读映射模型
            publicstaticfinal MapMode READ_ONLY
                = new MapMode("READ_ONLY");
     
            //读写映射模型
            publicstaticfinal MapMode READ_WRITE
                = new MapMode("READ_WRITE");
     
            /**
             * 私有模式(复制在写)映射
             */
            publicstaticfinal MapMode PRIVATE
                = new MapMode("PRIVATE");
     
            privatefinal String name;
     
            private MapMode(String name) {
                this.name = name;
            }
        }
     
        /**
         * 将该通道文件的一个区域直接映射到内存中
         */
        public abstract MappedByteBuffer map(MapMode mode,long position, long size) throws IOException;
     
        /**
         * 获取当前通道文件的给定区域上的锁
       * 区域就是从position处开始,size长度 
       * shared为true代表获取共享锁,false代表获取独占锁
         */
        public abstract FileLock lock(long position, long size, boolean shared) throws IOException;
     
        /**
         * 获取当前通道文件上的独占锁
         */
        public final FileLock lock() throws IOException {
            return lock(0L, Long.MAX_VALUE, false);
        }
     
        /**
         * 尝试获取给定的通道文件区域上的锁
         * 区域就是从position处开始,size长度 
       * shared为true代表获取共享锁,false代表获取独占锁
         */
        public abstract FileLock tryLock(long position, long size, boolean shared)
            throws IOException;
     
        /**
         * 尝试获取当前通道文件上的独占锁
         */
        public final FileLock tryLock() throws IOException {
            return tryLock(0L, Long.MAX_VALUE, false);
        }
     
    }

三、FileChannel常用方法介绍

3.1 通道获取

FileChannel 可以通过 FileInputStream, FileOutputStream, RandomAccessFile 的对象中的 getChannel() 方法来获取,也可以通过静态方法 FileChannel.open(Path, OpenOption ...) 来打开。

  • FileChannel.open()的方式

通过静态静态方法 FileChannel.open() 打开的通道可以指定打开模式,模式通过 StandardOpenOption 枚举类型指定。

示例

    FileChannel channell = FileChannel.open(
        Paths.get("data","test","c.txt"),	// 路径:data/test/c.txt
        StandardOpenOption.CREATE,
        StandardOpenOption.WRITE
    );
    
    FileChannel channel2 = FileChannel.open(
        new File("a.txt").toPath(),
        StandardOpenOption.CREATE_NEW,
        StandardOpenOption.WRITE,StandardOpenOption.READ
    );

path获取

  • Paths.get(String first, String... more) :将传入的参数根据顺序进行统一拼接成为一个完整文件路径;
  • new File(String pathname).toPath() :传入一个路径参数;

StandardOpenOption 枚举类型

    public enum StandardOpenOption implements OpenOption {
        READ, // 读
        WRITE, // 写
        APPEND, // 在写模式下,进行追加写
        TRUNCATE_EXISTING, // 如果文件已经存在,并且它被打开以进行WRITE访问,那么它的长度将被截断为0。如果文件仅以READ访问方式打开,则忽略此选项。
        CREATE, // 如果文件不存在,请创建一个新文件。如果还设置了CREATE_NEW选项,则忽略此选项。与其他文件系统操作相比,检查文件是否存在以及创建文件(如果不存在)是原子性的。
        CREATE_NEW, // 创建一个新文件,如果文件已经存在则失败。与其他文件系统操作相比,检查文件是否存在以及创建文件(如果不存在)是原子性的。
        DELETE_ON_CLOSE, // 关闭时删除文件
        SPARSE, // 稀疏文件。当与CREATE_NEW选项一起使用时,此选项将提示新文件将是稀疏的。当文件系统不支持创建稀疏文件时,该选项将被忽略。
        SYNC, // 要求对文件内容或元数据的每次更新都以同步方式写入底层存储设备。
        DSYNC; // 要求对文件内容的每次更新都以同步方式写入底层存储设备。
    }
  • 从 FileInputStream / FileOutputStream 中获取

FileInputStream 对象中获取的通道是以读的方式打开文件,从 FileOutpuStream 对象中获取的通道是以写的方式打开文件。

    FileOutputStream ous = new FileOutputStream(new File("a.txt"));
    FileChannel out = ous.getChannel(); // 获取一个只读通道
    
    FileInputStream ins = new FileInputStream(new File("a.txt"));
    FileChannel in = ins.getChannel();  // 获取一个只写通道
  • 从 RandomAccessFile 中获取

从 RandomAccessFaile 中获取的通道取决于 RandomAccessFaile 对象是以什么方式创建的

    RandomAccessFile file = new RandomAccessFile("a.txt", "rw");
    FileChannel channel = file.getChannel(); // 获取一个可读写文件通道

模式说明

  • r :读模式
  • w :写模式
  • rw :读写模式

3.2 读取数据

读取数据的 read(ByteBuffer buf) 方法返回的值表示读取到的字节数,如果读到了文件末尾,返回值为 -1。读取数据时,position 会往后移动。

将数据读取到单个缓冲区

和一般通道的操作一样,数据也是需要读取到1个缓冲区中,然后从缓冲区取出数据。在调用 read 方法读取数据的时候,可以传入参数 position 和 length 来指定开始读取的位置和长度。

    FileChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
    ByteBuffer buf = ByteBuffer.allocate(5);
    while(channel.read(buf)!=-1){
        buf.flip();
        System.out.print(new String(buf.array()));
        buf.clear();
    }
    channel.close();
读取到多个缓冲区

文件通道 FileChannel 实现了 ScatteringByteChannel 接口,可以将文件通道中的内容同时读取到多个 ByteBuffer 当中,这在处理包含若干长度固定数据块的文件时很有用。

    ScatteringByteChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
    ByteBuffer key = ByteBuffer.allocate(5), value=ByteBuffer.allocate(10);
    ByteBuffer[] buffers = new ByteBuffer[]{key, value};
    while(channel.read(buffers)!=-1){
        key.flip();
        value.flip();
        System.out.println(new String(key.array()));
        System.out.println(new String(value.array()));
        key.clear();
        value.clear();
    }
    channel.close();

3.3 写入数据

从单个缓冲区写入

单个缓冲区操作也非常简单,它返回往通道中写入的字节数。

    FileChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.WRITE);
    ByteBuffer buf = ByteBuffer.allocate(5);
    byte[] data = "Hello, Java NIO.".getBytes();
    for (int i = 0; i < data.length; ) {
        buf.put(data, i, Math.min(data.length - i, buf.limit() - buf.position()));
        buf.flip();
        i += channel.write(buf);
        buf.compact();
    }
    channel.force(false);
    channel.close();
从多个缓冲区写入

FileChannel 实现了 GatherringByteChannel 接口,与 ScatteringByteChannel 相呼应。可以一次性将多个缓冲区的数据写入到通道中。

    FileChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.WRITE);
    ByteBuffer key = ByteBuffer.allocate(10), value = ByteBuffer.allocate(10);
    byte[] data = "017 Robothy".getBytes();
    key.put(data, 0, 3);
    value.put(data, 4, data.length-4);
    ByteBuffer[] buffers = new ByteBuffer[]{key, value};
    key.flip();
    value.flip();
    channel.write(buffers);
    channel.force(false); // 将数据刷出到磁盘
    channel.close();

RandomAccessFile、FileInputStream、FileOutputStream比较

获取方式 是否有文件读写权限
RandomAccessFile.getChannel 可读,是否可写根据传入mode来判断
FileInputStream.getChannel 可读,不可写
FileOutputStream.getChannel 可写,不可读

3.4 数据刷出

为了减少访问磁盘的次数,通过文件通道对文件进行操作之后可能不会立即刷出到磁盘,此时如果系统崩溃,将导致数据的丢失。为了减少这种风险,在进行了重要数据的操作之后应该调用 force() 方法强制将数据刷出到磁盘。

无论是否对文件进行过修改操作,即使文件通道是以只读模式打开的,只要调用了 force(metaData) 方法,就会进行一次 I/O 操作。参数 metaData 指定是否将元数据(例如:访问时间)也刷出到磁盘。

    channel.force(false); // 将数据刷出到磁盘,但不包括元数据

3.5 关闭FileChannel

用完FileChannel后必须将其关闭。

    channel.close();

3.6 其他方法

  • position()

描述:如果想在 FileChannel 的某一个指定位置读写数据,可以通过调用 FileChannel 的 position() 方法来获取当前的 position 值,也可以调用 FileChannel 的 position(long pos) 方法设置 position 的值。

    long pos = channel.position();
    
    channel.position(pos +123);

注意

如果设置的 position 值超出了 File 文件的最后位置,在读取该 Channel 时就会返回 -1 ,即返回“ 读取到文件的末尾 ”的标识。

但此时若向 Channel 中写入数据,该 Channel 连接的 File 会被“扩张”到这个设置的 position 值的位置,然后将数据写入到这个 File 中,这会导致该 File 带有“空洞”, 存储在磁盘上的这个物理文件就会不连续

  • size()

FileChannel 的 size() 方法会返回这个 FileChannel 连接的 File 文件的大小。

    long fileSize = channel.size();
  • truncate()

可以调用 FileChannel.truncate() 方法截断一个 File。截断时需要指定一个长度。

    channel.truncate(1024);

本示例将文件长度截断为1024个字节。

  • 如果给定大小小于该文件的当前大小,则截取该文件,丢弃文件末尾后面的所有字节。

  • 如果给定大小大于或等于该文件的当前大小,则不修改文件。

  • 无论是哪种情况,如果此通道的文件位置大于给定大小,则将位置设置为该大小。

  • transferTo 和 transferFrom方法

**通道之间的数据传输:**如果两个通道中有一个是 FileChannel,那你可以直接将数据从一个 channel 传输到另外一个 channel。

transferFrom() :FileChannel 的 transferFrom()方法可以将数据从源通道传输到 FileChannel 中。

    long transferFrom(ReadableByteChannel src,long position, long count)
  • src :源通道。
  • position :文件中的位置,从此位置开始传输;必须为非负数。
  • count : 要传输的最大字节数;必须为非负数。

如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数。

transferTo() :将数据从 FileChannel 传输到其他的 channel 中。

    long transferTo(long position, long count,WritableByteChannel target)
  • position :文件中的位置,从此位置开始传输,必须为非负数。
  • count :要传输的最大字节数;必须为非负数。
  • target :目标通道

示例

    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.nio.channels.FileChannel;
    
    /**
     * Created by lilinchao
     * Date 2022/5/27
     * Description 对文件进行复制操作
     */
    public class copyFileChannelDemo {
        public static void main(String[] args) throws Exception {
    
            //准备输入流(源文件)
            FileInputStream fileInputStream = new FileInputStream("datas/data.txt");
            //准备输出流(目标文件)
            FileOutputStream fileOutputStream = new FileOutputStream("datas/data3.txt");
    
            //根据流获取通道
            FileChannel inputStreamChannel = fileInputStream.getChannel();
            FileChannel outputStreamChannel = fileOutputStream.getChannel();
    
            //指向复制方法
    //        outputStreamChannel.transferFrom(inputStreamChannel,0,inputStreamChannel.size());
            inputStreamChannel.transferTo(0,inputStreamChannel.size(),outputStreamChannel);
    
            //关闭资源
            fileInputStream.close();
            fileOutputStream.close();
        }
    }

附参考文章链接

https://www.cnblogs.com/lxyit/p/9170741.html

https://mp.weixin.qq.com/s/Q1sxTp9JI6dDQc1rE3VOYA

https://blog.csdn.net/weixin_36586120/article/details/88756436

阅读全文