Java> 文件操作(Files, Path, File)

概要

Files: 操作文件的工具类,Java7加入,封装了用户机器上处理文件系统所需所有功能。包含了文件创建、复制、写入、读出、删除,获取文件信息,快捷访问、遍历目录等功能。使用较File更方便,由于结合了Path/Stream等类,很擅长批量处理文件。
Path: 表示文件路径,Java7加入,常用Paths创建,配合Files使用。
File: 传统文件类,Java 1.0加入,功能强大,但使用繁琐。

Path类

Path通过表示一个目录名序列,后面还可以跟着一个文件名,来表示路径。

创建方式

  • 通过指定路径字符串 Paths.get()
    通过Paths.get() 拼接多个字符串,组成路径。
    包含2类:1)绝对路径;2)相对路径。
    路径不必是一个存在的文件,仅仅只是一个抽象的名字序列。只有当要创建文件的时候,才会调用方法根据路径创建文件。
Path absolute = Paths.get("C:\\Users", "test.txt"); // 绝对路径, 使用Windows风格路径分隔符  
Path relative = Paths.get("config", "properties", "user.properties"); // 相对路径
  • 通过已有Path + 字符串组合 Path.resolve()和Path.resolveSibling()
// resolve path
Path basePath = Paths.get("rss"); // 通过字符串获取路径
Path resolvePath = basePath.resolve("resolvePath"); // 组合basePath和"resolvePath"得到新路径
Path resolveSibling = basePath.resolveSibling("resolveSibling"); // 得到basePath兄弟路径"resolveSibling"
// 打印转换path
System.out.println("basePath = " + basePath.toAbsolutePath());
System.out.println("resolvePath = " + resolvePath.toAbsolutePath());
System.out.println("resolveSibling = " + resolveSibling.toAbsolutePath());
  • 产生相对路径relativize
    relativize是resolve逆操作。
    p.resolve(r)结果产生路径q = "p/r";p.relative(q)产生r,即r="../q"。简单来说,就是resolve是利用母路径path+字符串(作为子路径),组合成新路径;relativize是通过母路径 - 组合的新路径,得到相对路径。
// relative path
System.out.println(basePath.relativize(resolvePath));
  • 打印转换path和relative path运行结果
basePath = F:\workspace\IDEA\Java_Core2\rss
resolvePath = F:\workspace\IDEA\Java_Core2\rss\resolvePath
resolveSibling = F:\workspace\IDEA\Java_Core2\resolveSibling
resolvePath
  • 其他常用操作
    normalize 移除所有冗余.和..部件
    toAbsolutePath 产生给定路径的绝对路径
    getParent 获取父路径
    getFileName 获取文件名
    getRoot 获取根目录,Unix是 / , Windows是所在盘符根目录
    toFile 转换成File类对象

通过Path构建Scanner对象

Scanner in = new Scanner(Paths.get("C:\\Users\test.txt"));

Files类

创建文件

  • 创建目录
    如果目录已经存在会抛出异常FileAlreadyExistsException. 创建目录是原子性的
Path path = Paths.get("dir");
Files.createDirectory(path); // 创建以path为路径的目录
  • 创建文件
    如果文件已经存在会抛出异常FileAlreadyExistsException. 创建文件是原子性的
Path path = Paths.get("file");
Files.createDirectory(pat); // 创建以path为路径的文件, 文件可以与目录路径及同名 
  • 在给定位置或者系统指定位置,创建临时文件/目录
Path newPath = Files.createTempFile(dir, prefix, suffix); // dir路径下, 创建以prefix为前缀, suffix为后缀的名称的文件
Path newPath = Files.createTempFile(prefix, suffix); // 系统默认临时目录路径下, 创建以prefix为前缀, suffix为后缀的名称的文件
Path newPath = Files.createTempDirectory(dir, prefix); // dir路径下, 创建以prefix为前缀, suffix为后缀的名称的目录
Path newPath = Files.createTempDirecotry(prefix); // 系统默认临时目录路径下, 创建以prefix为前缀, suffix为后缀的名称的目录

dir是一个Path对象,给定创建临时文件路径;
prefix,suffix可以为null字符串,分别指定文件名前缀、后缀;
系统默认临时文件夹路径,Win10x64:C:\Users\Martin\AppData\Local\Temp

读写文件

  • 读取/写中小文件
/* 一次读取所有文件内容 */
// 一次按二进制读取所有文件内容
byte[] bytes = Files.readAllBytes(path); // 文件路径Path -> 二进制数组byte[]
// 将bytes转换成字符串
String content = new String(bytes, charset); // charset指定字符编码, 如StandardCharsets.UTF_8

