FileChannel
NOTE
FileChannel 是 Java NIO 中用于文件操作的通道,它提供了比传统 IO 流更高效的文件读写能力。需要注意的是,FileChannel 只能工作在阻塞模式下,不支持非阻塞操作。
获取 FileChannel
获取 FileChannel 有三种常见方式:
1. 通过 FileInputStream 获取(只读)
try (FileInputStream fis = new FileInputStream("data.txt");
FileChannel channel = fis.getChannel()) {
// 只能读取,不能写入
}2. 通过 FileOutputStream 获取(只写)
try (FileOutputStream fos = new FileOutputStream("data.txt");
FileChannel channel = fos.getChannel()) {
// 只能写入,不能读取
}3. 通过 RandomAccessFile 获取(可读可写)
try (RandomAccessFile raf = new RandomAccessFile("data.txt", "rw");
FileChannel channel = raf.getChannel()) {
// 可读可写,模式由 RandomAccessFile 的 mode 参数决定
// "r" - 只读, "rw" - 读写
}读取
使用 read() 方法从 FileChannel 读取数据到 ByteBuffer,返回值表示读到了多少字节,-1 表示到达了文件的末尾。
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);写入
使用 write() 方法将 ByteBuffer 中的数据写入 FileChannel:
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello, FileChannel!".getBytes(StandardCharsets.UTF_8));
buffer.flip(); // 切换为读模式
// write() 返回写入的字节数
while (buffer.hasRemaining()) {
channel.write(buffer);
}WARNING
write() 方法不保证一次调用就能写入所有数据,因此需要在循环中调用,直到 buffer 中没有剩余数据。
关闭
FileChannel 使用完毕后必须关闭以释放系统资源。推荐使用 try-with-resources 语法:
try (FileChannel channel = FileChannel.open(Paths.get("data.txt"),
StandardOpenOption.READ, StandardOpenOption.WRITE)) {
// 使用 channel
} // 自动关闭位置
FileChannel 维护一个内部的 position(位置指针),表示当前读写的位置。
获取当前位置
long pos = channel.position();设置当前位置
long newPos = 100;
channel.position(newPos);IMPORTANT
设置 position 时需要注意:
- 如果 position 设置到文件末尾,此时读取会返回 -1
- 如果 position 超过文件末尾再写入,新内容和原文件末尾之间会产生空洞(0x00 字节)
大小
使用 size() 方法获取文件的大小(字节数):
long fileSize = channel.size();强制刷盘
操作系统出于性能考虑,会将数据先缓存在内存中,不会立刻写入磁盘。调用 force() 方法可以强制将数据刷入磁盘:
// true 表示同时刷新文件内容和元数据(权限、修改时间等)
// false 表示只刷新文件内容
channel.force(true);文件截断
使用 truncate() 方法可以将文件截断到指定大小:
// 将文件截断为 1024 字节,超出部分会被丢弃
channel.truncate(1024);零拷贝传输
FileChannel 提供了两个高效的零拷贝方法:
transferTo - 从当前 Channel 传输到目标 Channel
// 效率高,底层利用操作系统的零拷贝机制
// 适合文件到网络的传输
try (FileChannel from = FileChannel.open(Paths.get("source.txt"), StandardOpenOption.READ);
SocketChannel to = SocketChannel.open()) {
long position = 0;
long count = from.size();
// 最多传输 2GB,超过需要多次调用
from.transferTo(position, count, to);
}transferFrom - 从源 Channel 传输到当前 Channel
try (FileChannel to = FileChannel.open(Paths.get("dest.txt"),
StandardOpenOption.WRITE, StandardOpenOption.CREATE);
FileChannel from = FileChannel.open(Paths.get("source.txt"), StandardOpenOption.READ)) {
to.transferFrom(from, 0, from.size());
}TIP
transferTo/transferFrom 利用 DMA(直接内存访问),数据直接在内核空间传输,避免了用户态和内核态之间的数据拷贝,效率远高于传统的 read/write 循环。
Path 与 Paths
java.nio.file.Path 是 JDK 7 引入的接口,用于表示文件系统中的路径。Paths 是用于创建 Path 实例的工具类。
创建 Path
// 使用 Paths.get()
Path path1 = Paths.get("data.txt");
Path path2 = Paths.get("/Users", "hyj", "data.txt");
// JDK 11+ 推荐使用 Path.of()
Path path3 = Path.of("data.txt");
Path path4 = Path.of("/Users", "hyj", "data.txt");
// 从 URI 创建
Path path5 = Paths.get(URI.create("file:///Users/hyj/data.txt"));其中Paths.get("data.txt")是相对路径, 使用 user.dir 环境变量来定位,表示当前工作目录。
- 如果你在 IDE(如 IntelliJ IDEA 或 Eclipse)中运行: 默认的工作目录通常是你的项目根目录(Project Root)。
- 如果你在命令行运行: 工作目录就是你输入运行命令时所在的那个文件夹。
你可以通过这行代码打印出当前程序实际运行的基准路径:System.out.println(System.getProperty("user.dir"));
路径解析
- resolve (路径拼接)
逻辑: path1.resolve(path2)。如果 path2 是相对路径,它会接在 path1 后面;如果 path2 是绝对路径,则直接返回 path2(因为绝对路径已经到头了,没法再拼)。
resolveSibling (替换最后一级) 逻辑: 它是先找到当前路径的 getParent()(父目录),然后再执行 resolve。它常用于在同一个目录下更改文件名。
relativize (计算相对路径) 逻辑: 计算从 path1 出发,经过怎样的移动(如 .. 回退或进入子目录)能到达 path2。
限制:两个路径必须是同一类型的(要么都是相对路径,要么都是绝对路径),否则会抛出异常。
WARNING
这些方法都不会检查文件是否存在。它们只是单纯的字符串逻辑计算。即便你 resolve 出来的路径在硬盘上根本不存在,Java 也不会报错,直到你真正尝试用 Files.read() 等方法去读取它。
Path base = Paths.get("/Users/hyj");
// resolve - 拼接路径
Path resolved = base.resolve("Documents/data.txt");
// 结果: /Users/hyj/Documents/data.txt
// resolveSibling - 替换最后一级
Path sibling = Paths.get("/Users/hyj/old.txt").resolveSibling("new.txt");
// 结果: /Users/hyj/new.txt
// relativize - 计算相对路径
Path from = Paths.get("/Users/hyj");
Path to = Paths.get("/Users/hyj/Documents/data.txt");
Path relative = from.relativize(to);
// 结果: Documents/data.txt路径规范化
// normalize - 消除 . 和 ..
Path messy = Paths.get("/Users/hyj/../hyj/./Documents");
Path clean = messy.normalize();
// 结果: /Users/hyj/Documents
// toAbsolutePath - 转为绝对路径
Path absolute = Paths.get("data.txt").toAbsolutePath();
// toRealPath - 解析符号链接,返回真实路径(文件必须存在)
Path real = Paths.get("link.txt").toRealPath();路径信息
Path path = Paths.get("/Users/hyj/Documents/data.txt");
path.getFileName(); // data.txt
path.getParent(); // /Users/hyj/Documents
path.getRoot(); // /
path.getNameCount(); // 4 (Users, hyj, Documents, data.txt)
path.getName(0); // Users
path.getName(3); // data.txt
path.isAbsolute(); // true
// 迭代路径组件
for (Path component : path) {
System.out.println(component);
}
// 输出: Users, hyj, Documents, data.txt其中getRoot()方法,对于UNIX / Linux / macOS,整个系统只有一个根,getRoot() 总是返回:/。对于Windows,每个分区(盘符)都是一个独立的根,如果路径是 C:\Users\data.txt,getRoot() 返回:C:\;如果路径是网络共享路径(UNC),如 \\Server\Share\file.txt,getRoot() 返回:\\Server\Share\。
路径比较
Path p1 = Paths.get("/Users/hyj/data.txt");
Path p2 = Paths.get("/Users/hyj/data.txt");
Path p3 = Paths.get("/Users/hyj/other.txt");
p1.equals(p2); // true
p1.compareTo(p3); // 字典序比较
p1.startsWith("/Users"); // true
p1.endsWith("data.txt"); // trueFiles 工具类
java.nio.file.Files 是 JDK 7 引入的文件操作工具类,提供了大量静态方法简化文件操作。
基本操作
Path path = Paths.get("data.txt");
// 判断文件是否存在
boolean exists = Files.exists(path);
// 创建文件
Files.createFile(path);
// 创建目录(父目录必须存在)
Files.createDirectory(Paths.get("mydir"));
// 创建多级目录
Files.createDirectories(Paths.get("parent/child/grandchild"));
// 删除文件或空目录
Files.delete(path);
// 删除(如果存在)
Files.deleteIfExists(path);
// 复制文件
Files.copy(Paths.get("source.txt"), Paths.get("dest.txt"),
StandardCopyOption.REPLACE_EXISTING);
// 移动/重命名文件
Files.move(Paths.get("old.txt"), Paths.get("new.txt"),
StandardCopyOption.ATOMIC_MOVE);读写文件
Path path = Paths.get("data.txt");
// 读取所有字节
byte[] bytes = Files.readAllBytes(path);
// 读取所有行
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
// 写入字节
Files.write(path, "Hello".getBytes(StandardCharsets.UTF_8));
// 写入所有行
Files.write(path, List.of("line1", "line2"), StandardCharsets.UTF_8);
// 追加写入
Files.write(path, "append".getBytes(), StandardOpenOption.APPEND);遍历目录
1. Files.newDirectoryStream - 浅层扫描
只遍历当前目录下的直接子文件和子文件夹,不会进入子目录内部。非递归。
// 遍历目录下的直接子项
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get("."))) {
for (Path entry : stream) {
System.out.println(entry.getFileName());
}
}支持 glob 模式过滤:
// 只遍历 .java 文件
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get("."), "*.java")) {
for (Path entry : stream) {
System.out.println(entry);
}
}
// 匹配多种文件类型
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get("."), "*.{java,xml,properties}")) {
for (Path entry : stream) {
System.out.println(entry);
}
}TIP
DirectoryStream 实现了 Closeable,务必使用 try-with-resources 或手动关闭,否则会导致文件句柄泄露。
2. Files.walkFileTree - 深度遍历(访问者模式)
递归遍历目录树,采用访问者模式,可以在进入/离开目录、访问文件等时机执行自定义逻辑。
Files.walkFileTree(Paths.get("."), new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("进入目录: " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("访问文件: " + file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
System.out.println("离开目录: " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("访问失败: " + file + " - " + exc.getMessage());
return FileVisitResult.CONTINUE; // 继续遍历,不因单个文件失败而中断
}
});FileVisitResult 枚举值:
| 值 | 含义 |
|---|---|
CONTINUE | 继续遍历 |
TERMINATE | 立即终止整个遍历 |
SKIP_SUBTREE | 跳过当前目录的子树(仅在 preVisitDirectory 中有效) |
SKIP_SIBLINGS | 跳过当前目录的后续兄弟节点 |
实战:递归删除目录
Path dirToDelete = Paths.get("temp");
Files.walkFileTree(dirToDelete, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file); // 先删文件
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir); // 后删空目录
return FileVisitResult.CONTINUE;
}
});3. Files.walk - 深度遍历(Stream API)
JDK 8+ 引入,返回 Stream<Path>,更适合函数式编程风格。默认深度优先遍历。
// 遍历所有文件和目录
try (Stream<Path> paths = Files.walk(Paths.get("."))) {
paths.forEach(System.out::println);
}
// 只处理普通文件
try (Stream<Path> paths = Files.walk(Paths.get("."))) {
paths.filter(Files::isRegularFile)
.forEach(System.out::println);
}
// 查找所有 .java 文件
try (Stream<Path> paths = Files.walk(Paths.get("src"))) {
List<Path> javaFiles = paths
.filter(p -> p.toString().endsWith(".java"))
.toList();
}
// 限制遍历深度(第二个参数)
try (Stream<Path> paths = Files.walk(Paths.get("."), 2)) {
paths.forEach(System.out::println);
}IMPORTANT
Files.walk() 返回的 Stream 必须关闭。务必使用 try-with-resources,否则会导致资源泄露。
4. Files.find - 条件搜索
JDK 8+ 提供,比 walk + filter 更高效,因为过滤发生在遍历过程中。
// 查找 src 目录下所有大于 1KB 的 .java 文件,最大深度 10
try (Stream<Path> paths = Files.find(Paths.get("src"), 10,
(path, attrs) -> attrs.isRegularFile()
&& path.toString().endsWith(".java")
&& attrs.size() > 1024)) {
paths.forEach(System.out::println);
}三种遍历方式对比
| 方式 | 递归 | 风格 | 适用场景 |
|---|---|---|---|
newDirectoryStream | ❌ | 迭代器 | 单层目录遍历,支持 glob 过滤 |
walkFileTree | ✅ | 访问者模式 | 需要精细控制(删除、复制目录树) |
walk | ✅ | Stream API | 函数式过滤和收集 |
find | ✅ | Stream API | 按条件搜索,性能更优 |
文件属性
Path path = Paths.get("data.txt");
// 获取基本属性
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
System.out.println("Size: " + attrs.size());
System.out.println("Created: " + attrs.creationTime());
System.out.println("Modified: " + attrs.lastModifiedTime());
System.out.println("Is Directory: " + attrs.isDirectory());
// 判断方法
Files.isReadable(path);
Files.isWritable(path);
Files.isExecutable(path);
Files.isHidden(path);
Files.isSymbolicLink(path);总结
| 场景 | 推荐方案 |
|---|---|
| 简单文件读写 | Files 工具类 |
| 大文件处理 | FileChannel + ByteBuffer |
| 文件到网络传输 | FileChannel.transferTo() |
| 需要随机访问 | FileChannel + position() |
| 需要内存映射 | FileChannel.map() |
