一、IO 流的核心概念

IO 流(Input/Output Stream)是指数据在数据源(如文件、网络连接、内存等存储介质)与程序之间的传输通道,其本质是数据的有序流动过程。在 Java 中,IO 操作被抽象为流的形式,通过统一的接口规范实现不同设备间的标准化数据交互,这种抽象屏蔽了底层设备的差异性,使开发者可以专注于业务逻辑。

  1. 数据源与数据目的地

    • 数据源:提供数据的源头设备或存储介质,常见类型包括:
      • 磁盘文件(如 FileInputStream 读取的.txt/.csv文件)
      • 标准输入设备(如 System.in 对应的键盘输入)
      • 网络连接(如 Socket.getInputStream()获取的网络数据)
      • 内存缓冲区(如 ByteArrayInputStream 处理的字节数组)
    • 数据目的地:接收数据的终端设备或存储介质,典型示例有:
      • 显示终端(如 System.out 控制的控制台输出)
      • 持久化存储(如 FileOutputStream 写入的数据库文件)
      • 网络接收端(如 Socket.getOutputStream()发送的数据)
  2. 流的定向特性

    • 输入流(InputStream/Reader):数据从外部流向程序,完成读操作
      • 示例:FileInputStream 读取本地文件时,数据从磁盘文件→JVM内存
    • 输出流(OutputStream/Writer):数据从程序流向外部,完成写操作
      • 示例:FileOutputStream 写入文件时,数据从JVM内存→磁盘文件
    • 双向流(如 RandomAccessFile):支持读写混合操作
  3. Java IO 体系架构

    • 基础流:直接连接数据源的原始流
      • 文件流:FileInputStream/FileOutputStream
      • 字节数组流:ByteArrayInputStream/ByteArrayOutputStream
    • 装饰流:通过组合增强功能的处理流
      • 缓冲流:BufferedInputStream(提供8192字节的默认缓冲区)
      • 转换流:InputStreamReader(实现字节到字符的编码转换)
      • 对象流:ObjectOutputStream(支持Java对象序列化)
    • 设计优势:
      1. 符合开闭原则:新增功能无需修改现有类
      2. 动态组合:如可构建"文件输入流+缓冲装饰+字符转换"的复合流
      3. 职责分离:基础流处理设备连接,装饰流实现数据处理

应用场景示例:

  • 网络文件下载:URLConnection获取输入流→BufferedInputStream缓冲→FileOutputStream写入本地
  • 日志收集:System.in读取控制台输入→InputStreamReader字符转换→BufferedWriter写入日志文件
  • 数据持久化:ObjectOutputStream将内存对象序列化后通过FileOutputStream存储到磁盘

二、IO 流的分类体系

2.1 按数据流向划分

