Java 文件与 I/O 全面指南:传统 IO 与 NIO.2 深度解析(含完整实例)
✅ 标注说明 :
🔥 = 最常用 | 💡 = 推荐实践 | ⚠️ = 注意事项
一、java.io.File ------ 路径与元数据操作(基础但局限)
🔥 核心实例:文件/目录管理
java
import java.io.File;
import java.io.IOException;
public class FileDemo {
public static void main(String[] args) throws IOException {
// 创建文件对象(相对路径)
File file = new File("data/sample.txt");
// 🔥 创建多级目录(父目录不存在时)
file.getParentFile().mkdirs(); // 等价于 mkdirs()
// 🔥 创建新文件(若不存在)
if (file.createNewFile()) {
System.out.println("文件创建成功: " + file.getAbsolutePath());
}
// 属性查询
System.out.println("是否存在: " + file.exists());
System.out.println("是否为文件: " + file.isFile());
System.out.println("大小(字节): " + file.length());
System.out.println("最后修改时间: " + new java.util.Date(file.lastModified()));
// 🔥 列出目录内容
File dir = new File("data");
if (dir.isDirectory()) {
for (File f : dir.listFiles()) {
System.out.println(f.getName() + (f.isDirectory() ? " [DIR]" : ""));
}
}
// 删除文件(谨慎!)
// file.delete();
}
}
⚠️ 局限:
- 无法指定字符编码
- 异常信息模糊(如
mkdir()失败不说明原因)
✅ 现代替代 :NIO.2(见第三部分)
二、传统 IO 流体系(字节流 + 字符流)
🌐 流分类全景图
方向 → 输入流 (读) 输出流 (写)
│ │
字节流 → InputStream OutputStream
│ │
字符流 → Reader Writer
│ │
缓冲流 → Buffered... Buffered...
│ │
功能流 → Object/Data/Print Object/Data/Print
🔥 1. 字节流(处理二进制数据:图片、音频、zip等)
| 类 | 用途 | 实例 |
|---|---|---|
FileInputStream 🔥 |
从文件读字节 | new FileInputStream("img.jpg") |
FileOutputStream 🔥 |
向文件写字节 | new FileOutputStream("copy.jpg") |
ByteArrayInputStream |
从字节数组读 | new ByteArrayInputStream(bytes) |
ByteArrayOutputStream |
写入字节数组 | new ByteArrayOutputStream() |
BufferedInputStream 🔥 |
带缓冲读 | new BufferedInputStream(new FileInputStream(...)) |
BufferedOutputStream 🔥 |
带缓冲写 | new BufferedOutputStream(new FileOutputStream(...)) |
ObjectInputStream 🔥 |
反序列化对象 | new ObjectInputStream(new FileInputStream("obj.ser")) |
ObjectOutputStream 🔥 |
序列化对象 | new ObjectOutputStream(new FileOutputStream("obj.ser")) |
DataInputStream |
读基本类型 | dis.readInt() |
DataOutputStream |
写基本类型 | dos.writeDouble(3.14) |
💡 实例1:高效复制大文件(带缓冲)
java
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("source.zip"));
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("target.zip"))) {
byte[] buffer = new byte[8192]; // 8KB缓冲区
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
System.out.println("复制完成!");
} // 自动关闭资源(try-with-resources)
💡 实例2:对象序列化(保存用户状态)
java
// 定义可序列化类
class User implements java.io.Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient String password; // transient字段不序列化
// 构造方法、getter/setter...
}
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("user.dat"))) {
oos.writeObject(new User("张三", "123456"));
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("user.dat"))) {
User user = (User) ois.readObject();
System.out.println("加载用户: " + user.getName()); // password为null
}
🔥 2. 字符流(专为文本设计,注意编码!)
| 类 | 用途 | 实例 |
|---|---|---|
InputStreamReader 🔥 |
字节流转字符流(可指定编码) | new InputStreamReader(fis, StandardCharsets.UTF_8) |
OutputStreamWriter 🔥 |
字符流转字节流(可指定编码) | new OutputStreamWriter(fos, StandardCharsets.UTF_8) |
BufferedReader 🔥 |
带缓冲读文本(支持readLine) | new BufferedReader(new InputStreamReader(...)) |
BufferedWriter 🔥 |
带缓冲写文本 | new BufferedWriter(new OutputStreamWriter(...)) |
FileReader |
读文本(不推荐! 默认编码) | new FileReader("text.txt") ⚠️ |
FileWriter |
写文本(不推荐! 默认编码) | new FileWriter("text.txt") ⚠️ |
StringReader |
从字符串读 | new StringReader("Hello") |
StringWriter |
写入字符串 | new StringWriter() |
💡 实例1:安全读取UTF-8文本(推荐写法)
java
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("article.txt"),
StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
💡 实例2:写入带换行的文本(指定编码)
java
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("output.txt", true), // true=追加
StandardCharsets.UTF_8))) {
writer.write("第一行");
writer.newLine(); // 跨平台换行
writer.write("第二行");
writer.flush(); // 刷新缓冲区(try-with-resources会自动flush)
}
💡 实例3:统计文本行数
java
try (BufferedReader br = Files.newBufferedReader(
Paths.get("log.txt"), StandardCharsets.UTF_8)) {
long lineCount = br.lines().count(); // Java 8+ Stream API
System.out.println("总行数: " + lineCount);
}
三、NIO.2(java.nio.file)------ 现代文件操作(JDK 7+)
✅ 核心优势:
- 精确异常(
NoSuchFileException,AccessDeniedException)- 符号链接支持
- 高效文件操作(
copy,move)- 目录遍历、文件监听等高级功能
🔥 1. Path 与 Paths(路径表示)
| 操作 | 传统 IO (File) |
NIO.2 (Path) |
|---|---|---|
| 创建路径 | new File("a/b/c.txt") |
Paths.get("a", "b", "c.txt") |
| 绝对路径 | file.getAbsolutePath() |
path.toAbsolutePath() |
| 规范路径 | file.getCanonicalPath() |
path.toRealPath() |
| 父路径 | file.getParentFile() |
path.getParent() |
| 文件名 | file.getName() |
path.getFileName() |
💡 Path 实例
java
Path path = Paths.get("docs", "report.pdf");
System.out.println("绝对路径: " + path.toAbsolutePath());
System.out.println("父目录: " + path.getParent());
System.out.println("文件名: " + path.getFileName());
// 路径解析(相对路径转绝对)
Path base = Paths.get("/home/user");
Path resolved = base.resolve("project/data.txt"); // /home/user/project/data.txt
// 路径归一化(处理 ../)
Path p = Paths.get("/a/b/../c");
System.out.println(p.normalize()); // /a/c
🔥 2. Files 工具类(核心操作集)
| 操作 | 传统 IO | NIO.2(推荐) |
|---|---|---|
| 判断存在 | file.exists() |
Files.exists(path) |
| 创建文件 | file.createNewFile() |
Files.createFile(path) |
| 创建目录 | file.mkdirs() |
Files.createDirectories(path) 🔥 |
| 删除 | file.delete() |
Files.delete(path) / deleteIfExists |
| 复制 | 手动循环 | Files.copy(src, dst, REPLACE_EXISTING) 🔥 |
| 移动/重命名 | file.renameTo() |
Files.move(src, dst, REPLACE_EXISTING) 🔥 |
| 读所有行 | 手动 BufferedReader | Files.readAllLines(path, UTF_8) 🔥 |
| 写所有行 | 手动 BufferedWriter | Files.write(path, lines, UTF_8) 🔥 |
| 读字节数组 | 手动读取 | Files.readAllBytes(path) 🔥 |
| 写字节数组 | 手动写入 | Files.write(path, bytes) 🔥 |
💡 Files 实例集锦
java
Path src = Paths.get("input.txt");
Path dst = Paths.get("backup.txt");
Path dir = Paths.get("logs/2024/06");
// 🔥 创建多级目录(自动创建父目录)
Files.createDirectories(dir);
// 🔥 复制文件(带选项)
Files.copy(src, dst,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES); // 保留属性
// 🔥 读取所有文本行(指定UTF-8)
List<String> lines = Files.readAllLines(src, StandardCharsets.UTF_8);
lines.forEach(System.out::println);
// 🔥 写入文本(覆盖模式)
Files.write(Paths.get("output.txt"),
Arrays.asList("Line 1", "Line 2"),
StandardCharsets.UTF_8,
StandardOpenOption.CREATE, // 不存在则创建
StandardOpenOption.TRUNCATE_EXISTING); // 覆盖
// 🔥 读取二进制文件为字节数组
byte[] imgBytes = Files.readAllBytes(Paths.get("logo.png"));
// 🔥 检查文件属性
if (Files.isReadable(src) && Files.isRegularFile(src)) {
System.out.println("可读普通文件");
}
if (Files.isSymbolicLink(Paths.get("link"))) {
System.out.println("是符号链接");
}
🔥 3. 目录遍历:Files.walk() 与 FileVisitor
💡 实例:递归查找所有 .java 文件
java
Path startDir = Paths.get("src");
try (Stream<Path> stream = Files.walk(startDir, 3)) { // 最大深度3
stream
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
💡 实例:使用 FileVisitor 自定义遍历逻辑
java
Files.walkFileTree(Paths.get("project"), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.toString().endsWith(".log")) {
System.out.println("发现日志文件: " + file);
// Files.delete(file); // 可安全删除
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("访问失败: " + file);
return FileVisitResult.CONTINUE;
}
});
🔥 4. 文件监听:WatchService(监控目录变化)
💡 实例:监听目录新增/修改/删除事件
java
WatchService watcher = FileSystems.getDefault().newWatchService();
Path dir = Paths.get("monitor_dir");
dir.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE);
System.out.println("开始监听 " + dir + " ... (按Ctrl+C停止)");
while (true) {
WatchKey key = watcher.take(); // 阻塞等待事件
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
Path fileName = (Path) event.context();
System.out.printf("[%s] %s%n",
kind.name().substring(11), // 去掉"ENTRY_"
fileName);
}
key.reset(); // 重置key,继续监听
}
💡 应用场景:热部署、日志监控、配置文件热更新
四、传统 IO 与 NIO.2 对比总结
| 场景 | 传统 IO | NIO.2 | 推荐 |
|---|---|---|---|
| 路径操作 | File |
Path + Paths |
✅ NIO.2 |
| 文件属性查询 | File 方法 |
Files + Path |
✅ NIO.2 |
| 创建/删除/复制 | 手动实现 | Files.create*, copy, delete |
✅ NIO.2 |
| 读写小文本文件 | BufferedReader |
Files.readAllLines / write |
✅ NIO.2 |
| 流式读写大文件 | BufferedInputStream |
Files.newInputStream + 缓冲 |
⚖️ 两者皆可 |
| 对象序列化 | ObjectOutputStream |
无直接替代 | ✅ 传统 IO |
| 网络 I/O | Socket + 流 |
AsynchronousSocketChannel |
⚖️ 按需选择 |
| 目录遍历 | 递归 listFiles() |
Files.walk() / walkFileTree |
✅ NIO.2 |
| 文件监听 | 无 | WatchService |
✅ NIO.2 |
五、最佳实践黄金法则
-
路径与元数据操作 → 首选 NIO.2 (
Path,Files)java// ✅ 推荐 Files.createDirectories(path); // ❌ 避免 new File(pathStr).mkdirs(); -
文本文件读写 → NIO.2 + 显式编码
java// ✅ 推荐 Files.write(path, content, StandardCharsets.UTF_8); // ❌ 避免 new FileWriter(path); // 默认编码不可控! -
二进制流式处理 → 传统 IO + 缓冲 + try-with-resources
javatry (BufferedInputStream bis = new BufferedInputStream( Files.newInputStream(path))) { ... } -
资源管理 → 永远使用 try-with-resources
javatry (BufferedReader br = Files.newBufferedReader(path, UTF_8)) { ... } -
大文件处理 → 分块读取,避免 readAllBytes
javatry (InputStream in = Files.newInputStream(path)) { byte[] buf = new byte[8192]; while ((len = in.read(buf)) > 0) { ... } } -
异常处理 → 捕获具体异常(NIO.2优势)
javatry { Files.copy(src, dst); } catch (NoSuchFileException e) { System.err.println("源文件不存在"); } catch (AccessDeniedException e) { System.err.println("无权限"); }
六、终极选择指南
| 你的需求 | 推荐方案 |
|---|---|
| 创建目录、复制文件、检查属性 | Files.createDirectories(), Files.copy() 🔥 |
| 读取整个小文本文件 | Files.readAllLines(path, UTF_8) 🔥 |
| 写入整个小文本文件 | Files.write(path, lines, UTF_8) 🔥 |
| 逐行处理大文本文件 | Files.newBufferedReader(path, UTF_8) 🔥 |
| 复制大文件(二进制) | BufferedInputStream + BufferedOutputStream 🔥 |
| 保存/加载Java对象 | ObjectOutputStream / ObjectInputStream 🔥 |
| 递归查找文件 | Files.walk() 🔥 |
| 监听目录变化 | WatchService 🔥 |
| 网络流、Socket通信 | 传统 IO 流(InputStream/OutputStream) |
✅ 核心结论:
"路径操作用 NIO.2,流式读写用传统 IO(带缓冲),文本处理显式指定 UTF-8,资源管理必用 try-with-resources。"
传统 IO 与 NIO.2 不是替代关系,而是互补关系。掌握两者精髓,方能应对所有 I/O 场景。
✅ NIO.2 严格区分字节与字符操作,且设计更清晰、更安全!
核心结论 :
NIO.2 没有消除字节/字符概念 ,而是通过 API 设计强制分离 + 显式指定 Charset,彻底解决了传统 IO 中"混淆编码"的痛点。
一、NIO.2 如何区分字节与字符操作?
| 操作类型 | NIO.2 API | 返回类型 | 是否需指定 Charset | 本质 |
|---|---|---|---|---|
| 字节级操作 | Files.readAllBytes(path) |
byte[] |
❌ | 直接操作二进制 |
Files.write(path, byte[]) |
Path |
❌ | ||
Files.newInputStream(path) |
InputStream |
❌ | 传统字节流 | |
Files.newOutputStream(path) |
OutputStream |
❌ | ||
| 字符级操作 | Files.readAllLines(path, cs) |
List<String> |
✅ 必须 | 内部用 InputStreamReader |
Files.write(path, lines, cs) |
Path |
✅ 必须 | 内部用 OutputStreamWriter |
|
Files.newBufferedReader(path, cs) |
BufferedReader |
✅ 必须 | 字符流 | |
Files.newBufferedWriter(path, cs) |
BufferedWriter |
✅ 必须 |
🔑 关键设计 :
所有字符级 API 的 Charset 参数是 强制要求(非可选) ,编译器会报错!这从根本上杜绝了
FileReader/FileWriter的"默认编码陷阱"。
二、对比:传统 IO vs NIO.2 的编码处理
❌ 传统 IO 的隐患(FileReader/FileWriter)
java
// 危险!使用平台默认编码(Windows=GBK, Linux=UTF-8)
BufferedReader br = new BufferedReader(new FileReader("data.txt")); // 无 Charset 参数!
- 问题:跨平台运行时极易乱码
- 原因:API 设计缺陷(未强制要求指定编码)
✅ NIO.2 的安全设计
java
// 安全!Charset 是强制参数,编译器要求必须传
BufferedReader br = Files.newBufferedReader(
Paths.get("data.txt"),
StandardCharsets.UTF_8 // ✅ 显式指定,无歧义
);
- 优势:编译期强制规范,杜绝编码错误
- 本质 :NIO.2 的字符操作 内部仍使用传统字符流 (
InputStreamReader/OutputStreamWriter),但封装得更安全
三、NIO.2 字节/字符操作完整示例
🔥 字节级操作(处理二进制文件)
java
Path imgPath = Paths.get("logo.png");
// 读取为字节数组(无需 Charset)
byte[] bytes = Files.readAllBytes(imgPath);
// 写入字节数组(覆盖模式)
Files.write(Paths.get("copy.png"), bytes,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
// 获取 InputStream(用于流式处理)
try (InputStream in = Files.newInputStream(imgPath)) {
byte[] buf = new byte[8192];
while (in.read(buf) > 0) { /* 处理 */ }
}
🔥 字符级操作(处理文本文件)
java
Path txtPath = Paths.get("article.txt");
// 读取所有行(必须指定 Charset!)
List<String> lines = Files.readAllLines(txtPath, StandardCharsets.UTF_8);
// 逐行处理(Stream API)
try (Stream<String> stream = Files.lines(txtPath, StandardCharsets.UTF_8)) {
stream.filter(line -> !line.isBlank())
.map(String::trim)
.forEach(System.out::println);
}
// 写入文本(必须指定 Charset!)
Files.write(Paths.get("output.txt"),
Arrays.asList("第一行", "第二行"),
StandardCharsets.UTF_8, // ✅ 强制参数
StandardOpenOption.CREATE);
// 获取 BufferedWriter(必须指定 Charset!)
try (BufferedWriter bw = Files.newBufferedWriter(
Paths.get("log.txt"),
StandardCharsets.UTF_8, // ✅ 编译器强制要求
StandardOpenOption.APPEND)) {
bw.write("新增日志");
bw.newLine();
}
四、为什么有人误以为"NIO.2 不分字节字符"?
| 误解来源 | 真相 |
|---|---|
"NIO.2 用 Path 统一表示文件" |
Path 只是路径抽象,不涉及内容读写方式 |
"Files.copy() 既可复制文本也可复制图片" |
copy() 是字节级操作 (底层用 FileChannel),与内容类型无关 |
"NIO.2 没有 Reader/Writer 类" |
有! Files.newBufferedReader() 返回 BufferedReader(字符流) |
"NIO.2 用 ByteBuffer 处理所有数据" |
ByteBuffer 是 Channel 层概念,应用层仍需区分文本/二进制 |
💡 本质澄清 :
NIO.2 的
Files工具类 提供了两套清晰分离的 API:
- 字节 API → 操作
byte[]/InputStream/OutputStream- 字符 API → 操作
String/List<String>/Reader/Writer(强制 Charset)
五、终极对比表:字节 vs 字符操作在 NIO.2 中的体现
| 场景 | 应该用 | 为什么 |
|---|---|---|
| 复制图片/视频/zip | Files.copy(src, dst) 或 Files.readAllBytes() |
字节级操作,无需关心内容 |
| 读取配置文件(JSON/YAML) | Files.readString(path, UTF_8) (JDK 11+) |
字符操作,需指定编码 |
| 写日志(追加文本) | Files.newBufferedWriter(path, UTF_8, APPEND) |
字符流,强制编码安全 |
| 解析 CSV 文件 | Files.lines(path, UTF_8) |
Stream 处理文本行 |
| 上传文件到服务器 | Files.newInputStream(path) |
获取字节流供网络传输 |
✅ 总结:NIO.2 的设计哲学
| 特性 | 传统 IO (java.io) |
NIO.2 (java.nio.file) |
|---|---|---|
| 字节/字符分离 | 模糊(FileReader 隐式用默认编码) |
清晰强制分离 |
| Charset 要求 | 字符流可选(常被忽略) | 字符操作必须显式指定 |
| API 安全性 | 易出错(乱码风险高) | 编译期保障(无 Charset 无法编译) |
| 底层实现 | 直接操作流 | 封装传统流 + Channel 优化 |
🌟 记住这句话 :
"NIO.2 不是取消了字节/字符之分,而是用更严格、更安全的 API 设计,让开发者无法忽略这个区分。"它把"选择权"交还给开发者,并通过编译器强制规范------这才是现代 Java I/O 的精髓。