💡 摘要:你是否曾需要递归遍历目录结构?是否想知道Java NIO相比传统IO有什么优势?是否对Path、Files这些新API感到好奇?
别担心,文件操作和NIO是Java IO编程的重要进阶内容,能让你更高效地处理文件和网络IO。
本文将带你从文件操作的高级API讲起,学习Files类的各种实用方法。然后深入NIO的核心概念,理解Channel、Buffer、Selector的工作原理。
接着通过实战案例展示NIO在文件读写、目录遍历、非阻塞IO等方面的强大能力。最后对比传统IO与NIO的性能差异。从基础用法到高级特性,从同步阻塞到异步非阻塞,让你全面掌握Java现代IO编程技术。文末附性能测试和面试高频问题,助你写出更高效的IO代码。
一、Files类:现代文件操作API
1. Files类概述
Java 7引入的Files类提供了丰富的静态方法,用于文件操作,比传统的File类更强大、更易用。
基本文件操作:
java
import java.nio.file.*;
import java.io.IOException;
import java.util.List;
public class FilesExample {
public static void main(String[] args) {
Path path = Paths.get("test.txt");
// 检查文件是否存在
boolean exists = Files.exists(path);
System.out.println("文件存在: " + exists);
// 检查是否是目录
boolean isDirectory = Files.isDirectory(path);
System.out.println("是目录: " + isDirectory);
// 获取文件大小
try {
long size = Files.size(path);
System.out.println("文件大小: " + size + " bytes");
} catch (IOException e) {
System.err.println("无法获取文件大小: " + e.getMessage());
}
}
}
2. 文件读写操作
读写文本文件:
java
// 写入文件
Path outputPath = Paths.get("output.txt");
try {
String content = "Hello, Files API!\n这是第二行";
Files.write(outputPath, content.getBytes(StandardCharsets.UTF_8));
System.out.println("文件写入成功");
} catch (IOException e) {
e.printStackTrace();
}
// 读取文件
try {
List<String> lines = Files.readAllLines(outputPath, StandardCharsets.UTF_8);
for (String line : lines) {
System.out.println("行内容: " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
// 追加内容
try {
String appendText = "\n追加的内容";
Files.write(outputPath, appendText.getBytes(StandardCharsets.UTF_8),
StandardOpenOption.APPEND);
} catch (IOException e) {
e.printStackTrace();
}
二进制文件操作:
java
// 读写二进制文件
Path binaryPath = Paths.get("data.bin");
try {
// 写入二进制数据
byte[] data = {0x48, 0x65, 0x6C, 0x6C, 0x6F}; // "Hello"的字节表示
Files.write(binaryPath, data);
// 读取二进制数据
byte[] readData = Files.readAllBytes(binaryPath);
System.out.println("读取的数据: " + new String(readData));
} catch (IOException e) {
e.printStackTrace();
}
3. 文件与目录管理
创建和删除操作:
java
// 创建目录
Path dirPath = Paths.get("mydir/subdir");
try {
Files.createDirectories(dirPath); // 创建多级目录
System.out.println("目录创建成功");
} catch (IOException e) {
e.printStackTrace();
}
// 创建临时文件
try {
Path tempFile = Files.createTempFile("prefix_", ".txt");
System.out.println("临时文件: " + tempFile);
// 临时目录
Path tempDir = Files.createTempDirectory("temp_");
System.out.println("临时目录: " + tempDir);
} catch (IOException e) {
e.printStackTrace();
}
// 删除文件/目录
try {
Files.deleteIfExists(Paths.get("file_to_delete.txt"));
System.out.println("文件删除成功");
} catch (IOException e) {
e.printStackTrace();
}
文件移动和复制:
java
Path source = Paths.get("source.txt");
Path target = Paths.get("target.txt");
try {
// 复制文件
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
System.out.println("文件复制成功");
// 移动文件(重命名)
Path newLocation = Paths.get("backup/" + target.getFileName());
Files.createDirectories(newLocation.getParent());
Files.move(target, newLocation, StandardCopyOption.REPLACE_EXISTING);
System.out.println("文件移动成功");
} catch (IOException e) {
e.printStackTrace();
}
二、目录遍历与文件查找
1. 递归遍历目录
使用Files.walk():
java
Path startPath = Paths.get(".");
try {
Files.walk(startPath)
.filter(Files::isRegularFile) // 只处理普通文件
.filter(path -> path.toString().endsWith(".java")) // 过滤Java文件
.forEach(path -> {
try {
System.out.println("Java文件: " + path +
" 大小: " + Files.size(path) + " bytes");
} catch (IOException e) {
System.err.println("无法获取文件大小: " + path);
}
});
} catch (IOException e) {
e.printStackTrace();
}
使用Files.list():
java
Path dirPath = Paths.get("src/main/java");
try {
Files.list(dirPath)
.filter(Files::isDirectory)
.forEach(subDir -> System.out.println("子目录: " + subDir.getFileName()));
} catch (IOException e) {
e.printStackTrace();
}
2. 文件查找
使用Files.find():
java
Path searchPath = Paths.get(".");
try {
// 查找最近7天内修改过的.java文件
Files.find(searchPath, 10, (path, attrs) -> {
return path.toString().endsWith(".java") &&
attrs.lastModifiedTime().toMillis() >
System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1000L;
}).forEach(path -> System.out.println("最近修改: " + path));
} catch (IOException e) {
e.printStackTrace();
}
三、NIO核心概念入门
1. NIO与传统IO的区别
主要区别对比:
特性 | 传统IO | NIO |
数据流 | 面向流(Stream Oriented) | 面向缓冲区(Buffer Oriented) |
阻塞模式 | 阻塞IO(Blocking IO) | 非阻塞IO(Non-blocking IO) |
选择器 | 无 | 有(Selector) |
性能 | 连接数多时性能差 | 连接数多时性能好 |
适用场景 | 连接数少,数据量大 | 连接数多,数据量小 |
2. Buffer:数据容器
Buffer的基本用法:
java
import java.nio.*;
public class BufferExample {
public static void main(String[] args) {
// 创建ByteBuffer,容量为10字节
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println("初始状态: " + buffer);
// capacity:10, position:0, limit:10
// 写入数据
buffer.put((byte)'H');
buffer.put((byte)'e');
buffer.put((byte)'l');
buffer.put((byte)'l');
buffer.put((byte)'o');
System.out.println("写入5字节后: " + buffer);
// capacity:10, position:5, limit:10
// 切换为读模式
buffer.flip();
System.out.println("flip之后: " + buffer);
// capacity:10, position:0, limit:5
// 读取数据
while (buffer.hasRemaining()) {
byte b = buffer.get();
System.out.print((char) b);
}
System.out.println();
System.out.println("读取完成后: " + buffer);
// capacity:10, position:5, limit:5
// 清空缓冲区(为写入做准备)
buffer.clear();
System.out.println("clear之后: " + buffer);
// capacity:10, position:0, limit:10
}
}
Buffer的三种模式:
- 写模式:position指向下一个写入位置,limit等于capacity
- 读模式:调用flip()后,position重置为0,limit指向最后一个元素后一位
- 清空模式:调用clear()后,position重置为0,limit等于capacity
3. Channel:数据通道
FileChannel文件操作:
java
public class FileChannelExample {
public static void main(String[] args) {
Path path = Paths.get("test.txt");
// 使用FileChannel写入文件
try (FileChannel channel = FileChannel.open(path,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello, FileChannel!".getBytes(StandardCharsets.UTF_8));
buffer.flip(); // 切换为读模式
while (buffer.hasRemaining()) {
channel.write(buffer); // 写入文件
}
System.out.println("文件写入完成");
} catch (IOException e) {
e.printStackTrace();
}
// 使用FileChannel读取文件
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
buffer.flip(); // 切换为读模式
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("文件内容: " + new String(data, StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
}
文件复制优化:
java
public class FileCopyNIO {
public static void copyFile(String source, String target) {
Path sourcePath = Paths.get(source);
Path targetPath = Paths.get(target);
try (FileChannel sourceChannel = FileChannel.open(sourcePath, StandardOpenOption.READ);
FileChannel targetChannel = FileChannel.open(targetPath,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE)) {
// 使用transferTo进行高效的文件复制
long transferred = sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
System.out.println("复制字节数: " + transferred);
// 或者使用transferFrom
// long transferred = targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
} catch (IOException e) {
System.err.println("文件复制失败: " + e.getMessage());
}
}
public static void main(String[] args) {
copyFile("source.jpg", "target.jpg");
}
}
4. 内存映射文件
内存映射文件操作:
java
public class MappedFileExample {
public static void main(String[] args) {
Path path = Paths.get("mapped_file.txt");
try (FileChannel channel = FileChannel.open(path,
StandardOpenOption.CREATE,
StandardOpenOption.READ,
StandardOpenOption.WRITE)) {
// 创建内存映射缓冲区
MappedByteBuffer mappedBuffer = channel.map(
FileChannel.MapMode.READ_WRITE, 0, 1024 * 1024); // 映射1MB
// 写入数据
String message = "Hello, Memory Mapped File!";
mappedBuffer.put(message.getBytes(StandardCharsets.UTF_8));
// 读取数据
mappedBuffer.flip();
byte[] data = new byte[mappedBuffer.remaining()];
mappedBuffer.get(data);
System.out.println("读取内容: " + new String(data, StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
}
四、NIO文件操作实战
1. 大文件处理
分块读取大文件:
java
public class LargeFileProcessor {
public static void processLargeFile(String filePath, int bufferSize) {
Path path = Paths.get(filePath);
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
long fileSize = channel.size();
long position = 0;
while (position < fileSize) {
int bytesRead = channel.read(buffer, position);
if (bytesRead == -1) break;
buffer.flip();
processBuffer(buffer, bytesRead);
buffer.clear();
position += bytesRead;
System.out.printf("处理进度: %.2f%%\n",
(double)position / fileSize * 100);
}
} catch (IOException e) {
System.err.println("处理文件失败: " + e.getMessage());
}
}
private static void processBuffer(ByteBuffer buffer, int bytesRead) {
// 处理缓冲区数据
byte[] data = new byte[bytesRead];
buffer.get(data);
// 这里可以添加具体的处理逻辑
System.out.println("处理了 " + bytesRead + " 字节数据");
}
public static void main(String[] args) {
processLargeFile("large_file.dat", 8192); // 8KB缓冲区
}
}
2. 文件锁
文件锁机制:
java
public class FileLockExample {
public static void main(String[] args) {
Path path = Paths.get("locked_file.txt");
try (FileChannel channel = FileChannel.open(path,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE)) {
// 获取排他锁
FileLock lock = channel.lock();
System.out.println("获得文件锁");
try {
// 执行需要加锁的操作
ByteBuffer buffer = ByteBuffer.wrap("Locked content".getBytes());
channel.write(buffer);
// 模拟耗时操作
Thread.sleep(3000);
} finally {
lock.release();
System.out.println("释放文件锁");
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
五、性能对比:NIO vs 传统IO
1. 文件复制性能测试
java
public class PerformanceComparison {
public static void main(String[] args) {
String source = "large_file.iso"; // 1GB+的大文件
String target1 = "copy_io.iso";
String target2 = "copy_nio.iso";
// 传统IO性能测试
long startIO = System.currentTimeMillis();
copyFileIO(source, target1);
long timeIO = System.currentTimeMillis() - startIO;
// NIO性能测试
long startNIO = System.currentTimeMillis();
copyFileNIO(source, target2);
long timeNIO = System.currentTimeMillis() - startNIO;
System.out.printf("传统IO耗时: %d ms\n", timeIO);
System.out.printf("NIO耗时: %d ms\n", timeNIO);
System.out.printf("性能提升: %.2f%%\n",
(double)(timeIO - timeNIO) / timeIO * 100);
}
// 传统IO文件复制
private static void copyFileIO(String source, String target) {
try (InputStream is = new FileInputStream(source);
OutputStream os = new FileOutputStream(target)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
}
// NIO文件复制
private static void copyFileNIO(String source, String target) {
try (FileChannel sourceChannel = new FileInputStream(source).getChannel();
FileChannel targetChannel = new FileOutputStream(target).getChannel()) {
sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
} catch (IOException e) {
e.printStackTrace();
}
}
}
2. 内存映射文件性能优势
随机访问性能测试:
java
public class RandomAccessPerformance {
public static void main(String[] args) {
Path path = Paths.get("random_access_test.bin");
int fileSize = 100 * 1024 * 1024; // 100MB
int accessCount = 10000;
// 准备测试文件
createTestFile(path, fileSize);
// 传统RandomAccessFile测试
long startRAF = System.currentTimeMillis();
testRandomAccessFile(path, accessCount);
long timeRAF = System.currentTimeMillis() - startRAF;
// 内存映射文件测试
long startMapped = System.currentTimeMillis();
testMappedFile(path, accessCount);
long timeMapped = System.currentTimeMillis() - startMapped;
System.out.printf("RandomAccessFile耗时: %d ms\n", timeRAF);
System.out.printf("内存映射文件耗时: %d ms\n", timeMapped);
System.out.printf("性能提升: %.2f%%\n",
(double)(timeRAF - timeMapped) / timeRAF * 100);
}
private static void createTestFile(Path path, int size) {
try (FileChannel channel = FileChannel.open(path,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE)) {
ByteBuffer buffer = ByteBuffer.allocate(size);
for (int i = 0; i < size; i++) {
buffer.put((byte)(i % 256));
}
buffer.flip();
channel.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
private static void testRandomAccessFile(Path path, int accessCount) {
try (RandomAccessFile raf = new RandomAccessFile(path.toFile(), "r")) {
Random random = new Random();
for (int i = 0; i < accessCount; i++) {
long position = random.nextInt((int) raf.length());
raf.seek(position);
raf.readByte();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void testMappedFile(Path path, int accessCount) {
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
MappedByteBuffer mappedBuffer = channel.map(
FileChannel.MapMode.READ_ONLY, 0, channel.size());
Random random = new Random();
for (int i = 0; i < accessCount; i++) {
int position = random.nextInt(mappedBuffer.capacity());
mappedBuffer.get(position);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
六、总结:NIO最佳实践
1. 选择指南
使用传统IO的场景:
- ✅ 简单的文件读写操作
- ✅ 小文件处理
- ✅ 需要兼容老代码
- ✅ 简单的网络通信
使用NIO的场景:
- ✅ 大文件处理(内存映射文件)
- ✅ 高性能文件复制(transferTo/transferFrom)
- ✅ 非阻塞网络编程
- ✅ 需要文件锁机制
- ✅ 随机访问大文件
2. 性能优化建议
缓冲区大小选择:
java
// 合适的缓冲区大小(根据具体场景调整)
int bufferSize = 8192; // 8KB - 通用场景
int largeBufferSize = 65536; // 64KB - 大文件处理
int networkBufferSize = 4096; // 4KB - 网络传输
内存映射文件使用:
- ✅ 处理超大文件(超过内存容量)
- ✅ 随机访问频繁的场景
- ✅ 进程间共享数据
- ❌ 小文件(开销可能不划算)
七、面试高频问题
❓1. NIO和传统IO的主要区别是什么?
答:主要区别:NIO面向缓冲区、支持非阻塞IO、有选择器机制,适合高并发场景;传统IO面向流、阻塞式,适合连接数少的场景。
❓2. Buffer的flip()方法有什么作用?
答:flip()方法将Buffer从写模式切换到读模式,将limit设置为当前位置,position重置为0。
❓3. 内存映射文件有什么优势?
答:内存映射文件将文件直接映射到内存空间,避免了用户态和内核态的数据拷贝,提高了大文件随机访问的性能。
❓4. FileChannel的transferTo()有什么优势?
答:transferTo()方法在很多操作系统上会使用零拷贝技术,避免了数据在用户态和内核态之间的多次拷贝,大幅提高文件复制性能。
❓5. 什么时候使用Files类而不是传统File类?
答:Files类提供了更丰富、更易用的API,支持更多的文件操作,并且通常有更好的性能。在新代码中推荐使用Files类。