// 一次按行读取文件所有内容
List<String> lines = Files.readAllLines(path);

/* 一次写所有文件内容 */
// 写一个字符串到文件
Files.write(path, content.getBytes(charset)); 
// 追加字符串到文件
Files.write(path, content.getBytes(charset),StandardOpenOption.APPEND);
// 写一个行的集合到文件
Files.write(path, lines);
  • 大文件
    要处理大文件和二进制文件,需要用到输入流/输出流,或者使用读入器/写入器。
InputStream in = Files.newInputStream(path);
OutputStream out = Files.newOutputStream();
Reader reader = Files.newBufferedReader(path, charset);
Writer writer = Writer.newBufferedWriter(path, charset);

上面这些方法较单纯使用FileInputStream, FileOutputStream, BufferedReader, BufferedWriter更为简便。
例如,如果使用FileInputStream和FileOutputStream,需要这样使用

// read data from stream
try(DataInputStream in = new DataInputStream(new FileInputStream("filename"))) {
      ...
}

// write data to stream
try(DataOutputStream out = new DataOutputStream(new FileOutputStream("filename"))) {
      ...
}

简单来说,就是少了专门new的语句;对于Reader/Writer,还少了包装的语句。

复制文件

从一个位置复制到另外一个位置

Files.copy(fromPath, toPath); // fromPath和toPath都是Path对象, 如果目标路径已存在文件, 复制失败
Files.copy(fromPath, toPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); // 选项REPLACE_EXISTING表示想覆盖原有目标路径, COPY_ATTRIBUTES表示复制所有文件属性
Files.copy(inputStream, toPath); // 从输入流复制到目标路径
Files.copy(fromPath, outputStream); // 从源路径复制到输出流

移动文件

从一个位置移动到另外一个位置

Files.move(fromPath, toPath); // fromPath和toPath都是Path对象
Files.move(fromPath, toPath, StandardCopyOption.ATOMIC_MOVE); // ATOMIC_MOVE表示该操作是原子性的(要么成功移动到目标路径, 要么失败文件还在原来的位置)
// move操作无法从输入流到目标路径, 或者从源路径到输出流

删除文件

删除指定路径文件

Files.delete(path); // 如果指定路径不存在, 报异常NoSuchFileException
boolean deleted = Files.deleteIfExist(path); // 如果文件存在, 才会删除, 不会报异常. 可以用来删除空目录

获取文件信息

常用操作

boolean exists(path) // 文件存在?
boolean isHidden(path) // 文件隐藏?
boolean isReadable(path) // 文件可读?
boolean isWritable(path) // 文件可写?
boolean isExecutable(path) // 可执行?
boolean isRegularFile(path) // 是普通文件? 等价于!isSymbolicLink() && !isDirectory() && !isOther()
boolean isDirectory(path) // 是目录?
boolean isSymbolicLink(path) // 是符号链接? 

long fileSize = Files.size(path); // 获取文件字节数

获取基本文件属性集
基本文件属性集主要包括:

  1. 创建文件、最后一次访问以及最后一次修改时间;
  2. 文件是常规文件、目录,还是符号链接;
  3. 文件尺寸;
  4. 文件主键,具体所属类与文件系统相关,有可能是文件唯一标识符,有可能不是;
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);

PosixiFileAttributes posixAttributes = Files.readAttributes(path, PosixiFileAttributes.class); // 如果文件系统兼容POSIX, 才能获取到PosixiFileAttributes 实例

访问目录各项

  • 遍历指定目录下各项
    Files.list会返回Stream,而且是惰性读取,处理目录具有大量项时高效。不过,list不会进入子目录,进入子目录使用walk。使用示例
try(Stream<Path> entries = Files.list(dirPath)) { // 读取目录涉及需要关闭系统资源, 使用try块. 不进入子目录
      entries.forEach(System.out::println); // 打印每个entries项, 也就是打印每个path
}

try(Stream<Path> entries = Files.walk(dirPath)) { // 会进入子目录
      entries.forEach(System.out.println);
}
  • 遍历并删除指定目录下各项
    使用Files.walk遍历 得到Stream,再利用Stream的forEach方法对各项进行处理
/* 示例将当前目录rss下所有文件(包括目录)及子文件, 都复制到目录rss2下 */
Path source = Paths.get("rss"); // 根据实际情况设置字节的source路径
Path target = Paths.get("rss2");

