02 Java文件读写通道的使用以及文件的基本操作方法

一、FileChannel基本使用方法

1-1-1 概述

A channel for reading, writing, mapping, and manipulating a file.A file channel is a SeekableByteChannel that is connected to a file. It has a current position within its file which can be both queried and modified. 

特点:FileChannel 只能工作在阻塞模式下网络编程中socket channel配合selector可以工作在非阻塞模式下

  • Java中程序对于文件的字节操作需要借助文件读写通道,实际应用时,channel与buffer配合使用实现文件的修改

1-1-2 FileChannel基本使用方法

FileChannel获取的三种方式

不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法
1)通过 FileInputStream 获取的 channel 只能读
2)通过 FileOutputStream 获取的 channel 只能写
3)通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定

读取

/*channel 读取数据填充 ByteBuffer,返回值表示读到了多少字节,-1 表示到达了文件的末尾*/
int readBytes = channel.read(buffer); 

写入

注意点:每次写入前对buffer进行检查,看有没有数据,如果有数据应先写入剩余数据。

  • 主要原因在于buffer有可能不是一次写入。
ByteBuffer buffer = ...;
buffer.put(...); // 存入数据
buffer.flip();   // 切换读模式

while(buffer.hasRemaining()) {
    channel.write(buffer);
}

关闭

channel 必须关闭,不过调用了 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 close 方法会间接地调用 channel 的 close 方法


数据位置获取

  • 获取当前位置
long pos = channel.position();
  • 设置当前位置
long newPos = ...;
channel.position(newPos);
  • 设置当前位置时,如果设置为文件的末尾
    • 这时读取会返回 -1
    • 这时写入,会追加内容,但要注意如果 position 超过了文件末尾,再写入时在新内容和原末尾之间会有空洞(00)

大小

  • 使用 size 方法获取文件的大小

强制写入(注意点)

 force(true) 
  • 操作系统出于性能的考虑,会将数据缓存,不是立刻写入磁盘
  • 调用 force(true) 方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘

1-1-3 FileChannel应用:传输数据

transferTo:通过零拷贝技术将一个文件的内容拷贝到另外一个文件中。

Transfers bytes from this channel's file to the given writable byte channel.
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;

