首页 / Java 入门教程 / 文件与目录操作

Java 入门教程

文件与目录操作

本教程共 100 篇 · 第 92 篇 · 更新于 2026-08-05 · 约 14 分钟阅读

JavaJava 入门教程Files目录遍历文件复制walkFileTree

92. 文件与目录操作

本节目标:用 Files 工具类完成建、删、改、查、遍历这五类文件与目录操作。

创建文件和目录

Files 提供了一组 createXxx 方法。

创建空文件用 createFile()。它有个特点:如果文件已存在,会抛 FileAlreadyExistsException,而不是默默覆盖。

import java.io.IOException;
import java.nio.file.*;

public class CreateDemo {
    public static void main(String[] args) throws IOException {
        Path file = Path.of("new.txt");
        if (Files.notExists(file)) {
            Files.createFile(file);
            System.out.println("文件已创建");
        }

        // 单层目录:父目录必须已存在
        Path dir = Path.of("output");
        if (Files.notExists(dir)) {
            Files.createDirectory(dir);
        }

        // 多层目录:中间不存在的层级会一并创建
        Path deep = Path.of("output", "2026", "08", "logs");
        Files.createDirectories(deep);
        System.out.println("多层目录已创建:" + deep);
    }
}
```bash

`createDirectory()` 和 `createDirectories()` 差一个字母,行为差别很大。

前者只建一层,父目录不存在就报错。后者会把整条路径上缺失的目录全部补齐,而且**目录已存在时不报错**,所以更常用。

## 临时文件

处理中间数据时经常需要临时文件。手动拼路径既麻烦又容易冲突,用现成方法更好:

```java
// 在系统临时目录下创建,文件名形如 upload_1234567.tmp
Path temp = Files.createTempFile("upload_", ".tmp");
System.out.println(temp);

// 也可以创建临时目录
Path tempDir = Files.createTempDirectory("cache_");
Tip

临时文件不会自动删除。要么用完手动 Files.delete(),要么在打开时加上 StandardOpenOption.DELETE_ON_CLOSE,让流关闭时自动清理。

复制文件

Files.copy() 一行搞定复制,不用再写读写循环:

import java.io.IOException;
import java.nio.file.*;

public class CopyDemo {
    public static void main(String[] args) throws IOException {
        Path source = Path.of("source.txt");
        Files.writeString(source, "原始内容");

        Path target = Path.of("backup.txt");
        Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

        System.out.println("复制完成,内容:" + Files.readString(target));
    }
}
```bash

第三个参数是可变参数,常用两个选项:

| 选项 | 作用 |
| --- | --- |
| `REPLACE_EXISTING` | 目标已存在时覆盖,否则抛异常 |
| `COPY_ATTRIBUTES` | 一并复制修改时间等属性 |

不加 `REPLACE_EXISTING` 而目标文件已存在,就会抛 `FileAlreadyExistsException`。这是新手最常撞的墙。

> [!WARNING]
> `Files.copy()` 复制目录时,**只创建一个空目录,不会复制里面的内容**。要复制整个目录树,得配合下面讲的遍历方法逐个处理。

## 移动与重命名

移动和重命名在文件系统层面是同一个操作,都用 `Files.move()`:

```java
Path from = Path.of("old.txt");
Path to = Path.of("archive", "new.txt");

Files.createDirectories(to.getParent());
Files.move(from, to, StandardCopyOption.REPLACE_EXISTING);

同一磁盘内移动通常只是改一下目录项,速度极快,跟文件大小无关。跨磁盘时底层会退化成「复制 + 删除」,就慢了。

还有个 ATOMIC_MOVE 选项,保证移动要么完全成功要么完全不发生,不会出现半截状态。写日志轮转、文件锁这类逻辑时很关键,但不是所有文件系统都支持。

删除

两个方法,区别在于文件不存在时的反应:

Files.delete(path);            // 不存在则抛 NoSuchFileException
Files.deleteIfExists(path);    // 不存在返回 false,不抛异常
```bash

日常清理用 `deleteIfExists()` 更省心,不用先判断再删。

删目录有个硬性要求:**目录必须为空**。非空目录调用 `delete()` 会抛 `DirectoryNotEmptyException`。想删整棵树,必须先递归删光里面的内容。

> [!WARNING]
> 删除操作不进回收站,是真的没了。写自动清理逻辑前,务必反复确认路径拼对了。一个拼错的路径足以酿成事故。

## 遍历单层目录

只想看某个目录下有什么,用 `DirectoryStream`:

```java
import java.io.IOException;
import java.nio.file.*;

public class ListDir {
    public static void main(String[] args) throws IOException {
        Path dir = Path.of(".");

        try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
            for (Path entry : stream) {
                String type = Files.isDirectory(entry) ? "[目录]" : "[文件]";
                System.out.println(type + " " + entry.getFileName());
            }
        }
    }
}

DirectoryStream 实现了 AutoCloseable,必须放进 try-with-resources。它是惰性的,不会一次性把所有条目读进内存,所以哪怕目录里有几万个文件也扛得住。

它还支持通配符过滤,第二个参数叫 glob 模式:

// 只列出 .java 和 .txt 文件
try (DirectoryStream<Path> stream =
             Files.newDirectoryStream(dir, "*.{java,txt}")) {
    stream.forEach(p -> System.out.println(p.getFileName()));
}
```bash

glob 语法很简单:`*` 匹配任意多个字符,`?` 匹配一个字符,`{a,b}` 匹配任一选项,`[0-9]` 匹配范围。

## 递归遍历整棵树

`Files.walk()` 返回一个 `Stream<Path>`,包含指定目录下的所有层级:

```java
import java.io.IOException;
import java.nio.file.*;
import java.util.stream.Stream;

public class WalkDemo {
    public static void main(String[] args) throws IOException {
        Path root = Path.of(".");

        try (Stream<Path> stream = Files.walk(root, 3)) { // 最多下探 3 层
            stream.filter(Files::isRegularFile)
                  .filter(p -> p.toString().endsWith(".md"))
                  .limit(10)
                  .forEach(System.out::println);
        }
    }
}

第二个参数限制深度,不传就是无限深。同样要 try-with-resources,理由和上一节的 Files.lines() 一样。

配合 Stream 可以做统计。比如算整个目录占多少空间:

try (Stream<Path> stream = Files.walk(Path.of("."))) {
    long total = stream.filter(Files::isRegularFile)
                       .mapToLong(p -> {
                           try {
                               return Files.size(p);
                           } catch (IOException e) {
                               return 0L;
                           }
                       })
                       .sum();
    System.out.println("总大小:" + total / 1024 + " KB");
}
```bash

Lambda 里不能直接抛受检异常,所以要写 try-catch,这是 `Files.walk()` 用起来略啰嗦的地方。

## 精细控制用 walkFileTree

需要在遍历过程中做判断、跳过某些目录、或者处理访问失败时,`Files.walk()` 就不够用了。

`Files.walkFileTree()` 采用访问者模式,能在每个节点插入自己的逻辑:

```java
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class DeleteTree {
    public static void main(String[] args) throws IOException {
        Path root = Path.of("output");
        if (Files.notExists(root)) {
            return;
        }

        Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                    throws IOException {
                Files.delete(file);      // 先删文件
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException e)
                    throws IOException {
                Files.delete(dir);       // 子项清空后再删目录
                return FileVisitResult.CONTINUE;
            }
        });
        System.out.println("目录树已删除");
    }
}

这就是「递归删除非空目录」的标准写法。关键在于 postVisitDirectory 在子项都处理完之后才被调用,此时目录已经空了,可以安全删除。

visitFile 的返回值还能是 SKIP_SUBTREE(跳过整个子目录)或 TERMINATE(立刻结束遍历),控制粒度很细。

读取文件属性

import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;

public class Attributes {
    public static void main(String[] args) throws IOException {
        Path path = Path.of("source.txt");

        BasicFileAttributes attrs =
                Files.readAttributes(path, BasicFileAttributes.class);

        System.out.println("大小:" + attrs.size() + " 字节");
        System.out.println("创建时间:" + attrs.creationTime());
        System.out.println("最后修改:" + attrs.lastModifiedTime());
        System.out.println("是目录:" + attrs.isDirectory());
    }
}
```bash

一次 `readAttributes()` 拿到所有属性,比分别调用 `Files.size()`、`Files.getLastModifiedTime()` 更高效——后者每次都要访问一趟文件系统。

## 复制整个目录树

前面说过 `Files.copy()` 对目录只建空壳。真要复制整棵树,把遍历和复制组合起来就行:

```java
import java.io.IOException;
import java.nio.file.*;
import java.util.stream.Stream;

public class CopyTree {
    public static void main(String[] args) throws IOException {
        Path src = Path.of("output");
        Path dst = Path.of("output-backup");

        try (Stream<Path> stream = Files.walk(src)) {
            stream.forEach(source -> {
                try {
                    // 用 relativize 算出相对位置,再拼到目标目录下
                    Path target = dst.resolve(src.relativize(source));
                    Files.copy(source, target,
                            StandardCopyOption.REPLACE_EXISTING);
                } catch (IOException e) {
                    System.err.println("复制失败:" + source);
                }
            });
        }
        System.out.println("目录树复制完成");
    }
}

关键在 dst.resolve(src.relativize(source)) 这一句。它先算出当前项相对于源目录的位置,再把这个相对路径接到目标目录下,就得到了对应的新位置。

Files.walk() 保证父目录先于子项被访问,所以复制到子文件时,父目录已经建好了,不会报错。

注意路径的大小写

Windows 的文件系统不区分大小写,Data.txtdata.txt 是同一个文件。Linux 和 macOS 默认区分,是两个不同的文件。

这个差异经常导致「本地跑得好好的,部署到服务器就找不到文件」。开发时统一用小写文件名,能避开绝大多数这类问题。

Tip

类似的还有路径分隔符。代码里不要硬编码 "docs\\java",用 Path.of("docs", "java") 让 API 去处理平台差异。

本节小结

  • createDirectories() 建多层且已存在不报错,比 createDirectory() 好用。
  • Files.copy() 复制目录只建空壳,内容要自己递归处理。
  • move() 同盘内极快,ATOMIC_MOVE 保证不出现中间状态。
  • 删除不进回收站,非空目录必须先清空。
  • 单层遍历用 DirectoryStream 并支持 glob 过滤。
  • 递归遍历用 Files.walk(),需要精细控制时用 walkFileTree()
  • 批量读属性用 readAttributes(),比逐个查询快。