Java 文件与 I/O 全面指南:传统 IO 与 NIO.2 深度解析(含完整实例)

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. PathPaths(路径表示)

操作 传统 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

五、最佳实践黄金法则

  1. 路径与元数据操作首选 NIO.2 (Path, Files)

    java 复制代码
    // ✅ 推荐
    Files.createDirectories(path);
    // ❌ 避免
    new File(pathStr).mkdirs();
  2. 文本文件读写NIO.2 + 显式编码

    java 复制代码
    // ✅ 推荐
    Files.write(path, content, StandardCharsets.UTF_8);
    // ❌ 避免
    new FileWriter(path); // 默认编码不可控!
  3. 二进制流式处理传统 IO + 缓冲 + try-with-resources

    java 复制代码
    try (BufferedInputStream bis = new BufferedInputStream(
             Files.newInputStream(path))) { ... }
  4. 资源管理永远使用 try-with-resources

    java 复制代码
    try (BufferedReader br = Files.newBufferedReader(path, UTF_8)) { ... }
  5. 大文件处理分块读取,避免 readAllBytes

    java 复制代码
    try (InputStream in = Files.newInputStream(path)) {
        byte[] buf = new byte[8192];
        while ((len = in.read(buf)) > 0) { ... }
    }
  6. 异常处理捕获具体异常(NIO.2优势)

    java 复制代码
    try {
        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 的精髓。

相关推荐
寻寻觅觅☆8 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
YJlio8 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
l1t8 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
青云计划9 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿9 小时前
Jsoniter(java版本)使用介绍
java·开发语言
ceclar1239 小时前
C++使用format
开发语言·c++·算法
山塘小鱼儿9 小时前
本地Ollama+Agent+LangGraph+LangSmith运行
python·langchain·ollama·langgraph·langsimth
探路者继续奋斗9 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd
码说AI10 小时前
python快速绘制走势图对比曲线
开发语言·python