Java InputStream和OutputStream实现类完全指南

#

文章目录

    • [引言:Java I/O体系概述](#引言:Java I/O体系概述)
    • 第一部分:文件操作流
      • [1.1 FileInputStream和FileOutputStream](#1.1 FileInputStream和FileOutputStream)
        • [1.1.1 基本概念](#1.1.1 基本概念)
        • [1.1.2 构造方法](#1.1.2 构造方法)
        • [1.1.3 核心方法](#1.1.3 核心方法)
        • [1.1.4 使用示例](#1.1.4 使用示例)
        • [1.1.5 性能考虑](#1.1.5 性能考虑)
      • [1.2 FileInputStream的内部工作原理](#1.2 FileInputStream的内部工作原理)
    • 第二部分:缓冲流
      • [2.1 BufferedInputStream和BufferedOutputStream](#2.1 BufferedInputStream和BufferedOutputStream)
        • [2.1.1 基本概念](#2.1.1 基本概念)
        • [2.1.2 工作原理](#2.1.2 工作原理)
        • [2.1.3 构造方法](#2.1.3 构造方法)
        • [2.1.4 性能对比示例](#2.1.4 性能对比示例)
        • [2.1.5 重要注意事项](#2.1.5 重要注意事项)
      • [2.2 缓冲流的缓冲区大小选择](#2.2 缓冲流的缓冲区大小选择)
    • 第三部分:内存操作流
      • [3.1 ByteArrayInputStream和ByteArrayOutputStream](#3.1 ByteArrayInputStream和ByteArrayOutputStream)
        • [3.1.1 基本概念](#3.1.1 基本概念)
        • [3.1.2 特点](#3.1.2 特点)
        • [3.1.3 ByteArrayInputStream详解](#3.1.3 ByteArrayInputStream详解)
        • [3.1.4 ByteArrayOutputStream详解](#3.1.4 ByteArrayOutputStream详解)
      • [3.3 其他内存操作流](#3.3 其他内存操作流)
    • 第四部分:对象序列化流
      • [4.1 ObjectInputStream和ObjectOutputStream](#4.1 ObjectInputStream和ObjectOutputStream)
        • [4.1.1 基本概念](#4.1.1 基本概念)
        • [4.1.2 序列化要求](#4.1.2 序列化要求)
        • [4.1.3 构造方法](#4.1.3 构造方法)
        • [4.1.4 核心方法](#4.1.4 核心方法)
        • [4.1.5 使用示例](#4.1.5 使用示例)
        • [4.1.6 自定义序列化](#4.1.6 自定义序列化)
        • [4.1.7 序列化注意事项](#4.1.7 序列化注意事项)
    • 第五部分:数据流
      • [5.1 DataInputStream和DataOutputStream](#5.1 DataInputStream和DataOutputStream)
        • [5.1.1 基本概念](#5.1.1 基本概念)
        • [5.1.2 特点](#5.1.2 特点)
        • [5.1.3 构造方法](#5.1.3 构造方法)
        • [5.1.4 核心方法](#5.1.4 核心方法)
        • [5.1.5 使用示例](#5.1.5 使用示例)
        • [5.1.6 UTF-8字符串格式说明](#5.1.6 UTF-8字符串格式说明)
    • 第六部分:管道流
      • [6.1 PipedInputStream和PipedOutputStream](#6.1 PipedInputStream和PipedOutputStream)
        • [6.1.1 基本概念](#6.1.1 基本概念)
        • [6.1.2 特点](#6.1.2 特点)
        • [6.1.3 构造方法](#6.1.3 构造方法)
        • [6.1.4 连接方法](#6.1.4 连接方法)
        • [6.1.5 使用示例](#6.1.5 使用示例)
        • [6.1.6 注意事项](#6.1.6 注意事项)
    • 第七部分:回退输入流
      • [7.1 PushbackInputStream](#7.1 PushbackInputStream)
        • [7.1.1 基本概念](#7.1.1 基本概念)
        • [7.1.2 工作原理](#7.1.2 工作原理)
        • [7.1.3 构造方法](#7.1.3 构造方法)
        • [7.1.4 核心方法](#7.1.4 核心方法)
        • [7.1.5 使用示例](#7.1.5 使用示例)
        • [7.1.6 性能考虑](#7.1.6 性能考虑)
    • 第八部分:打印流
      • [8.1 PrintStream](#8.1 PrintStream)
        • [8.1.1 基本概念](#8.1.1 基本概念)
        • [8.1.2 特点](#8.1.2 特点)
        • [8.1.3 构造方法](#8.1.3 构造方法)
        • [8.1.4 核心方法](#8.1.4 核心方法)
        • [8.1.5 使用示例](#8.1.5 使用示例)
        • [8.1.6 重要注意事项](#8.1.6 重要注意事项)
    • 第九部分:过滤流
      • [9.1 FilterInputStream和FilterOutputStream](#9.1 FilterInputStream和FilterOutputStream)
        • [9.1.1 基本概念](#9.1.1 基本概念)
        • [9.1.2 自定义过滤流示例](#9.1.2 自定义过滤流示例)
    • 第十部分:序列输入流
      • [10.1 SequenceInputStream](#10.1 SequenceInputStream)
        • [10.1.1 基本概念](#10.1.1 基本概念)
        • [10.1.2 构造方法](#10.1.2 构造方法)
        • [10.1.3 使用示例](#10.1.3 使用示例)
        • [10.1.4 注意事项](#10.1.4 注意事项)
    • 第十一部分:校验和流
      • [11.1 CheckedInputStream和CheckedOutputStream](#11.1 CheckedInputStream和CheckedOutputStream)
        • [11.1.1 基本概念](#11.1.1 基本概念)
        • [11.1.2 构造方法](#11.1.2 构造方法)
        • [11.1.3 Checksum实现类](#11.1.3 Checksum实现类)
        • [11.1.4 使用示例](#11.1.4 使用示例)
    • 第十二部分:压缩流
      • [12.1 GZIPInputStream和GZIPOutputStream](#12.1 GZIPInputStream和GZIPOutputStream)
        • [12.1.1 基本概念](#12.1.1 基本概念)
        • [12.1.2 构造方法](#12.1.2 构造方法)
        • [12.1.3 使用示例](#12.1.3 使用示例)
      • [12.2 ZipInputStream和ZipOutputStream](#12.2 ZipInputStream和ZipOutputStream)
        • [12.2.1 基本概念](#12.2.1 基本概念)
        • [12.2.2 使用示例](#12.2.2 使用示例)
    • 第十三部分:组合流与最佳实践
      • [13.1 流的装饰器模式](#13.1 流的装饰器模式)
      • [13.2 常用组合模式](#13.2 常用组合模式)
      • [13.3 性能优化最佳实践](#13.3 性能优化最佳实践)
        • [13.3.1 缓冲区大小选择](#13.3.1 缓冲区大小选择)
        • [13.3.2 避免常见陷阱](#13.3.2 避免常见陷阱)
      • [13.4 异常处理最佳实践](#13.4 异常处理最佳实践)
    • 第十四部分:总结与选择指南
      • [14.1 实现类对比表](#14.1 实现类对比表)
      • [14.2 选择指南](#14.2 选择指南)
      • [14.3 性能考虑要点](#14.3 性能考虑要点)
      • [14.4 最终建议](#14.4 最终建议)

引言:Java I/O体系概述

Java的I/O(输入/输出)是通过流(Stream)实现的,流是一组有序的数据序列。根据数据的流向,流可以分为输入流(InputStream)和输出流(OutputStream)。字节流以字节(8位二进制)为单位处理数据,可以操作任何类型的文件,包括文本、图片、音频、视频等二进制文件。

InputStreamOutputStream是Java字节流的两个抽象基类,位于java.io包中。它们定义了字节流读写的基本方法,拥有众多的实现类,每个实现类都针对特定的数据源或处理需求进行了优化。本文将详细介绍这些实现类的特点、使用方法和最佳实践。

第一部分:文件操作流

1.1 FileInputStream和FileOutputStream

1.1.1 基本概念

FileInputStreamFileOutputStream是最基础的字节流实现类,用于从文件中读取字节数据或将字节数据写入文件。它们是Java程序与文件系统交互的最直接方式。

1.1.2 构造方法

FileInputStream构造方法

java 复制代码
// 通过文件路径创建
FileInputStream fis = new FileInputStream(String name);

// 通过File对象创建
FileInputStream fis = new FileInputStream(File file);

// 通过FileDescriptor创建
FileInputStream fis = new FileInputStream(FileDescriptor fdObj);

FileOutputStream构造方法

java 复制代码
// 通过文件路径创建(覆盖模式)
FileOutputStream fos = new FileOutputStream(String name);

// 通过文件路径创建(可指定追加模式)
FileOutputStream fos = new FileOutputStream(String name, boolean append);

// 通过File对象创建(覆盖模式)
FileOutputStream fos = new FileOutputStream(File file);

// 通过File对象创建(可指定追加模式)
FileOutputStream fos = new FileOutputStream(File file, boolean append);
1.1.3 核心方法

FileInputStream主要方法

  • int read(): 读取一个字节,返回0-255的字节值,到达文件末尾返回-1
  • int read(byte[] b): 读取最多b.length字节到数组中,返回实际读取的字节数
  • int read(byte[] b, int off, int len): 读取最多len字节到数组的指定位置
  • long skip(long n): 跳过并丢弃n个字节
  • int available(): 返回可读取的字节数估计值
  • void close(): 关闭流并释放相关资源

FileOutputStream主要方法

  • void write(int b): 写入一个字节
  • void write(byte[] b): 写入整个字节数组
  • void write(byte[] b, int off, int len): 写入字节数组的一部分
  • void flush(): 刷新缓冲区(对于FileOutputStream,write方法直接写入文件,flush通常不做任何操作)
  • void close(): 关闭流并释放资源
1.1.4 使用示例

基本文件复制示例

java 复制代码
public class FileCopyExample {
    public static void main(String[] args) {
        // 使用try-with-resources自动关闭资源(JDK 7+)
        try (FileInputStream fis = new FileInputStream("source.jpg");
             FileOutputStream fos = new FileOutputStream("dest.jpg")) {
            
            byte[] buffer = new byte[8192]; // 8KB缓冲区
            int bytesRead;
            
            while ((bytesRead = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, bytesRead);
            }
            
            System.out.println("文件复制完成");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

追加模式写入示例

java 复制代码
public class AppendToFileExample {
    public static void main(String[] args) {
        String logEntry = "[" + new Date() + "] 用户登录系统\n";
        
        try (FileOutputStream fos = new FileOutputStream("app.log", true)) {
            fos.write(logEntry.getBytes(StandardCharsets.UTF_8));
            System.out.println("日志写入成功");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
1.1.5 性能考虑
  • 单字节读写(read()write(int))效率极低,每次调用都会触发底层系统调用
  • 建议使用缓冲区(字节数组)批量读写,缓冲区大小通常设为4KB-8KB
  • 对于更高性能需求,可以配合缓冲流使用

1.2 FileInputStream的内部工作原理

FileInputStream实际通过JNI(Java Native Interface)调用操作系统底层的文件读取API。在Windows上是ReadFile函数,在Linux/Unix上是read系统调用。因此,频繁的单字节读写会频繁陷入内核态,造成性能瓶颈。


第二部分:缓冲流

2.1 BufferedInputStream和BufferedOutputStream

2.1.1 基本概念

BufferedInputStreamBufferedOutputStream装饰器模式的典型应用,它们为现有的输入输出流添加缓冲功能,通过减少实际的I/O操作次数来显著提升性能。

2.1.2 工作原理

BufferedInputStream 内部维护一个字节数组作为缓冲区。当调用read()方法时,它会尽可能多地读取数据填充缓冲区,后续的读取操作直接从缓冲区返回数据,只有当缓冲区数据耗尽时才再次从底层输入流读取。

BufferedOutputStream 同样维护一个字节数组作为缓冲区。写入的数据首先存入缓冲区,当缓冲区满或调用flush()方法时,才将缓冲区数据一次性写入底层输出流。

2.1.3 构造方法
java 复制代码
// 使用默认缓冲区大小(8192字节)
BufferedInputStream bis = new BufferedInputStream(InputStream in);
BufferedOutputStream bos = new BufferedOutputStream(OutputStream out);

// 指定缓冲区大小
BufferedInputStream bis = new BufferedInputStream(InputStream in, int size);
BufferedOutputStream bos = new BufferedOutputStream(OutputStream out, int size);
2.1.4 性能对比示例
java 复制代码
public class BufferedStreamPerformance {
    public static void main(String[] args) {
        String fileName = "test.dat";
        
        // 测试无缓冲写入
        long start = System.nanoTime();
        try (FileOutputStream fos = new FileOutputStream(fileName)) {
            for (int i = 0; i < 1000000; i++) {
                fos.write(i); // 单字节写入,每次都会触发系统调用
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("无缓冲写入时间: " + (end - start) / 1_000_000 + " ms");
        
        // 测试缓冲写入
        start = System.nanoTime();
        try (BufferedOutputStream bos = new BufferedOutputStream(
                new FileOutputStream(fileName))) {
            for (int i = 0; i < 1000000; i++) {
                bos.write(i); // 写入到缓冲区,满8K时才真正写入
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        end = System.nanoTime();
        System.out.println("缓冲写入时间: " + (end - start) / 1_000_000 + " ms");
        
        // 清理
        new File(fileName).delete();
    }
}

运行结果通常显示缓冲流比无缓冲流快几十甚至上百倍。

2.1.5 重要注意事项

关于flush()方法
BufferedOutputStreamflush()方法强制将缓冲区中的数据写入底层输出流。在关闭流之前,close()方法会自动调用flush()。但在以下情况需要手动调用:

  • 需要确保数据立即写入(如日志记录)
  • 写入完成后还要继续使用底层流
  • 长时间写入且希望避免数据滞留缓冲区

关于mark/reset功能
BufferedInputStream支持mark()reset()方法,允许读取位置回退。默认情况下mark有效,但如果在标记后读取的字节数超过缓冲区大小,标记可能会失效。可以通过构造函数指定更大的缓冲区来避免这个问题。

java 复制代码
public class MarkResetExample {
    public static void main(String[] args) throws IOException {
        byte[] data = "Hello World".getBytes();
        try (BufferedInputStream bis = new BufferedInputStream(
                new ByteArrayInputStream(data))) {
            
            // 标记当前位置(起始位置)
            bis.mark(10); // 参数表示标记有效期内最多可以读取的字节数
            
            // 读取前5个字节
            byte[] buffer = new byte[5];
            bis.read(buffer);
            System.out.println("第一次读取: " + new String(buffer));
            
            // 重置到标记位置
            bis.reset();
            
            // 重新读取
            bis.read(buffer);
            System.out.println("重置后读取: " + new String(buffer));
        }
    }
}

2.2 缓冲流的缓冲区大小选择

默认的8192字节(8KB)是经过实践检验的较优值,与文件系统的块大小和CPU缓存行大小相匹配。对于大文件顺序读写,可以适当增大缓冲区(如64KB或256KB)来进一步提升性能。但过大的缓冲区可能导致内存浪费和缓存利用率下降。


第三部分:内存操作流

3.1 ByteArrayInputStream和ByteArrayOutputStream

3.1.1 基本概念

ByteArrayInputStreamByteArrayOutputStream是操作内存中字节数组的流实现类。它们不涉及磁盘或网络I/O,所有操作都在内存中进行,因此速度极快,适合临时数据处理。

3.1.2 特点
  • 无需关闭 :这两个流的close()方法是空实现,调用没有实际效果
  • 内存操作:数据存储在JVM堆内存中
  • 线程不安全:多个线程同时访问需要外部同步
  • 自动扩容ByteArrayOutputStream在写入数据时会自动扩容
3.1.3 ByteArrayInputStream详解

构造方法

java 复制代码
// 使用整个字节数组作为数据源
ByteArrayInputStream bais = new ByteArrayInputStream(byte[] buf);

// 使用字节数组的一部分作为数据源
ByteArrayInputStream bais = new ByteArrayInputStream(byte[] buf, int offset, int length);

使用示例

java 复制代码
public class ByteArrayInputStreamExample {
    public static void main(String[] args) {
        byte[] data = "Java I/O 内存操作流示例".getBytes(StandardCharsets.UTF_8);
        
        try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
            byte[] buffer = new byte[1024];
            int bytesRead = bais.read(buffer);
            
            String result = new String(buffer, 0, bytesRead, StandardCharsets.UTF_8);
            System.out.println("读取的内容: " + result);
            
            // mark/reset支持(基于数组,完全可用)
            bais.reset(); // 重置到流的开头
            System.out.println("重置后可用字节数: " + bais.available());
            
            // 跳过前5个字节
            bais.skip(5);
            byte[] remaining = bais.readAllBytes();
            System.out.println("跳过5字节后: " + new String(remaining, StandardCharsets.UTF_8));
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
3.1.4 ByteArrayOutputStream详解

构造方法

java 复制代码
// 默认缓冲区大小32字节
ByteArrayOutputStream baos = new ByteArrayOutputStream();

// 指定初始缓冲区大小
ByteArrayOutputStream baos = new ByteArrayOutputStream(int size);

核心方法

java 复制代码
// 写入数据
void write(int b)
void write(byte[] b, int off, int len)

// 获取数据
byte[] toByteArray()          // 返回当前写入数据的副本
String toString()              // 使用平台默认编码转换为字符串
String toString(String charsetName) // 使用指定编码转换为字符串

// 写入到另一个输出流
void writeTo(OutputStream out)

// 重置(清空缓冲区)
void reset()

// 当前缓冲区大小
int size()

综合使用示例

java 复制代码
public class ByteArrayOutputStreamExample {
    public static void main(String[] args) {
        // 场景1:合并多个数据源
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            // 写入不同类型的数据
            baos.write("用户名: ".getBytes(StandardCharsets.UTF_8));
            baos.write("张三\n".getBytes(StandardCharsets.UTF_8));
            baos.write("年龄: ".getBytes(StandardCharsets.UTF_8));
            baos.write(String.valueOf(25).getBytes(StandardCharsets.UTF_8));
            
            // 获取合并后的数据
            byte[] combinedData = baos.toByteArray();
            System.out.println("合并结果:\n" + new String(combinedData, StandardCharsets.UTF_8));
            
            // 场景2:写入到文件
            try (FileOutputStream fos = new FileOutputStream("user.txt")) {
                baos.writeTo(fos);
            }
            
            // 场景3:重用同一个流
            baos.reset();
            baos.write("新的数据".getBytes(StandardCharsets.UTF_8));
            System.out.println("重置后: " + baos.toString(StandardCharsets.UTF_8.name()));
            
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 场景4:作为临时缓冲区处理网络数据
        byte[] networkData = simulateNetworkReceive();
        ByteArrayInputStream bais = new ByteArrayInputStream(networkData);
        processData(bais);
    }
    
    private static void processData(InputStream input) {
        // 处理输入流数据
        // 这里可以是解析协议、解压缩等操作
    }
    
    private static byte[] simulateNetworkReceive() {
        return "模拟的网络数据包".getBytes(StandardCharsets.UTF_8);
    }
}

3.3 其他内存操作流

除了字节数组流,Java还提供了字符数组流和字符串流:

  • CharArrayReader / CharArrayWriter:操作字符数组的字符流
  • StringReader / StringWriter:操作字符串的字符流

这些流在处理文本数据时更加方便,但本质上是为字符流设计的。


第四部分:对象序列化流

4.1 ObjectInputStream和ObjectOutputStream

4.1.1 基本概念

ObjectInputStreamObjectOutputStream是用于对象序列化的高级流,可以将Java对象转换为字节序列(序列化),或者将字节序列恢复为Java对象(反序列化)。

4.1.2 序列化要求
  • 对象类必须实现java.io.Serializable接口(标记接口,无方法需要实现)
  • 所有非静态、非瞬态(transient)字段都会被序列化
  • 如果父类没有实现Serializable,父类必须有默认构造函数
  • 序列化版本ID:private static final long serialVersionUID,用于版本控制
4.1.3 构造方法
java 复制代码
// 需要包装另一个输出流
ObjectOutputStream oos = new ObjectOutputStream(OutputStream out);

// 需要包装另一个输入流
ObjectInputStream ois = new ObjectInputStream(InputStream in);
4.1.4 核心方法

ObjectOutputStream

java 复制代码
// 写入对象
void writeObject(Object obj)

// 写入基本类型(实现了DataOutput接口)
void writeInt(int v)
void writeBoolean(boolean v)
void writeUTF(String str)  // 写入字符串(Modified UTF-8格式)

// 刷新(确保所有数据写入底层流)
void flush()

// 重置(清除已缓存的对象引用)
void reset()

ObjectInputStream

java 复制代码
// 读取对象
Object readObject()

// 读取基本类型
int readInt()
boolean readBoolean()
String readUTF()
4.1.5 使用示例

基本序列化示例

java 复制代码
import java.io.*;
import java.time.LocalDateTime;

// 可序列化的用户类
class User implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String username;
    private String password;        // 敏感信息
    private transient String token;  // 瞬态字段不序列化
    private LocalDateTime loginTime;
    
    public User(String username, String password) {
        this.username = username;
        this.password = password;
        this.loginTime = LocalDateTime.now();
    }
    
    @Override
    public String toString() {
        return "User{username='" + username + "', password='" + password + 
               "', token='" + token + "', loginTime=" + loginTime + '}';
    }
    
    // getters and setters
    public void setToken(String token) { this.token = token; }
}

public class SerializationExample {
    public static void main(String[] args) {
        String fileName = "user.ser";
        
        // 序列化
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(fileName))) {
            
            User user = new User("张三", "password123");
            user.setToken("auth-token-xyz");
            
            oos.writeObject(user);
            System.out.println("对象序列化完成: " + user);
            
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 反序列化
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream(fileName))) {
            
            User deserializedUser = (User) ois.readObject();
            System.out.println("对象反序列化完成: " + deserializedUser);
            // token字段为null,因为它被标记为transient
            
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
4.1.6 自定义序列化

类可以定义以下方法来自定义序列化行为:

java 复制代码
private void writeObject(ObjectOutputStream oos) throws IOException {
    // 自定义写入逻辑
    oos.defaultWriteObject(); // 写入默认字段
    // 写入额外的数据
}

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    // 自定义读取逻辑
    ois.defaultReadObject(); // 读取默认字段
    // 读取额外的数据并初始化
}

private void readObjectNoData() throws ObjectStreamException {
    // 当序列化流缺少该类的数据时调用
}
4.1.7 序列化注意事项
  1. 性能考虑:序列化会遍历整个对象图,对大对象图进行序列化可能较慢
  2. 安全考虑:反序列化可能执行恶意代码,始终验证输入来源
  3. 版本兼容:修改类结构可能导致反序列化失败,使用serialVersionUID控制兼容性
  4. 循环引用:ObjectOutputStream可以处理对象间的循环引用
  5. 关闭流:必须确保正确关闭,否则可能数据不完整

第五部分:数据流

5.1 DataInputStream和DataOutputStream

5.1.1 基本概念

DataInputStreamDataOutputStream提供了读写Java基本数据类型(int、long、float、double等)和字符串的方法,使得处理二进制数据更加方便。

5.1.2 特点
  • 平台无关:使用与机器无关的方式读写数据,保证数据在不同平台间可移植
  • 类型安全:可以直接读写基本类型,无需手动转换字节
  • 网络传输:常用于网络协议实现和二进制文件格式处理
5.1.3 构造方法
java 复制代码
// 包装另一个输入流
DataInputStream dis = new DataInputStream(InputStream in);

// 包装另一个输出流
DataOutputStream dos = new DataOutputStream(OutputStream out);
5.1.4 核心方法

DataOutputStream(实现了DataOutput接口):

java 复制代码
// 写入基本类型
void writeBoolean(boolean v)
void writeByte(int v)
void writeShort(int v)
void writeChar(int v)
void writeInt(int v)
void writeLong(long v)
void writeFloat(float v)
void writeDouble(double v)

// 写入字符串
void writeBytes(String s)      // 只写入每个字符的低字节
void writeChars(String s)      // 写入完整的字符(每个字符2字节)
void writeUTF(String str)      // 写入UTF-8格式字符串(先写入长度,再写入字节)

// 获取写入的字节数
int size()

DataInputStream(实现了DataInput接口):

java 复制代码
// 读取基本类型
boolean readBoolean()
byte readByte()
short readShort()
char readChar()
int readInt()
long readLong()
float readFloat()
double readDouble()

// 读取字符串
String readUTF()
5.1.5 使用示例
java 复制代码
public class DataStreamExample {
    public static void main(String[] args) {
        String fileName = "data.bin";
        
        // 写入不同类型的数据
        try (DataOutputStream dos = new DataOutputStream(
                new FileOutputStream(fileName))) {
            
            dos.writeInt(100);
            dos.writeDouble(3.14159);
            dos.writeBoolean(true);
            dos.writeUTF("Hello World");
            dos.writeChar('A');
            
            System.out.println("总共写入 " + dos.size() + " 字节");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 按相同顺序读取
        try (DataInputStream dis = new DataInputStream(
                new FileInputStream(fileName))) {
            
            int intValue = dis.readInt();
            double doubleValue = dis.readDouble();
            boolean boolValue = dis.readBoolean();
            String strValue = dis.readUTF();
            char charValue = dis.readChar();
            
            System.out.println("读取结果:");
            System.out.println("int: " + intValue);
            System.out.println("double: " + doubleValue);
            System.out.println("boolean: " + boolValue);
            System.out.println("String: " + strValue);
            System.out.println("char: " + charValue);
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
5.1.6 UTF-8字符串格式说明

writeUTF()方法写入的字符串格式有特定结构:

  • 前两个字节:字符串长度(以字节为单位)
  • 后续字节:字符串的Modified UTF-8编码

Modified UTF-8与标准UTF-8的区别:

  • 空字符('\u0000')编码为两个字节(0xC0, 0x80)而不是一个字节
  • 只支持1字节、2字节和3字节的字符(基本多语言平面)
  • 增补字符(surrogate pairs)编码为两个3字节序列

因此,用readUTF()读取时必须使用writeUTF()写入的格式。


第六部分:管道流

6.1 PipedInputStream和PipedOutputStream

6.1.1 基本概念

PipedInputStreamPipedOutputStream用于在同一JVM中的不同线程之间建立通信管道。一个线程通过PipedOutputStream写入数据,另一个线程通过PipedInputStream读取数据。

6.1.2 特点
  • 线程间通信:专为多线程环境设计
  • 阻塞操作:读空或写满时会阻塞线程
  • 有限缓冲区:默认缓冲区大小为1024字节
  • 一对一连接:一个输入流只能连接一个输出流
6.1.3 构造方法
java 复制代码
// 创建未连接的管道
PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream();

// 创建时连接
PipedInputStream pis = new PipedInputStream(PipedOutputStream src);
PipedOutputStream pos = new PipedOutputStream(PipedInputStream snk);

// 指定缓冲区大小
PipedInputStream pis = new PipedInputStream(int pipeSize);
PipedInputStream pis = new PipedOutputStream(PipedOutputStream src, int pipeSize);
6.1.4 连接方法
java 复制代码
// 连接输入流和输出流
pis.connect(PipedOutputStream src);
pos.connect(PipedInputStream snk);
6.1.5 使用示例

基本管道通信

java 复制代码
public class PipeExample {
    public static void main(String[] args) {
        try {
            // 创建管道流
            PipedOutputStream pos = new PipedOutputStream();
            PipedInputStream pis = new PipedInputStream(pos);
            
            // 写入线程
            Thread writerThread = new Thread(() -> {
                try {
                    String[] messages = {"消息1", "消息2", "消息3", "结束"};
                    for (String msg : messages) {
                        pos.write(msg.getBytes(StandardCharsets.UTF_8));
                        pos.write('\n'); // 添加换行符作为分隔
                        System.out.println("写入: " + msg);
                        Thread.sleep(1000); // 模拟延迟
                    }
                    pos.close(); // 关闭输出流,告诉读取线程数据结束
                } catch (IOException | InterruptedException e) {
                    e.printStackTrace();
                }
            });
            
            // 读取线程
            Thread readerThread = new Thread(() -> {
                try {
                    BufferedReader reader = new BufferedReader(
                            new InputStreamReader(pis, StandardCharsets.UTF_8));
                    String line;
                    while ((line = reader.readLine()) != null) {
                        System.out.println("读取: " + line);
                    }
                    pis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            
            writerThread.start();
            readerThread.start();
            
            // 等待线程完成
            writerThread.join();
            readerThread.join();
            
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

处理管道断开

java 复制代码
public class PipeDisconnectHandling {
    public static void main(String[] args) {
        try {
            PipedOutputStream pos = new PipedOutputStream();
            PipedInputStream pis = new PipedInputStream(pos);
            
            // 写入少量数据后关闭
            Thread writer = new Thread(() -> {
                try {
                    pos.write("部分数据".getBytes());
                    pos.close(); // 提前关闭
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            
            // 延迟读取
            Thread reader = new Thread(() -> {
                try {
                    Thread.sleep(2000); // 等待写入线程结束
                    byte[] buffer = new byte[1024];
                    int bytesRead = pis.read(buffer);
                    if (bytesRead > 0) {
                        System.out.println("读取到: " + new String(buffer, 0, bytesRead));
                    }
                    
                    // 尝试再次读取
                    bytesRead = pis.read(buffer);
                    System.out.println("第二次读取结果: " + bytesRead); // 应为-1
                    
                } catch (IOException | InterruptedException e) {
                    System.err.println("管道错误: " + e.getMessage());
                } finally {
                    try {
                        pis.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
            
            writer.start();
            reader.start();
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
6.1.6 注意事项
  1. 必须在多线程环境使用:单线程中使用可能导致死锁
  2. 避免死锁:读写线程互相等待可能导致死锁
  3. 缓冲区满:默认缓冲区较小,大数据传输时需调整
  4. 流关闭:正确关闭流,避免资源泄漏

第七部分:回退输入流

7.1 PushbackInputStream

7.1.1 基本概念

PushbackInputStream是一个特殊的输入流装饰器,允许将读取的字节推回到流中,以便重新读取。这在解析器实现中特别有用,比如需要预读数据来决定下一步操作。

7.1.2 工作原理

PushbackInputStream内部维护一个回退缓冲区(字节数组)。当调用unread()方法时,数据被写入这个缓冲区。读取操作优先从回退缓冲区读取数据,只有当缓冲区为空时才从底层输入流读取。

7.1.3 构造方法
java 复制代码
// 使用默认回退缓冲区大小(1字节)
PushbackInputStream pbis = new PushbackInputStream(InputStream in);

// 指定回退缓冲区大小
PushbackInputStream pbis = new PushbackInputStream(InputStream in, int size);
7.1.4 核心方法
java 复制代码
// 推回一个字节(可以多次调用)
void unread(int b) throws IOException

// 推回整个字节数组
void unread(byte[] b) throws IOException

// 推回字节数组的一部分
void unread(byte[] b, int off, int len) throws IOException

// 可用字节数(回退缓冲区中的字节数 + 底层流可用字节数)
int available() throws IOException
7.1.5 使用示例

简单的解析器示例

java 复制代码
public class PushbackParserExample {
    public static void main(String[] args) {
        String data = "123+456";
        
        try (PushbackInputStream pbis = new PushbackInputStream(
                new ByteArrayInputStream(data.getBytes()), 2)) {
            
            // 读取第一个数字
            int num1 = readNumber(pbis);
            System.out.println("第一个数字: " + num1);
            
            // 预读运算符
            int next = pbis.read();
            if (next == '+') {
                System.out.println("遇到加号运算符");
                
                // 读取第二个数字
                int num2 = readNumber(pbis);
                System.out.println("第二个数字: " + num2);
                System.out.println("结果: " + (num1 + num2));
            } else {
                // 如果不是运算符,推回流中
                pbis.unread(next);
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    // 读取连续的数字字符,返回整数
    private static int readNumber(PushbackInputStream pbis) throws IOException {
        StringBuilder sb = new StringBuilder();
        int b;
        
        while ((b = pbis.read()) != -1) {
            if (b >= '0' && b <= '9') {
                sb.append((char) b);
            } else {
                // 遇到非数字字符,推回流中
                pbis.unread(b);
                break;
            }
        }
        
        return sb.length() > 0 ? Integer.parseInt(sb.toString()) : 0;
    }
}

复杂的分词器示例

java 复制代码
public class SimpleTokenizer {
    private PushbackInputStream input;
    private static final int BUFFER_SIZE = 1024;
    
    public SimpleTokenizer(InputStream input) {
        this.input = new PushbackInputStream(input, BUFFER_SIZE);
    }
    
    public String nextToken() throws IOException {
        skipWhitespace();
        
        int b = input.read();
        if (b == -1) {
            return null; // EOF
        }
        
        // 处理不同类型的token
        if (isDigit(b)) {
            return readNumber(b);
        } else if (isLetter(b)) {
            return readWord(b);
        } else if (isOperator(b)) {
            return readOperator(b);
        } else {
            // 单个字符token
            return String.valueOf((char) b);
        }
    }
    
    private String readNumber(int firstDigit) throws IOException {
        StringBuilder sb = new StringBuilder();
        sb.append((char) firstDigit);
        
        while (true) {
            int b = input.read();
            if (b == -1 || !isDigit(b)) {
                if (b != -1) {
                    input.unread(b); // 推回非数字字符
                }
                break;
            }
            sb.append((char) b);
        }
        
        return sb.toString();
    }
    
    private String readWord(int firstLetter) throws IOException {
        StringBuilder sb = new StringBuilder();
        sb.append((char) firstLetter);
        
        while (true) {
            int b = input.read();
            if (b == -1 || !isLetterOrDigit(b)) {
                if (b != -1) {
                    input.unread(b);
                }
                break;
            }
            sb.append((char) b);
        }
        
        return sb.toString();
    }
    
    private String readOperator(int firstChar) throws IOException {
        // 处理多字符运算符,如 ==, !=, <=, >= 等
        StringBuilder sb = new StringBuilder();
        sb.append((char) firstChar);
        
        int next = input.read();
        if (next != -1) {
            String possibleOp = sb.toString() + (char) next;
            if (isMultiCharOperator(possibleOp)) {
                sb.append((char) next);
            } else {
                input.unread(next);
            }
        }
        
        return sb.toString();
    }
    
    private void skipWhitespace() throws IOException {
        int b;
        while ((b = input.read()) != -1 && Character.isWhitespace(b)) {
            // 跳过空白字符
        }
        if (b != -1) {
            input.unread(b);
        }
    }
    
    private boolean isDigit(int b) {
        return b >= '0' && b <= '9';
    }
    
    private boolean isLetter(int b) {
        return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z');
    }
    
    private boolean isLetterOrDigit(int b) {
        return isLetter(b) || isDigit(b) || b == '_';
    }
    
    private boolean isOperator(int b) {
        return b == '+' || b == '-' || b == '*' || b == '/' || 
               b == '=' || b == '<' || b == '>' || b == '!';
    }
    
    private boolean isMultiCharOperator(String op) {
        return op.equals("==") || op.equals("!=") || 
               op.equals("<=") || op.equals(">=");
    }
    
    public static void main(String[] args) {
        String code = "if (x <= 100) { result = 42; }";
        
        try (InputStream is = new ByteArrayInputStream(code.getBytes())) {
            SimpleTokenizer tokenizer = new SimpleTokenizer(is);
            String token;
            while ((token = tokenizer.nextToken()) != null) {
                System.out.println("Token: " + token);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
7.1.6 性能考虑
  • 回退缓冲区使用从后往前填充的方式,这样读取时可以从前往后访问
  • 缓冲区满时会抛出IOException
  • 频繁的unread操作会增加内存复制开销

第八部分:打印流

8.1 PrintStream

8.1.1 基本概念

PrintStream是一个功能丰富的输出流装饰器,提供了打印各种数据类型的便利方法。最著名的PrintStream实例就是System.outSystem.err

8.1.2 特点
  • 便利的输出方法 :提供重载的print()println()方法
  • 格式化输出 :支持printf()format()方法
  • 不抛出IOException :异常被内部捕获,可通过checkError()检查状态
  • 自动刷新:可配置为遇到换行符时自动刷新
  • 字符编码:支持指定字符编码
8.1.3 构造方法
java 复制代码
// 包装OutputStream
PrintStream ps = new PrintStream(OutputStream out);
PrintStream ps = new PrintStream(OutputStream out, boolean autoFlush);
PrintStream ps = new PrintStream(OutputStream out, boolean autoFlush, String encoding);

// 直接创建文件流
PrintStream ps = new PrintStream(String fileName);
PrintStream ps = new PrintStream(String fileName, String encoding);
PrintStream ps = new PrintStream(File file);
8.1.4 核心方法

打印方法

java 复制代码
// 打印不换行
void print(boolean b)
void print(char c)
void print(int i)
void print(long l)
void print(float f)
void print(double d)
void print(char[] s)
void print(String s)
void print(Object obj)

// 打印并换行
void println()
void println(boolean x)
// ... 各种类型的println重载

// 格式化输出
PrintStream printf(String format, Object... args)
PrintStream format(String format, Object... args)

错误检查

java 复制代码
// 检查是否发生错误
boolean checkError()

// 清除错误状态(protected方法)
protected void clearError()
8.1.5 使用示例

基本输出

java 复制代码
public class PrintStreamBasicExample {
    public static void main(String[] args) {
        try (PrintStream ps = new PrintStream("output.txt", "UTF-8")) {
            
            // 各种打印方法
            ps.print(true);
            ps.print(' ');
            ps.print(123);
            ps.print(' ');
            ps.print(3.14159);
            ps.println(); // 换行
            
            ps.println("这是一行文本");
            ps.println(new Object());
            
            // 格式化输出
            ps.printf("姓名:%s,年龄:%d,身高:%.2f米%n", 
                     "张三", 25, 1.75);
            
            // 格式化并返回PrintStream,支持链式调用
            ps.format("圆周率:%.10f%n", Math.PI)
              .format("当前时间:%tF %<tT%n", new Date());
            
            // 检查错误状态
            if (ps.checkError()) {
                System.err.println("写入过程中发生错误");
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

重定向System.out

java 复制代码
public class RedirectSystemOutExample {
    public static void main(String[] args) {
        // 保存原始System.out
        PrintStream originalOut = System.out;
        
        try {
            // 创建文件输出流
            PrintStream fileOut = new PrintStream(
                new FileOutputStream("console.log", true), 
                true,  // 自动刷新
                "UTF-8"
            );
            
            // 重定向System.out
            System.setOut(fileOut);
            
            // 这些输出会写入文件
            System.out.println("这是写入文件的第一行");
            System.out.printf("当前时间:%tF %<tT%n", new Date());
            
            // 创建日志记录
            logMessage("系统启动");
            logMessage("用户登录");
            
            System.out.println("写入完成");
            
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 恢复原始System.out
            System.setOut(originalOut);
            System.out.println("已恢复控制台输出");
        }
    }
    
    private static void logMessage(String message) {
        // 此时System.out已被重定向
        System.out.printf("[%tF %<tT] %s%n", new Date(), message);
    }
}

自定义日志类

java 复制代码
public class Logger {
    private PrintStream out;
    private PrintStream err;
    private boolean debugEnabled;
    
    public Logger(String logFile) throws IOException {
        // 普通日志输出到文件
        this.out = new PrintStream(
            new FileOutputStream(logFile, true),
            true,
            "UTF-8"
        );
        
        // 错误日志同时输出到文件和标准错误
        this.err = new PrintStream(
            new TeeOutputStream(System.err, 
                new FileOutputStream(logFile + ".err", true)),
            true,
            "UTF-8"
        );
    }
    
    public void info(String format, Object... args) {
        out.printf("[INFO] " + format + "%n", args);
    }
    
    public void error(String format, Object... args) {
        err.printf("[ERROR] " + format + "%n", args);
    }
    
    public void debug(String format, Object... args) {
        if (debugEnabled) {
            out.printf("[DEBUG] " + format + "%n", args);
        }
    }
    
    public void setDebugEnabled(boolean enabled) {
        this.debugEnabled = enabled;
    }
    
    public void close() {
        out.close();
        err.close();
    }
    
    // 辅助类:将输出同时写入两个流
    private static class TeeOutputStream extends OutputStream {
        private OutputStream out1;
        private OutputStream out2;
        
        public TeeOutputStream(OutputStream out1, OutputStream out2) {
            this.out1 = out1;
            this.out2 = out2;
        }
        
        @Override
        public void write(int b) throws IOException {
            out1.write(b);
            out2.write(b);
        }
        
        @Override
        public void flush() throws IOException {
            out1.flush();
            out2.flush();
        }
        
        @Override
        public void close() throws IOException {
            try {
                out1.close();
            } finally {
                out2.close();
            }
        }
    }
}
8.1.6 重要注意事项
  1. 异常处理 :PrintStream不会抛出IOException,必须通过checkError()检查错误
  2. 性能考虑:频繁的print()调用仍然会加锁,多线程环境下可以考虑缓冲
  3. 自动刷新:启用autoFlush时,写入字节数组、调用println()或遇到'\n'时会自动刷新
  4. 字符编码:处理非ASCII字符时必须指定正确的编码

第九部分:过滤流

9.1 FilterInputStream和FilterOutputStream

9.1.1 基本概念

FilterInputStreamFilterOutputStream是装饰器模式中的装饰者基类。它们本身不添加额外功能,只是简单地将所有方法调用转发给底层流。所有装饰器类(如BufferedInputStream、DataInputStream)都继承自这些过滤流。

9.1.2 自定义过滤流示例
java 复制代码
// 自定义过滤流:将读取的字节转换为大写
public class UpperCaseInputStream extends FilterInputStream {
    
    protected UpperCaseInputStream(InputStream in) {
        super(in);
    }
    
    @Override
    public int read() throws IOException {
        int b = super.read();
        return (b == -1) ? -1 : Character.toUpperCase(b);
    }
    
    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int bytesRead = super.read(b, off, len);
        if (bytesRead > 0) {
            for (int i = off; i < off + bytesRead; i++) {
                b[i] = (byte) Character.toUpperCase(b[i] & 0xFF);
            }
        }
        return bytesRead;
    }
}

// 自定义过滤流:计算写入数据的CRC32校验和
public class CRCOutputStream extends FilterOutputStream {
    private CRC32 crc = new CRC32();
    private long bytesWritten = 0;
    
    public CRCOutputStream(OutputStream out) {
        super(out);
    }
    
    @Override
    public void write(int b) throws IOException {
        out.write(b);
        crc.update(b);
        bytesWritten++;
    }
    
    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        out.write(b, off, len);
        crc.update(b, off, len);
        bytesWritten += len;
    }
    
    public long getCRCValue() {
        return crc.getValue();
    }
    
    public long getBytesWritten() {
        return bytesWritten;
    }
}

// 使用示例
public class CustomFilterExample {
    public static void main(String[] args) {
        String data = "hello world";
        
        // 使用UpperCaseInputStream
        try (InputStream is = new UpperCaseInputStream(
                new ByteArrayInputStream(data.getBytes()))) {
            
            byte[] buffer = new byte[1024];
            int bytesRead = is.read(buffer);
            System.out.println("大写转换结果: " + 
                new String(buffer, 0, bytesRead));
            
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 使用CRCOutputStream
        try (CRCOutputStream cos = new CRCOutputStream(
                new FileOutputStream("test.dat"))) {
            
            cos.write(data.getBytes());
            cos.write("\n第二行".getBytes());
            
            System.out.println("写入字节数: " + cos.getBytesWritten());
            System.out.println("CRC32校验和: " + Long.toHexString(cos.getCRCValue()));
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

第十部分:序列输入流

10.1 SequenceInputStream

10.1.1 基本概念

SequenceInputStream可以将多个输入流连接成一个输入流,按顺序读取每个流,直到所有流都读完。这在处理分段文件、合并多个数据源时非常有用。

10.1.2 构造方法
java 复制代码
// 使用枚举
SequenceInputStream sis = new SequenceInputStream(Enumeration<? extends InputStream> e);

// 使用两个流
SequenceInputStream sis = new SequenceInputStream(InputStream s1, InputStream s2);
10.1.3 使用示例
java 复制代码
public class SequenceInputStreamExample {
    public static void main(String[] args) {
        // 准备多个数据源
        String[] data = {"第一部分数据\n", "第二部分数据\n", "第三部分数据\n"};
        List<InputStream> streams = new ArrayList<>();
        
        for (String d : data) {
            streams.add(new ByteArrayInputStream(d.getBytes()));
        }
        
        // 创建枚举
        Enumeration<InputStream> enumeration = Collections.enumeration(streams);
        
        // 合并流
        try (SequenceInputStream sis = new SequenceInputStream(enumeration);
             FileOutputStream fos = new FileOutputStream("combined.txt")) {
            
            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = sis.read(buffer)) != -1) {
                fos.write(buffer, 0, bytesRead);
            }
            
            System.out.println("所有数据已合并到 combined.txt");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 读取合并后的文件验证
        try (FileInputStream fis = new FileInputStream("combined.txt")) {
            byte[] content = fis.readAllBytes();
            System.out.println("合并文件内容:\n" + new String(content));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
10.1.4 注意事项
  • 关闭SequenceInputStream会自动关闭所有包含的输入流
  • 适合处理流式数据,不适合随机访问
  • 可以用于分割文件的重新组装

第十一部分:校验和流

11.1 CheckedInputStream和CheckedOutputStream

11.1.1 基本概念

CheckedInputStreamCheckedOutputStream是Java标准库中用于计算数据校验和的过滤流。它们在读写数据的同时更新校验和,常用于数据完整性验证。

11.1.2 构造方法
java 复制代码
// 需要指定Checksum实现
CheckedInputStream cis = new CheckedInputStream(InputStream in, Checksum cksum);
CheckedOutputStream cos = new CheckedOutputStream(OutputStream out, Checksum cksum);

// 获取校验和
Checksum getChecksum()
11.1.3 Checksum实现类
  • CRC32:32位循环冗余校验
  • Adler32:比CRC32更快但可靠性略低的校验算法
  • 可自定义实现Checksum接口
11.1.4 使用示例
java 复制代码
import java.util.zip.CRC32;
import java.util.zip.Adler32;
import java.util.zip.CheckedInputStream;
import java.util.zip.CheckedOutputStream;

public class ChecksumExample {
    
    // 计算文件的CRC32校验和
    public static long calculateCRC32(String filePath) throws IOException {
        try (CheckedInputStream cis = new CheckedInputStream(
                new FileInputStream(filePath), new CRC32())) {
            
            byte[] buffer = new byte[8192];
            while (cis.read(buffer) != -1) {
                // 持续读取,校验和在内部自动更新
            }
            
            return cis.getChecksum().getValue();
        }
    }
    
    // 复制文件并同时计算校验和
    public static long copyWithChecksum(String source, String dest) throws IOException {
        try (CheckedInputStream cis = new CheckedInputStream(
                new FileInputStream(source), new CRC32());
             FileOutputStream fos = new FileOutputStream(dest)) {
            
            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = cis.read(buffer)) != -1) {
                fos.write(buffer, 0, bytesRead);
            }
            
            return cis.getChecksum().getValue();
        }
    }
    
    // 写入数据并计算校验和
    public static void writeWithChecksum(String filePath, byte[] data) throws IOException {
        try (CheckedOutputStream cos = new CheckedOutputStream(
                new FileOutputStream(filePath), new Adler32())) {
            
            cos.write(data);
            cos.flush();
            
            long checksum = cos.getChecksum().getValue();
            System.out.printf("写入完成,Adler32校验和: 0x%x%n", checksum);
            
            // 将校验和单独保存
            try (FileOutputStream ckFile = new FileOutputStream(filePath + ".cksum")) {
                DataOutputStream dos = new DataOutputStream(ckFile);
                dos.writeLong(checksum);
            }
        }
    }
    
    public static void main(String[] args) {
        try {
            String testFile = "testdata.bin";
            
            // 生成测试数据
            byte[] data = new byte[1024 * 1024]; // 1MB
            new Random().nextBytes(data);
            
            // 写入数据并计算校验和
            writeWithChecksum(testFile, data);
            
            // 读取文件并验证校验和
            long calculatedCRC = calculateCRC32(testFile);
            System.out.printf("读取文件的CRC32校验和: 0x%x%n", calculatedCRC);
            
            // 复制文件并验证校验和是否一致
            String copyFile = "copy_" + testFile;
            long copyCRC = copyWithChecksum(testFile, copyFile);
            System.out.printf("复制文件的CRC32校验和: 0x%x%n", copyCRC);
            
            if (calculatedCRC == copyCRC) {
                System.out.println("校验和一致,文件复制正确");
            } else {
                System.out.println("校验和不一致,文件可能损坏");
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

第十二部分:压缩流

12.1 GZIPInputStream和GZIPOutputStream

12.1.1 基本概念

Java的java.util.zip包提供了多种压缩和解压缩流,其中最常用的是GZIPInputStreamGZIPOutputStream,它们实现了GZIP文件格式的压缩和解压缩。

12.1.2 构造方法
java 复制代码
// 压缩输出流
GZIPOutputStream gzos = new GZIPOutputStream(OutputStream out);
GZIPOutputStream gzos = new GZIPOutputStream(OutputStream out, int size); // 指定缓冲区大小

// 解压缩输入流
GZIPInputStream gzis = new GZIPInputStream(InputStream in);
GZIPInputStream gzis = new GZIPInputStream(InputStream in, int size);
12.1.3 使用示例
java 复制代码
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

public class GZIPExample {
    
    // 压缩文件
    public static void compressFile(String sourceFile, String gzipFile) throws IOException {
        try (FileInputStream fis = new FileInputStream(sourceFile);
             FileOutputStream fos = new FileOutputStream(gzipFile);
             GZIPOutputStream gzos = new GZIPOutputStream(fos)) {
            
            byte[] buffer = new byte[8192];
            int bytesRead;
            
            while ((bytesRead = fis.read(buffer)) != -1) {
                gzos.write(buffer, 0, bytesRead);
            }
            
            // GZIPOutputStream需要finish()来写入压缩数据尾部
            gzos.finish();
            
            System.out.println("文件已压缩: " + gzipFile);
        }
    }
    
    // 解压缩文件
    public static void decompressFile(String gzipFile, String outputFile) throws IOException {
        try (GZIPInputStream gzis = new GZIPInputStream(new FileInputStream(gzipFile));
             FileOutputStream fos = new FileOutputStream(outputFile)) {
            
            byte[] buffer = new byte[8192];
            int bytesRead;
            
            while ((bytesRead = gzis.read(buffer)) != -1) {
                fos.write(buffer, 0, bytesRead);
            }
            
            System.out.println("文件已解压缩: " + outputFile);
        }
    }
    
    // 压缩数据流
    public static byte[] compressData(byte[] data) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (GZIPOutputStream gzos = new GZIPOutputStream(baos)) {
            gzos.write(data);
            gzos.finish();
        }
        return baos.toByteArray();
    }
    
    // 解压缩数据流
    public static byte[] decompressData(byte[] compressedData) throws IOException {
        ByteArrayInputStream bais = new ByteArrayInputStream(compressedData);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        
        try (GZIPInputStream gzis = new GZIPInputStream(bais)) {
            byte[] buffer = new byte[8192];
            int bytesRead;
            
            while ((bytesRead = gzis.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesRead);
            }
        }
        
        return baos.toByteArray();
    }
    
    public static void main(String[] args) {
        try {
            // 创建测试文件
            String testFile = "test.txt";
            try (FileWriter fw = new FileWriter(testFile)) {
                for (int i = 0; i < 10000; i++) {
                    fw.write("这是一行测试文本,用于演示GZIP压缩功能。\n");
                }
            }
            
            // 压缩文件
            String gzipFile = "test.txt.gz";
            compressFile(testFile, gzipFile);
            
            // 检查压缩效果
            File original = new File(testFile);
            File compressed = new File(gzipFile);
            System.out.printf("原始大小: %d bytes%n", original.length());
            System.out.printf("压缩后大小: %d bytes%n", compressed.length());
            System.out.printf("压缩率: %.2f%%%n", 
                (1 - (double)compressed.length() / original.length()) * 100);
            
            // 解压缩文件
            String decompressedFile = "decompressed.txt";
            decompressFile(gzipFile, decompressedFile);
            
            // 验证解压缩后内容相同
            String originalContent = new String(Files.readAllBytes(original.toPath()));
            String decompressedContent = new String(Files.readAllBytes(
                new File(decompressedFile).toPath()));
            
            System.out.println("内容相同: " + originalContent.equals(decompressedContent));
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

12.2 ZipInputStream和ZipOutputStream

12.2.1 基本概念

ZipInputStreamZipOutputStream用于处理ZIP格式的压缩文件,支持包含多个条目的ZIP档案。

12.2.2 使用示例
java 复制代码
import java.util.zip.*;

public class ZipExample {
    
    // 创建ZIP文件
    public static void createZipFile(String zipFileName, Map<String, byte[]> files) 
            throws IOException {
        try (ZipOutputStream zos = new ZipOutputStream(
                new FileOutputStream(zipFileName))) {
            
            for (Map.Entry<String, byte[]> entry : files.entrySet()) {
                // 创建ZIP条目
                ZipEntry zipEntry = new ZipEntry(entry.getKey());
                zos.putNextEntry(zipEntry);
                
                // 写入文件数据
                zos.write(entry.getValue());
                
                // 关闭当前条目
                zos.closeEntry();
            }
            
            System.out.println("ZIP文件创建完成: " + zipFileName);
        }
    }
    
    // 读取ZIP文件
    public static void readZipFile(String zipFileName) throws IOException {
        try (ZipInputStream zis = new ZipInputStream(
                new FileInputStream(zipFileName))) {
            
            ZipEntry entry;
            byte[] buffer = new byte[8192];
            
            while ((entry = zis.getNextEntry()) != null) {
                System.out.println("读取条目: " + entry.getName());
                System.out.println("  大小: " + entry.getSize() + " bytes");
                System.out.println("  压缩后大小: " + entry.getCompressedSize() + " bytes");
                System.out.println("  方法: " + (entry.getMethod() == ZipEntry.DEFLATED ? 
                    "DEFLATED" : "STORED"));
                
                // 读取条目内容
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                int bytesRead;
                while ((bytesRead = zis.read(buffer)) != -1) {
                    baos.write(buffer, 0, bytesRead);
                }
                
                System.out.println("  内容长度: " + baos.size() + " bytes");
                System.out.println();
                
                zis.closeEntry();
            }
        }
    }
    
    // 使用ZipFile随机访问ZIP条目(推荐用于大量文件)
    public static void readWithZipFile(String zipFileName) throws IOException {
        try (ZipFile zipFile = new ZipFile(zipFileName)) {
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                System.out.println("ZIP条目: " + entry.getName());
                
                // 直接获取输入流读取内容
                try (InputStream is = zipFile.getInputStream(entry)) {
                    byte[] content = is.readAllBytes();
                    System.out.println("  内容大小: " + content.length);
                }
            }
        }
    }
    
    public static void main(String[] args) {
        try {
            // 准备要压缩的文件
            Map<String, byte[]> files = new HashMap<>();
            files.put("document.txt", "这是文档内容".getBytes(StandardCharsets.UTF_8));
            files.put("data.bin", new byte[1000]);
            files.put("subdir/notes.txt", "这是子目录中的笔记".getBytes(StandardCharsets.UTF_8));
            
            // 创建ZIP文件
            String zipFile = "archive.zip";
            createZipFile(zipFile, files);
            
            // 读取ZIP文件
            System.out.println("\n使用ZipInputStream读取:");
            readZipFile(zipFile);
            
            System.out.println("\n使用ZipFile读取:");
            readWithZipFile(zipFile);
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

第十三部分:组合流与最佳实践

13.1 流的装饰器模式

Java I/O库广泛使用了装饰器模式(Decorator Pattern),允许动态地为流添加功能。这种设计使得组合不同功能的流变得非常简单。

java 复制代码
// 一个典型的组合流示例
InputStream input = new BufferedInputStream(  // 添加缓冲功能
                     new GZIPInputStream(      // 添加解压缩功能
                       new FileInputStream("data.gz"))); // 基础文件输入

13.2 常用组合模式

java 复制代码
public class StreamCombinations {
    
    // 高效读取压缩的序列化对象
    public static Object readCompressedObject(String file) 
            throws IOException, ClassNotFoundException {
        try (ObjectInputStream ois = new ObjectInputStream(
                new BufferedInputStream(
                    new GZIPInputStream(
                        new FileInputStream(file))))) {
            return ois.readObject();
        }
    }
    
    // 写入带校验和的压缩数据
    public static void writeCompressedWithChecksum(String file, byte[] data) 
            throws IOException {
        try (CheckedOutputStream cos = new CheckedOutputStream(
                new GZIPOutputStream(
                    new BufferedOutputStream(
                        new FileOutputStream(file))), 
                new CRC32())) {
            
            cos.write(data);
            cos.flush();
            
            System.out.printf("数据CRC32: 0x%x%n", cos.getChecksum().getValue());
        }
    }
    
    // 网络数据传输组合
    public static void sendObjectOverNetwork(Socket socket, Object obj) 
            throws IOException {
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new BufferedOutputStream(
                    socket.getOutputStream()))) {
            
            oos.writeObject(obj);
            oos.flush();
        }
    }
    
    // 读取配置文件(使用PushbackInputStream处理注释)
    public static Properties readConfig(String file) throws IOException {
        Properties props = new Properties();
        
        try (PushbackInputStream pbis = new PushbackInputStream(
                new BufferedInputStream(
                    new FileInputStream(file)), 2)) {
            
            StringBuilder currentLine = new StringBuilder();
            int b;
            
            while ((b = pbis.read()) != -1) {
                // 检查是否是注释行(以#开头)
                if (b == '#') {
                    // 跳过整行
                    while ((b = pbis.read()) != -1 && b != '\n') {
                        // 跳过注释内容
                    }
                    continue;
                }
                
                // 处理非注释行
                currentLine.append((char) b);
                
                // 如果遇到换行符,处理这一行
                if (b == '\n') {
                    String line = currentLine.toString().trim();
                    if (!line.isEmpty()) {
                        // 解析属性行
                        String[] parts = line.split("=", 2);
                        if (parts.length == 2) {
                            props.setProperty(parts[0].trim(), parts[1].trim());
                        }
                    }
                    currentLine = new StringBuilder();
                }
            }
        }
        
        return props;
    }
}

13.3 性能优化最佳实践

13.3.1 缓冲区大小选择
java 复制代码
public class BufferSizeOptimization {
    
    // 根据设备特性选择缓冲区大小
    public static int getOptimalBufferSize() {
        // 对于HDD,建议64KB
        // 对于SSD,建议16KB-32KB
        // 对于网络流,建议8KB-16KB
        
        String os = System.getProperty("os.name").toLowerCase();
        if (os.contains("linux")) {
            // Linux通常使用4KB页大小,缓冲区设为4KB的倍数
            return 8192; // 8KB
        } else if (os.contains("win")) {
            // Windows通常使用64KB的簇大小
            return 65536; // 64KB
        } else {
            return 16384; // 16KB默认值
        }
    }
    
    // 智能缓冲流创建
    public static InputStream createOptimizedInputStream(File file) 
            throws IOException {
        int bufferSize = getOptimalBufferSize();
        return new BufferedInputStream(
            new FileInputStream(file), bufferSize);
    }
}
13.3.2 避免常见陷阱
java 复制代码
public class CommonPitfalls {
    
    // 错误:没有使用缓冲
    public static void badCopy(File source, File dest) throws IOException {
        try (FileInputStream fis = new FileInputStream(source);
             FileOutputStream fos = new FileOutputStream(dest)) {
            
            int b;
            while ((b = fis.read()) != -1) {  // 单字节读取,性能极差
                fos.write(b);
            }
        }
    }
    
    // 正确:使用缓冲
    public static void goodCopy(File source, File dest) throws IOException {
        try (BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream(source));
             BufferedOutputStream bos = new BufferedOutputStream(
                new FileOutputStream(dest))) {
            
            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, bytesRead);
            }
        }
    }
    
    // 错误:不正确的编码处理
    public static void badEncodingHandling(String file) throws IOException {
        try (FileInputStream fis = new FileInputStream(file)) {
            byte[] data = fis.readAllBytes();
            String text = new String(data); // 使用平台默认编码,可能乱码
            System.out.println(text);
        }
    }
    
    // 正确:指定编码
    public static void goodEncodingHandling(String file) throws IOException {
        try (FileInputStream fis = new FileInputStream(file)) {
            byte[] data = fis.readAllBytes();
            String text = new String(data, StandardCharsets.UTF_8);
            System.out.println(text);
        }
    }
    
    // 错误:关闭顺序不当
    public static void badCloseOrder(String file) throws IOException {
        GZIPOutputStream gzos = null;
        FileOutputStream fos = null;
        
        try {
            fos = new FileOutputStream(file);
            gzos = new GZIPOutputStream(fos);
            gzos.write("data".getBytes());
        } finally {
            gzos.close(); // 先关闭包装流
            fos.close();  // 后关闭底层流 - GZIPOutputStream已经关闭了它
        }
    }
    
    // 正确:try-with-resources自动处理
    public static void goodCloseOrder(String file) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(file);
             GZIPOutputStream gzos = new GZIPOutputStream(fos)) {
            
            gzos.write("data".getBytes());
        } // 自动关闭,顺序正确(后创建的先关闭)
    }
}

13.4 异常处理最佳实践

java 复制代码
public class ExceptionHandlingBestPractices {
    
    // 完整的异常处理模式
    public static void safeFileCopy(File source, File dest) {
        // 使用try-with-resources自动关闭资源(JDK 7+)
        try (InputStream in = new BufferedInputStream(new FileInputStream(source));
             OutputStream out = new BufferedOutputStream(new FileOutputStream(dest))) {
            
            byte[] buffer = new byte[8192];
            int bytesRead;
            
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
            
            out.flush(); // 确保所有数据写入
            
        } catch (FileNotFoundException e) {
            System.err.println("文件未找到: " + e.getMessage());
            // 根据业务需求处理,可能是创建文件或抛出业务异常
        } catch (IOException e) {
            System.err.println("I/O错误: " + e.getMessage());
            // 记录日志,可能重试或通知用户
            e.printStackTrace();
        } catch (SecurityException e) {
            System.err.println("没有权限访问文件: " + e.getMessage());
            // 权限不足的处理
        }
    }
    
    // 处理部分写入的情况
    public static void robustWrite(File file, byte[] data) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(file)) {
            int written = 0;
            int remaining = data.length;
            
            while (remaining > 0) {
                int bytesToWrite = Math.min(remaining, 8192);
                fos.write(data, written, bytesToWrite);
                written += bytesToWrite;
                remaining -= bytesToWrite;
            }
            
            System.out.printf("成功写入 %d 字节到 %s%n", written, file);
        }
    }
    
    // 使用finally确保资源关闭(JDK 6及之前版本)
    public static void legacyResourceHandling(String file) {
        InputStream in = null;
        try {
            in = new FileInputStream(file);
            // 处理数据
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    // 记录关闭异常,但不应掩盖原始异常
                    e.printStackTrace();
                }
            }
        }
    }
}

第十四部分:总结与选择指南

14.1 实现类对比表

实现类 类型 主要用途 特点 适用场景
FileInputStream/FileOutputStream 节点流 文件读写 基础文件操作 所有文件I/O的基础
BufferedInputStream/BufferedOutputStream 处理流 缓冲功能 提高性能,减少I/O次数 所有需要提高性能的场景
ByteArrayInputStream/ByteArrayOutputStream 节点流 内存操作 速度快,无需关闭 临时数据存储、数据转换
ObjectInputStream/ObjectOutputStream 处理流 对象序列化 保存和恢复对象 对象持久化、RMI
DataInputStream/DataOutputStream 处理流 基本类型读写 平台无关的二进制数据 网络协议、二进制文件格式
PipedInputStream/PipedOutputStream 节点流 线程间通信 阻塞式管道通信 生产者-消费者模式
PushbackInputStream 处理流 回退功能 允许重新读取 语法解析、协议解析
PrintStream 处理流 格式化输出 方便的打印方法 日志输出、System.out
SequenceInputStream 处理流 流合并 合并多个输入流 文件合并、分段数据处理
GZIPInputStream/GZIPOutputStream 处理流 数据压缩 GZIP格式压缩 文件压缩、网络传输优化
ZipInputStream/ZipOutputStream 处理流 ZIP文件处理 支持多条目 ZIP档案操作
CheckedInputStream/CheckedOutputStream 处理流 校验和计算 数据完整性验证 文件传输验证、存储校验

14.2 选择指南

根据不同的使用场景,推荐以下选择策略:

文件读写场景

  • 小文件:直接使用FileInputStream/FileOutputStream
  • 大文件:使用BufferedInputStream/BufferedOutputStream包装
  • 需要频繁读写:始终使用缓冲流

内存数据处理

  • 临时数据:使用ByteArrayInputStream/ByteArrayOutputStream
  • 数据格式转换:内存流作为中间缓冲区

数据持久化

  • 对象存储:使用ObjectInputStream/ObjectOutputStream
  • 基本类型数据:使用DataInputStream/DataOutputStream
  • 文本数据:使用字符流(Reader/Writer)

网络通信

  • 高效传输:组合BufferedOutputStream和DataOutputStream
  • 对象传输:组合ObjectOutputStream和缓冲流
  • 压缩传输:组合GZIPOutputStream和缓冲流

多线程编程

  • 线程间数据交换:使用PipedInputStream/PipedOutputStream
  • 注意:必须在不同线程中使用,避免死锁

特殊需求

  • 需要预读功能:使用PushbackInputStream
  • 需要格式化输出:使用PrintStream
  • 需要数据验证:使用CheckedInputStream/CheckedOutputStream
  • 需要处理ZIP档案:使用ZipInputStream/ZipOutputStream或ZipFile

14.3 性能考虑要点

  1. 缓冲区大小:8KB-64KB通常是最佳范围
  2. 减少系统调用 :每次read()/write()都可能触发系统调用
  3. 批量操作 :尽量使用数组参数的read(byte[])write(byte[])方法
  4. 避免频繁创建流:流创建有开销,考虑复用
  5. 及时关闭:使用try-with-resources确保资源释放

14.4 最终建议

Java的I/O体系虽然庞大,但掌握了核心概念和常用实现类后,可以灵活组合出满足各种需求的解决方案。建议:

  1. 优先使用缓冲流提高性能
  2. 始终使用try-with-resources自动管理资源
  3. 根据数据类型选择合适的流(字节流还是字符流)
  4. 注意字符编码问题,始终明确指定编码
  5. 理解装饰器模式,学会组合不同功能的流
相关推荐
喵手1 小时前
Python爬虫实战:自动化构建 arXiv 本地知识库 - 从 PDF 下载到元数据索引!
爬虫·python·自动化·arxiv·本地知识库·pdf下载·元数据索引
Vic101011 小时前
链表算法三道
java·数据结构·算法·链表
mjhcsp1 小时前
C++区间 DP解析
开发语言·c++
闲人编程1 小时前
Celery分布式任务队列
redis·分布式·python·celery·任务队列·异步化
deephub1 小时前
深入RAG架构:分块策略、混合检索与重排序的工程实现
人工智能·python·大语言模型·rag
danyang_Q2 小时前
vscode python-u问题
开发语言·vscode·python
予枫的编程笔记2 小时前
【Kafka基础篇】RabbitMQ、RocketMQ、Kafka怎么选?3种主流MQ核心差异实测解析
kafka·rabbitmq·rocketmq·分布式流处理·发布订阅模型·消息队列(mq)·点对点模型
再难也得平2 小时前
[LeetCode刷题]128.最长连续序列(从零开始的java题解)
java·算法·leetcode
忘忧记2 小时前
python QT sqlsite版本 图书管理系统
开发语言·python·qt