try(Stream<Path> entries = Files.walk(source)) {
      entries.forEach( p-> {
            try{
                  Path q = target.resolve(source.relative(p)); // 取得p相对于source的相对路径后, 再拼接到target路径下. 相当于是说, 将每个文件相对路径都由source转移到target下
                  if(!Files.exists(q)) {
                        if(Files.isDirectory(q)) Files.createDirectory(q); // 如果是目录, 在target路径下, 根据相对路径创建对应目录
                        else Files.copy(p, q);  // 如果是文件, 从source路径复制到target下
                  }
            } catch(IOException e) {
                  e.printStackTrace();
            }
      });
}

目录流

使用Files.walk有一个缺陷:无法方便地删除目录,因为要删除父目录,必须先删除子目录。否则,会抛出异常。
使用File.newDirectoryStream对象,产生一个DirectoryStream,对遍历过程可以进行更细粒度控制。DirectoryStream不是Stream,而是专门用于目录遍历的接口。它是Iterable的子接口,可以用Iterable的迭代和增强forEach方法。
还可以搭配glob模式来过滤文件,示例是过滤出dir目录下 后缀名为 .java的文件:

try(DirectoryStream<Path> entries = Files.newDirectoryStream(dir, "*.java")){
      for (Path entry: entries) {
            Process entry
      }
}

glob模式

模式 描述 示例
* 匹配路径组成部分中0个或多个字符串 *.java 匹配当前目录中的所有java文件
** 匹配跨目录边界的0个或多个字符串 **.java 匹配在所有子目录中的java文件
? 匹配一个字符 ????.java 匹配所有4个字符的java文件(不含扩展名)
[...] 匹配一个字符集合, 可以使用连线字符[0-9]和取反字符[!0-9] Test[0-9A-F].java 匹配Testx.java, 其中x是一个十六进制数
匹配由逗号隔开的多个可选项之一 *.{java,class} 匹配所有的java文件和类class文件
\ 转义任意模式中的字符以及\字符 *\** 匹配所有文件名中包含*的文件

注意:如果使用Windows,必须对glob的反斜杠转义两次:一次是glob语法转义,另外一次是java字符串转义:Files.newDirectoryStream(dir, "C:\\\\") // 相当于C:\\

访问目录所有子孙

如果想要访问某个目录下所有子孙,可以使用walkFileTree(),并向其传递一个FileVisitor对象。这个方法并非简单遍历,而是在遇到文件或目录时,目录被处理前后,访问文件错误时,FileVisitor会收到通知,然后指定执行方式:跳过该文件、跳过目录、跳过兄弟文件、终止访问。

// walkFileTree得到的通知:
FileVisitResult visitFile()  // 遇到文件或目录时
FileVisitResult preVisitDirectory() // 一个目录被处理前
FileVisitResult postVisitDirectory() // 一个目录被处理后
FileVisitResult visitFileFailed() // 试图访问文件失败, 或目录发生错误时

// 收到通知后, 可以设置指定的操作
FileVisitResult.CONTINURE // 继续访问下一个文件
FileVisitResult.SKIP_SUBTREE // 继续访问, 但不再访问这个目录下任何文件
FileVisitResult.SKIP_SIBLINGS // 继续访问, 但不再访问这个文件的兄弟文件(同一个目录下的文件)
FileVisitResult.TERMINATE // 终止访问

便捷类SimpleFileVisitor + Files.walkFileTree()可以实现对目录的细粒度访问,并在在收到相关通知时,有机会进行相应处理。默认SimpleFileVisitor类实现FileVisitor接口,除visitFileFailed() 外(抛出异常并终止访问),其余方法都是直接继续访问,而不做任何处理。
注意:preVisitDirectory()和postVisitDirectory()通常需要覆盖,否则,访问时遇到不允许打开的目录或者不允许访问的文件时立即失败,进而直接跳转到visitFileFailed()

示例,展示如何打印给定目录下的所有子目录:

Files.walkFileTree(Paths.get("F:\\test"), new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
        System.out.println(dir);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
        System.out.println("postVisitDirectory " + dir);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
        return FileVisitResult.SKIP_SUBTREE;
    }
});

执行结果:

F:\test
F:\test\dir1
F:\test\dir1\subdir1
postVisitDirectory F:\test\dir1\subdir1
postVisitDirectory F:\test\dir1
F:\test\dir2
postVisitDirectory F:\test\dir2
F:\test\dir3
postVisitDirectory F:\test\dir3
postVisitDirectory F:\test

示例2,删除目录树(包括其中的文件)
利用walkFileTree访问到对应路径目录时,利用便捷类SimpleFileVisitor的preVisitDirectory先删除当前目录下的所有文件,然后访问完毕后,在postVisitDirectory删除当前访问完毕的目录。