输入流(InputStream/Reader)

  • 功能特性:负责将外部数据源的数据读取到程序中
  • 核心实现
    • 字节输入流:InputStream及其子类(如FileInputStream用于文件读取)
    • 字符输入流:Reader及其子类(如InputStreamReader可指定字符编码)
  • 典型应用场景
    • 读取磁盘文件内容
    • 从网络连接接收数据
    • 读取用户键盘输入(System.in
  • 注意事项:使用后必须调用close()方法释放资源

输出流(OutputStream/Writer)

  • 功能特性:负责将程序产生的数据写入到目标位置
  • 核心实现
    • 字节输出流:OutputStream及其子类(如FileOutputStream
    • 字符输出流:Writer及其子类(如PrintWriter可格式化输出)
  • 典型应用场景
    • 写入日志文件
    • 向网络连接发送数据
    • 控制台输出(System.out
  • 注意事项:建议使用try-with-resources确保流关闭

2.2 按数据单位划分

字节流

  • 底层实现:直接操作8位字节,不进行任何转换
  • 核心类
    • InputStream:所有字节输入流的抽象基类
    • OutputStream:所有字节输出流的抽象基类
  • 适用场景
    • 二进制文件(如图片、视频、压缩包)
    • 网络数据传输
    • 需要精确控制字节级操作的场景
  • 示例代码
    try (FileInputStream fis = new FileInputStream("image.jpg");
         FileOutputStream fos = new FileOutputStream("copy.jpg")) {
        byte[] buffer = new byte[1024];
        int bytesRead;
        while ((bytesRead = fis.read(buffer)) != -1) {
            fos.write(buffer, 0, bytesRead);
        }
    }
    

字符流

  • 底层实现:基于字节流,通过字符编码(如UTF-8、GBK)进行转换
  • 核心类
    • Reader:所有字符输入流的抽象基类
    • Writer:所有字符输出流的抽象基类
  • 适用场景
    • 文本文件处理
    • 需要国际化支持的文本操作
    • 需要自动处理字符编码的场景
  • 编码相关问题
    • 默认使用平台编码(可通过file.encoding系统属性查看)
    • 推荐显式指定编码(如new InputStreamReader(fis, "UTF-8")
  • 示例代码
    try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(new FileInputStream("text.txt"), "UTF-8"))) {
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }
    }
    

2.3 按流的角色划分

节点流

  • 特点:直接与数据源/目的地建立连接
  • 常见实现
    • 文件流:FileInputStreamFileOutputStreamFileReaderFileWriter
    • 数组流:ByteArrayInputStreamByteArrayOutputStream
    • 管道流:PipedInputStreamPipedOutputStream
  • 使用注意
    • 通常作为处理流的构造参数
    • 需要手动管理资源释放

处理流(装饰流)

  • 设计模式:基于装饰者模式,动态扩展功能
  • 常见类型
    • 缓冲流:BufferedInputStreamBufferedWriter(提高IO效率)
    • 转换流:InputStreamReaderOutputStreamWriter(字节/字符转换)
    • 对象流:ObjectInputStreamObjectOutputStream(序列化)
    • 数据流:DataInputStreamDataOutputStream(基本数据类型操作)
  • 优势
    • 功能可组合(如缓冲+字符转换)
    • 避免直接操作底层资源
  • 典型组合示例
    // 带缓冲的UTF-8编码字符读取流
    try (BufferedReader br = new BufferedReader(
            new InputStreamReader(
                new FileInputStream("data.txt"), StandardCharsets.UTF_8))) {
        // 读取操作...
    }
    
    // 带缓冲的对象序列化输出流
    try (ObjectOutputStream oos = new ObjectOutputStream(
            new BufferedOutputStream(
                new FileOutputStream("objects.dat")))) {
        oos.writeObject(someObject);
    }
    

综合应用建议

  1. 优先使用处理流组合,特别是缓冲流能显著提升性能
  2. 文本处理时明确指定字符编码,避免跨平台问题
  3. 使用try-with-resources确保资源释放
  4. 大数据量操作时注意缓冲区大小设置(默认8KB)

三、字节流核心类详解

字节流是Java IO体系中最基础的数据传输方式,可以处理任意类型的二进制数据(包括文本、图像、音频、视频等),适用于所有I/O操作场景。与字符流不同,字节流直接操作原始字节数据,不涉及任何编码转换。

3.1 输入字节流(InputStream)

InputStream是抽象类,作为所有字节输入流的父类,定义了基本的读操作方法,构成了Java IO的基础架构。

核心方法详解

  1. int read()

    • 读取单个字节,返回值为0-255之间的int值(无符号字节)
    • 如果到达流末尾则返回-1
    • 示例:逐个字节读取文件内容
    int byteValue;
    while((byteValue = inputStream.read()) != -1) {
        // 处理每个字节
    }
    

  2. int read(byte[] b)

    • 尝试读取足够字节填充数组b
    • 返回实际读取的字节数,可能小于数组长度
    • 到达流末尾时返回-1
    • 典型应用:批量读取数据提高效率
  3. int read(byte[] b, int off, int len)

    • 从偏移量off开始,最多读取len个字节到数组b中
    • 同样返回实际读取字节数
    • 适用于需要精确控制读取位置的情况
  4. void close()

    • 关闭流并释放相关系统资源
    • 应该总是放在finally块或使用try-with-resources语句

常用子类及使用场景

  1. FileInputStream

    • 从文件系统中读取数据的具体实现
    • 典型用法(使用try-with-resources确保自动关闭):
    try (FileInputStream fis = new FileInputStream("data.bin")) {
        byte[] buffer = new byte[4096];  // 4KB缓冲区
        int bytesRead;
        while ((bytesRead = fis.read(buffer)) != -1) {
            // 处理读取的数据
            processData(buffer, bytesRead);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    

  2. ByteArrayInputStream

    • 将内存中的字节数组作为输入源
    • 常用于测试或处理内存数据
    byte[] data = {65, 66, 67, 68};  // ABCD的ASCII码
    try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
        // 从内存数组读取
    }
    

  3. FilterInputStream(装饰器模式)

    • 作为装饰器类的父类,为其他流添加功能
    • BufferedInputStream:提供缓冲功能,显著减少实际I/O操作次数
      try (InputStream is = new BufferedInputStream(
                           new FileInputStream("largefile.dat"), 8192)) {
          // 使用8KB缓冲区高效读取
      }
      

    • DataInputStream:允许读取Java基本数据类型
    • ObjectInputStream:用于对象反序列化

3.2 输出字节流(OutputStream)

OutputStream是所有字节输出流的抽象父类,定义了数据写入的基本操作。

核心方法详解

  1. void write(int b)

    • 写入单个字节(低8位,高24位被忽略)
    • 示例:逐个字节写入
    outputStream.write(65);  // 写入字母'A'
    

  2. void write(byte[] b)

    • 写入整个字节数组
    • 相当于write(b, 0, b.length)
  3. void write(byte[] b, int off, int len)

    • 写入数组b中从off开始的len个字节
    • 适用于只写入数组部分内容的情况
  4. void flush()

    • 强制将缓冲区内容写入目标设备
    • 对文件流可能影响不大,但对网络流很重要
    • 通常应在完成一系列写操作后调用
  5. void close()

    • 关闭前会自动调用flush()
    • 释放系统资源

常用子类及使用场景

  1. FileOutputStream

    • 向文件写入数据的基本实现
    • 构造方法参数可以是File对象或路径字符串
    • 第二个参数append指定是否追加模式
    try (FileOutputStream fos = new FileOutputStream("log.txt", true)) {
        String logEntry = "New log entry\n";
        byte[] bytes = logEntry.getBytes(StandardCharsets.UTF_8);
        fos.write(bytes);
        fos.flush();  // 确保数据立即写入磁盘
    } catch (IOException e) {
        e.printStackTrace();
    }
    

  2. ByteArrayOutputStream

    • 将数据写入内存缓冲区
    • 可通过toByteArray()获取写入的字节数组
    • 常用于构建内存中的二进制数据
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write("Hello".getBytes());
    byte[] result = baos.toByteArray();
    

  3. BufferedOutputStream

    • 为输出流添加缓冲功能
    • 默认缓冲区大小8KB,可自定义
    • 显著减少实际I/O操作次数
    try (OutputStream os = new BufferedOutputStream(
                          new FileOutputStream("output.bin"))) {
        for (int i = 0; i < 1000; i++) {
            os.write(i);  // 实际不会立即写入磁盘
        }
        // 自动close()时会flush()
    }
    

  4. 其他装饰流

    • DataOutputStream:写入Java基本数据类型
    • ObjectOutputStream:用于对象序列化
    • PrintStream:System.out就是此类型,提供print/println方法

性能优化建议

  1. 总是使用缓冲流处理大文件(默认8KB缓冲区通常足够)
  2. 合理设置缓冲区大小(太大浪费内存,太小增加I/O次数)
  3. 批量读写(使用数组)比单字节操作效率高得多
  4. 使用try-with-resources确保流正确关闭
  5. 网络传输时及时调用flush()确保数据发送

四、字符流核心类详解

字符流是Java IO中专门为文本处理设计的一组类,它们能够自动处理字节与字符之间的编码转换,大大简化了文本数据的读写操作。

4.1 输入字符流(Reader)

Reader是Java中所有字符输入流的抽象父类,它定义了读取字符数据的基本方法。

核心方法详解

  1. int read()

    • 读取单个字符,返回读取的字符(0-65535)
    • 如果已到达流的末尾,则返回-1
    • 示例:int ch = reader.read(); // 读取一个字符
  2. int read(char[] cbuf)

    • 尝试读取足够多的字符填充整个字符数组
    • 返回实际读取的字符数,如果到达流末尾则返回-1
    • 示例:char[] buffer = new char[1024]; int bytesRead = reader.read(buffer);
  3. int read(char[] cbuf, int off, int len)

    • 从指定偏移量开始,读取最多len个字符到数组
    • 适合处理大文件时控制每次读取的数据量
    • 示例:reader.read(buffer, 10, 100); // 从数组第10个位置开始存入100个字符
  4. void close()

    • 关闭流并释放相关系统资源
    • 应该在finally块中调用或使用try-with-resources语句

常用子类及其应用场景

  1. FileReader

    • 直接从文件读取字符的便捷类
    • 使用平台默认字符编码(可能导致跨平台问题)
    • 适合读取小文本文件
    • 示例:
      try (FileReader fr = new FileReader("example.txt")) {
          // 读取操作
      }
      

  2. BufferedReader

    • 提供缓冲功能,显著提高读取效率
    • 特有的readLine()方法可以方便地逐行读取文本
    • 适合处理大文本文件或需要逐行处理的场景
    • 典型用法:
      try (BufferedReader br = new BufferedReader(new FileReader("text.txt"))) {
          String line;
          while ((line = br.readLine()) != null) {
              System.out.println(line); // 处理每一行文本
          }
      } catch (IOException e) {
          e.printStackTrace(); // 异常处理
      }
      

  3. InputStreamReader

    • 字节流到字符流的桥梁,可以指定字符编码
    • 处理不同编码的文本文件时特别有用
    • 示例(指定UTF-8编码):
      try (InputStreamReader isr = new InputStreamReader(
              new FileInputStream("utf8.txt"), StandardCharsets.UTF_8)) {
          // 读取操作
          char[] buffer = new char[1024];
          int charsRead;
          while ((charsRead = isr.read(buffer)) != -1) {
              // 处理读取的字符数据
          }
      }
      

4.2 输出字符流(Writer)

Writer是所有字符输出流的抽象父类,定义了写入字符数据的基本操作。

核心方法详解

  1. void write(int c)

    • 写入单个字符(低16位)
    • 示例:writer.write('A'); // 写入字符'A'
  2. void write(char[] cbuf)

    • 写入整个字符数组
    • 示例:char[] data = {'H','e','l','l','o'}; writer.write(data);
  3. void write(String str)

    • 直接写入整个字符串
    • 最常用的字符串输出方法
    • 示例:writer.write("Hello World");
  4. void flush()

    • 强制将缓冲区中的内容写入目标
    • 在需要确保数据确实写入时调用
    • 示例:writer.flush(); // 确保数据写入文件
  5. void close()

    • 关闭流并释放资源
    • 通常会自动调用flush()

常用子类及其应用场景

  1. FileWriter

    • 直接向文件写入字符的便捷类
    • 使用平台默认字符编码
    • 适合写入小文本文件
    • 示例:
      try (FileWriter fw = new FileWriter("output.txt")) {
          fw.write("这是要保存的文本内容");
      }
      

  2. BufferedWriter

    • 提供缓冲功能,提高写入效率
    • 特有的newLine()方法可以写入平台相关的换行符
    • 适合大量文本写入或需要频繁换行的场景
    • 示例:
      try (BufferedWriter bw = new BufferedWriter(new FileWriter("log.txt"))) {
          bw.write("第一行内容");
          bw.newLine(); // 写入换行符
          bw.write("第二行内容");
      }
      

  3. OutputStreamWriter

    • 字符流到字节流的桥梁,可以指定字符编码
    • 处理需要特定编码的文本输出时特别有用
    • 示例(指定UTF-8编码):
      try (OutputStreamWriter osw = new OutputStreamWriter(
              new FileOutputStream("utf8.txt"), StandardCharsets.UTF_8)) {
          osw.write("中文内容"); // 确保中文字符正确保存为UTF-8编码
          osw.write(System.lineSeparator()); // 写入系统相关的换行符
          osw.write("更多内容...");
      }
      

在实际开发中,通常会将缓冲流与其他流配合使用以提高IO效率,特别是在处理大文件或需要频繁读写操作时。正确选择字符编码对于多语言支持至关重要,UTF-8通常是推荐的选择。

五、特殊流类型

除了基础的字节流和字符流,Java 还提供了处理特定场景的特殊流,这些流在实际开发中有着广泛的应用场景。

5.1 对象流(序列化与反序列化)

ObjectInputStream 和 ObjectOutputStream 用于实现对象的序列化(将对象转换为字节序列)和反序列化(将字节序列恢复为对象)。这些流在网络通信、对象持久化等场景中非常有用。

使用要求详解

  1. Serializable 接口

    • 必须实现 java.io.Serializable 接口
    • 这是一个标记接口,不包含任何方法
    • 示例:public class User implements Serializable {...}
  2. 属性序列化规则

    • 所有非静态成员变量都必须是可序列化的
    • 对于不需要序列化的字段,使用 transient 修饰
    • 示例:private transient String password; // 不序列化密码字段
  3. serialVersionUID

    • 显式声明可以提高版本兼容性
    • 格式:private static final long serialVersionUID = 1L;
    • 如果没有显式声明,JVM 会根据类结构自动生成,可能导致类结构改变后反序列化失败

完整代码示例

// 序列化示例
try (ObjectOutputStream oos = new ObjectOutputStream(
    new FileOutputStream("object.dat"))) {
    User user = new User("张三", 25);
    oos.writeObject(user); // 序列化对象到文件
    oos.flush(); // 确保数据写入
} catch (IOException e) {
    e.printStackTrace();
}

// 反序列化示例
try (ObjectInputStream ois = new ObjectInputStream(
    new FileInputStream("object.dat"))) {
    User user = (User) ois.readObject(); // 从文件恢复对象
    System.out.println(user.getName()); // 输出: 张三
} catch (IOException | ClassNotFoundException e) {
    e.printStackTrace();
}

5.2 打印流(PrintStream/PrintWriter)

打印流提供了方便的打印功能,支持多种数据类型的格式化输出,常用于日志记录和控制台输出。

两种打印流对比

  1. PrintStream

    • 字节打印流
    • System.out 就是 PrintStream 实例
    • 自动调用 flush() 方法
    • 示例:System.out.println("Hello World");
  2. PrintWriter

    • 字符打印流
    • 支持指定字符编码
    • 需要手动调用 flush() 或启用自动刷新
    • 更适合处理文本文件

高级使用示例

// 使用PrintWriter记录日志
try (PrintWriter pw = new PrintWriter(
    new FileWriter("log.txt", true), // true表示追加模式
    true)) { // true表示自动刷新
    pw.println("===== 系统启动 =====");
    pw.printf("当前时间: %tF %<tT%n", new Date()); // 格式化日期时间
    pw.printf("用户%s登录成功, 权限级别: %d%n", "admin", 3);
    
    // 记录异常信息
    try {
        // 模拟异常
        int result = 10 / 0;
    } catch (Exception e) {
        e.printStackTrace(pw); // 将异常堆栈输出到日志文件
    }
}

5.3 转换流(InputStreamReader/OutputStreamWriter)

转换流是字节流与字符流之间的桥梁,主要用于处理字符编码转换问题。

核心功能

  1. InputStreamReader

    • 将字节输入流转换为字符输入流
    • 可以指定字符编码
    • 示例:new InputStreamReader(new FileInputStream("data.txt"), "UTF-8")
  2. OutputStreamWriter

    • 将字符输出流转换为字节输出流
    • 可以指定字符编码
    • 示例:new OutputStreamWriter(new FileOutputStream("output.txt"), "GBK")

编码处理最佳实践

// 读取UTF-8编码的文件
try (BufferedReader reader = new BufferedReader(
    new InputStreamReader(
        new FileInputStream("data.txt"), 
        StandardCharsets.UTF_8))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

// 写入GBK编码的文件
try (BufferedWriter writer = new BufferedWriter(
    new OutputStreamWriter(
        new FileOutputStream("output.txt"), 
        "GBK"))) {
    writer.write("中文内容");
    writer.newLine();
    writer.write("English content");
}

编码问题注意事项

  1. 总是显式指定字符编码,不要依赖平台默认编码
  2. 推荐使用UTF-8编码,它是跨平台的通用编码
  3. 对于中文环境,注意GBK和UTF-8的区别
  4. 可以使用StandardCharsets类中定义的常量(如StandardCharsets.UTF_8)来避免拼写错误

六、IO 流使用的注意事项

6.1 资源关闭机制详解

IO 流(如文件流、网络流等)属于系统级资源,必须确保正确关闭以避免以下问题:

  • 资源泄露:导致文件句柄耗尽,系统无法打开新文件
  • 数据丢失:输出流未正常关闭可能导致缓冲区数据未写入
  • 并发问题:已打开的文件可能被其他进程锁定

最佳实践方案

方案一:try-with-resources(Java 7+)

// 支持同时管理多个资源
try (InputStream in = new FileInputStream("input.txt");
     OutputStream out = new FileOutputStream("output.txt")) {
    // 资源操作代码
    byte[] buffer = new byte[1024];
    int bytesRead;
    while ((bytesRead = in.read(buffer)) != -1) {
        out.write(buffer, 0, bytesRead);
    }
} catch (IOException e) {
    // 异常处理
    logger.error("文件操作失败", e);
}

方案二:try-finally(兼容旧版本)

InputStream in = null;
OutputStream out = null;
try {
    in = new FileInputStream("input.txt");
    out = new FileOutputStream("output.txt");
    // 操作代码...
} catch (IOException e) {
    logger.error("IO操作异常", e);
} finally {
    // 关闭顺序与创建顺序相反
    try {
        if (out != null) out.close();
    } catch (IOException e) {
        logger.warn("关闭输出流失败", e);
    }
    try {
        if (in != null) in.close();
    } catch (IOException e) {
        logger.warn("关闭输入流失败", e);
    }
}

6.2 缓冲流优化技巧

缓冲流通过内存缓冲区(默认8KB)减少物理IO次数,性能提升可达10倍以上。

实践建议

  1. 组合使用示例
// 文件复制的最佳实践
try (InputStream fis = new FileInputStream("source.mp4");
     BufferedInputStream bis = new BufferedInputStream(fis, 32768);  // 32KB缓冲区
     OutputStream fos = new FileOutputStream("target.mp4");
     BufferedOutputStream bos = new BufferedOutputStream(fos, 32768)) {
    
    byte[] buffer = new byte[8192];
    int bytesRead;
    while ((bytesRead = bis.read(buffer)) != -1) {
        bos.write(buffer, 0, bytesRead);
    }
}

  1. 缓冲区大小调优原则

    • 小文件(1MB以下):使用默认8KB即可
    • 中等文件(1-100MB):建议16-32KB
    • 大文件(100MB+):可设置为64-256KB
  2. 关键注意事项

    • 输出流必须调用flush()或关闭流才能确保数据写入
    • 缓冲流嵌套顺序:缓冲流应包装在最外层
    • 网络传输时,缓冲区大小应考虑网络MTU(通常1500字节)

6.3 编码处理规范

常见编码问题场景

1.文件读取乱码

// 错误做法:依赖平台默认编码
new FileReader("data.txt");

// 正确做法:显式指定编码
new InputStreamReader(new FileInputStream("data.txt"), StandardCharsets.UTF_8);

2.编码转换示例

// GBK转UTF-8
try (InputStreamReader gbkReader = new InputStreamReader(
        new FileInputStream("gbk.txt"), "GBK");
     OutputStreamWriter utf8Writer = new OutputStreamWriter(
        new FileOutputStream("utf8.txt"), StandardCharsets.UTF_8)) {
    
    char[] buffer = new char[1024];
    int charsRead;
    while ((charsRead = gbkReader.read(buffer)) != -1) {
        utf8Writer.write(buffer, 0, charsRead);
    }
}

编码选择指南

场景 推荐编码 说明
Web应用 UTF-8 国际化标准
中文Windows系统 GBK/GB2312 兼容旧系统
数据库交互 UTF-8 通用标准
二进制数据 ISO-8859-1 保留字节原始值

6.4 异常处理规范

完整异常处理示例

public void processLargeFile(String filename) throws IOException {
    if (filename == null) {
        throw new IllegalArgumentException("文件名不能为空");
    }
    
    Path filePath = Paths.get(filename);
    if (!Files.exists(filePath)) {
        throw new FileNotFoundException("文件不存在: " + filename);
    }
    
    try (BufferedReader reader = Files.newBufferedReader(filePath)) {
        String line;
        while ((line = reader.readLine()) != null) {
            // 处理每行数据
            if (line.length() > 1000) {
                logger.warn("发现超长行: {}", line.substring(0, 50) + "...");
            }
        }
    } catch (IOException e) {
        logger.error("处理文件{}失败,文件大小: {}", 
            filename, Files.size(filePath), e);
        throw new IOException("处理文件失败: " + filename, e);
    }
}

异常处理要点

  1. 资源检查

    • 检查文件是否存在(Files.exists)
    • 检查磁盘空间(Files.getFileStore)
    • 检查文件权限
  2. 异常信息

    • 包含操作文件名
    • 记录文件大小
    • 保留原始异常链
  3. 特殊异常处理

    • FileSystemException:处理权限问题
    • ClosedChannelException:处理流被意外关闭
    • OutOfMemoryError:大文件处理时可能发生

6.5 高级优化建议

NIO文件操作

// 高性能文件复制
Path source = Paths.get("source.iso");
Path target = Paths.get("target.iso");
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

// 遍历目录
try (Stream<Path> paths = Files.walk(Paths.get("/data"))) {
    paths.filter(Files::isRegularFile)
         .filter(p -> p.toString().endsWith(".log"))
         .forEach(this::processLogFile);
}

性能对比测试

方法 1GB文件复制时间 内存占用
基础流 12.5s
缓冲流(8KB) 1.8s 8KB
缓冲流(64KB) 1.2s 64KB
NIO Files.copy 0.9s 依赖系统缓存

内存优化技巧

1.大文件处理

// 分块读取大文件
try (RandomAccessFile raf = new RandomAccessFile("huge.bin", "r")) {
    byte[] buffer = new byte[4 * 1024 * 1024]; // 4MB块
    for (long offset = 0; offset < raf.length(); offset += buffer.length) {
        int bytesRead = raf.read(buffer);
        processChunk(buffer, bytesRead);
    }
}

2.流复用原则

// 错误做法:在循环内创建流
for (String file : fileList) {
    try (InputStream in = new FileInputStream(file)) {
        // 处理...
    }
}

// 正确做法:复用缓冲流
try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("output.dat"))) {
    for (String file : fileList) {
        try (InputStream in = new FileInputStream(file)) {
            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
        }
    }
}

七、IO 流性能优化策略

1. 优先使用缓冲流

缓冲流(如 BufferedInputStream/BufferedOutputStream)内置了缓冲区机制,可以将多次小规模的 IO 操作合并为一次较大规模的 IO 操作。例如:

  • 默认缓冲区大小通常为 8KB
  • 读取 1KB 文件时,无缓冲流需要 1024 次磁盘访问
  • 使用缓冲流仅需 1 次磁盘访问即可完成

2. 合理设置缓冲区大小

缓冲区大小的设置需要平衡内存使用和 IO 效率:

  • 小文件(<1MB):8KB-16KB 缓冲区足够
  • 中等文件(1MB-100MB):32KB-64KB 缓冲区
  • 大文件(>100MB):可考虑 128KB 或更大
  • 注意:过大的缓冲区(如 1MB)可能会浪费内存而收益不明显

3. 使用数组批量读写

批量操作显著优于单字节操作:

// 低效方式 (不推荐)
int b;
while((b = in.read()) != -1) {
    out.write(b);
}

// 高效方式 (推荐)
byte[] buffer = new byte[8192];
int len;
while((len = in.read(buffer)) != -1) {
    out.write(buffer, 0, len);
}

测试表明,批量读写可比单字节操作快 10-100 倍。

4. 减少流的嵌套层数

过多的装饰流会增加处理开销:

// 不推荐:三层嵌套
new BufferedReader(new InputStreamReader(new FileInputStream("file.txt")));

// 推荐:两层嵌套
new BufferedReader(new FileReader("file.txt"));

每增加一层装饰流,大约会增加 5%-10% 的处理开销。

5. 及时关闭不再使用的流

资源管理最佳实践:

  • 使用 try-with-resources 语法确保流关闭
try (InputStream in = new FileInputStream("file.txt");
     OutputStream out = new FileOutputStream("output.txt")) {
    // IO 操作
}

  • 未关闭的流可能导致:
    • 文件锁定(无法删除或修改)
    • 内存泄漏
    • 系统资源耗尽(文件描述符)

对于网络连接相关的流,更应及时关闭以避免连接泄漏。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