public class test7 {
    public static void main(String[] args) {
        String FROM = "words.txt";
        String TO = "to.txt";
        long start = System.nanoTime();
        try (FileChannel from = new FileInputStream(FROM).getChannel();
             FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
            /*起始位置,数据大小,目标读写通道*/
            /*transferTo:底层使用零拷贝技术,效率比较高*/
            /*注意点:这个方法调用一次传输的文件大小是有上限的,大约2G左右,多余的不会传送,因此文件太大需要分多次传送*/
            // from.transferTo(0, from.size(), to);        // 将words.txt的内容拷贝到to.txt中。

            /*通过循环确保大文件多次传输完成*/
            long size = from.size();
            for(long left = size;left > 0;) {
                left -= from.transferTo(size - left, from.size(), to);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

注意点:

  • 较大的文件需要考虑多次传输问题。

二、Paths配合Files工具类的基本使用

2-1 概述

Paths类作用:jdk7 引入了 Path 和 Paths 类,Path 用来表示文件路径,Paths 是工具类,用来获取 Path 实例,Path配合Paths类使用。

Files类作用:配合Paths类读取指定的文件。

Interface Path

Class Paths

2-2 Paths的使用示例

Path source = Paths.get("1.txt"); // 相对路径 使用 user.dir 环境变量来定位 1.txt
Path source = Paths.get("d:\1.txt"); // 绝对路径 代表了  d:1.txt
Path source = Paths.get("d:/1.txt"); // 绝对路径 同样代表了  d:1.txt
Path projects = Paths.get("d:\data", "projects"); // 代表了  d:dataprojects

示例

import java.nio.file.Path;
import java.nio.file.Paths;
public class test8 {
    public static void main(String[] args) {
        Path path = Paths.get("d:\data\projects\a\..\b");
        System.out.println(path);
        System.out.println(path.normalize()); // 正常化路径
    }
}

执行结果

  • 能够对..和.进行解析
d:dataprojectsa..
d:dataprojects

2-3 Files的使用

2-3-1 基本使用

检查文件是否存在

Path path = Paths.get("helloword/data.txt");
System.out.println(Files.exists(path));

创建一级目录

Path path = Paths.get("helloword/d1");
Files.createDirectory(path);
  • 如果目录已存在,会抛异常 FileAlreadyExistsException
  • 不能一次创建多级目录,否则会抛异常 NoSuchFileException

创建多级目录

Path path = Paths.get("helloword/d1/d2");
Files.createDirectories(path);

拷贝文件

Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/target.txt");
Files.copy(source, target);
  • 如果文件已存在,会抛异常 FileAlreadyExistsException

如果希望用 source 覆盖掉 target,需要用 StandardCopyOption 来控制

Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

移动文件

Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/data.txt");
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
  • StandardCopyOption.ATOMIC_MOVE 保证文件移动的原子性

删除文件

Path target = Paths.get("helloword/target.txt");

Files.delete(target);
  • 如果文件不存在,会抛异常 NoSuchFileException

删除目录

Path target = Paths.get("helloword/d1");
Files.delete(target);
  • 目录还有内容,会抛异常 DirectoryNotEmptyException

2-3-2 遍历目录文件(访问者模式的实例)

需求:统计某个目录下有多少文件,目录以及jar包。

package part1;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.atomic.AtomicInteger;

public class test9 {
    public static void main(String[] args) {
        // 这里采用原子累加器的原因: 匿名内部类访问的局部变量,这个局部变量必须被final修饰,为了保证数据一致性,
        // 所以普通局部变量无法在这里进行累加
        // 在JDK1.8前,不加无法通过编译,JDK1.8字节码生成的时候会自动加上
        AtomicInteger dirCount = new AtomicInteger();
        AtomicInteger fileCount = new AtomicInteger();
        AtomicInteger jarCount = new AtomicInteger();
        try {                           /*起始目录,用于遍历文件的实例*/
            Files.walkFileTree(Paths.get("C:\Program Files\Java\jdk1.8.0_131"), new SimpleFileVisitor<Path>(){
                // 访问目录前调用:统计目录个数
                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
//                    System.out.println(dir);
                    dirCount.incrementAndGet();
                    return super.preVisitDirectory(dir, attrs);
                }
                // 访问目录时调用:统计文件个数
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
//                    System.out.println(file);
                    fileCount.incrementAndGet();
                    if (file.toFile().getName().endsWith(".jar")) {
                        jarCount.incrementAndGet();
                    }
                    return super.visitFile(file, attrs);
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("Number of directory:"+dirCount);
        System.out.println("Number of directory:"+fileCount);
        System.out.println("Number of jar:"+jarCount);
    }
}

执行结果

Number of directory:134
Number of directory:1476

上面代码的注意点

  • 匿名内部类中不能采用局部变量进行累加
  • 上面的将对目录与文件的操作交给访问者类,是设计模式中的访问者模式

访问者模式

介绍
意图:主要将数据结构与数据操作分离。
主要解决:稳定的数据结构和易变的操作耦合问题。
何时使用:需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。
如何解决:在被访问的类里面加一个对外提供接待访问者的接口。
关键代码:在数据基础类里面有一个方法接受访问者,将自身引用传入访问者。
应用实例:您在朋友家做客,您是访问者,朋友接受您的访问,您通过朋友的描述,然后对朋友的描述做出一个判断,这就是访问者模式。
优点: 1、符合单一职责原则。 2、优秀的扩展性。 3、灵活性。
缺点: 1、具体元素对访问者公布细节,违反了迪米特原则。 2、具体元素变更比较困难。 3、违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
使用场景: 1、对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。 2、需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。
注意事项:访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器。

2-3-3 删除多级目录

  • 删除是危险操作,确保要递归删除的文件夹没有重要内容
Path path = Paths.get("d:\a");
Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 
        throws IOException {
        Files.delete(file);
        return super.visitFile(file, attrs);
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) 
        throws IOException {
        Files.delete(dir);
        return super.postVisitDirectory(dir, exc);
    }
});

2-3-4 拷贝多级目录

long start = System.currentTimeMillis();
String source = "D:\Snipaste-1.16.2-x64";
String target = "D:\Snipaste-1.16.2-x64aaa";

Files.walk(Paths.get(source)).forEach(path -> {
    try {
        String targetName = path.toString().replace(source, target);
        // 是目录
        if (Files.isDirectory(path)) {
            Files.createDirectory(Paths.get(targetName));
        }
        // 是普通文件
        else if (Files.isRegularFile(path)) {
            Files.copy(path, Paths.get(targetName));
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
});
long end = System.currentTimeMillis();
System.out.println(end - start);

参考资料

01 FileChannel的官方API文档

02 Netty基础课程

原文地址:https://www.cnblogs.com/kfcuj/p/14707966.html