Files.walkFileTree(Paths.get("F:\\test"), new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
        System.out.println(dir);

        // 删除dir路径下所有文件(不包含子目录)
        Files.list(dir).forEach(p->{
            try {
                if (!Files.isDirectory(p))
                    Files.delete(p);

            } catch (IOException e) {
                e.printStackTrace();
            }
        });

        return FileVisitResult.CONTINUE;
    }

    // 删除目录
    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
        System.out.println("postVisitDirectory " + dir);
        if (null != exc) throw exc;
        Files.delete(dir);
        return FileVisitResult.CONTINUE;
    }
} );

File类

创建文件

通过路径字符串创建

File file = new File("filePath");  // 在当前路径创建名为"filePath"的文件, 此时磁盘上还没有创建对应文件或目录

// 磁盘上创建文件或目录
File tempFile = File.createTempFile(prefix, suffix, directory);  // 在指定目录或当前目录(directory缺省),创建以prefix/suffix为前缀/后缀的临时文件
file.createNewFile(); // 以file所代表路径, 创建新文件
file.mkdir(); // 以file所代表路径, 创建新目录, 注意mkdir()和createNewFile() 不能同时用同一个file所代表路径创建同名文件/目录对象, 否则后者无法创建

读写文件

单纯File类是没有办法直接读写文件的,这点跟Files是一个明显区别,File类需要借助输入流/输出流(FileInputStream/FileOutputStream),或者读入器/写出器(Reader/Writer)来实现读写功能。
示例是用DataInputStream和DataOutoutStream进行文件读写包装,支持字符本文以及二进制数据。使用Reader和Writer的方式,这里省略。感兴趣可参考系统学习 Java IO (十三)----字符读写 Reader/Writer 及其常用子类

// 写数据到文件file
try(DataOutputStream out = new DataOutputStream(new FileOutputStream((file)))) {
    out.writeInt(1);
    out.writeChar('a');
}

//从文件file读数据
try(DataInputStream in = new DataInputStream(new FileInputStream(file))) {
    // read数据类型的个数和顺序一定要和write一致
    int a = in.readInt();
    char c = in.readChar();

    System.out.println(a);
    System.out.println(c);
}

复制和移动

复制和移动文件,File类没有专门的API,需要自行实现。这里只举一个复制的例子,移动可以看成是 复制+删除源文件。

/**
* 利用File类复制文件(包括目录)
* @note 要求文件只能是普通文件或者目录
*/
public static File copyFile(String dest, String src) throws IOException {
    File destFile = new File(dest);
    File srcFile = new File(src);
    
    // 源文件不存在, 无法复制
    if (!srcFile.exists()) {
        System.out.println("源文件不存在, 路径: " + srcFile);
        return null;
    }
    
    // 根据源文件类型, 在目标路径新建文件或目录
    if (srcFile.isDirectory()) {
        destFile.mkdir();
        return destFile;
    }
    else srcFile.createNewFile();

    // 处理源文件为文件(非目录)的情形
    try(Scanner scanner = new Scanner(new FileInputStream(srcFile))) {

        try(PrintWriter writer = new PrintWriter(new FileOutputStream(destFile))){
            while (scanner.hasNext()) {
                String line = scanner.nextLine();
                writer.println(line);
            }
        }
    }

    return destFile;
}

删除文件

删除文件包括删除一般文件,还有目录。文件比较容易,直接调用file.delete()即可,目录的情况较为复杂,因为涉及到目录不为空的情况。与Files.delete()删除目录类似,必须确保带删除目录为空目录,也就是说需要遍历目录及其子目录,先删除目录下的内容,才能删除对应目录,这里不再遨述,后续有机会再补充完整这块示例。

获取文件信息

直接调用File对象接口,即可以查询到文件对应信息,主要包括

String getName() // 获取文件名称
String getParent() // 获取所在目录名称
File getParentFile() // 获取文件路径
boolean canRead() // 获取文件是否可读
boolean canWrite() // 获取文件是否可写
boolean exists() // 获取文件是否存在
boolean isDirectory() // 表示是否是一个目录
boolean isFile() // 表示是否是一个标注文件
long lastModified() // 最近一次修改
long length() // 文件长度
String[] list() // 路径名所表示的目录中的文件名列表
String[] list(FilenameFilter filter) // // 路径名所表示的目录中的文件名列表, 文件名经由filter过滤
String[] listFiles() // 路径名所表示的目录中的文件路列表
boolean setReadOnly() // 设置文件为只读

Java File类|菜鸟教程

posted @ 2020-11-28 17:28  明明1109  阅读(8507)  评论(2编辑  收藏  举报