目录

Java IO流详解

一、IO流体系总览

1. 四大抽象基类

2. IO流家族图谱

二、字节流

1. InputStream & OutputStream

2. FileInputStream & FileOutputStream

三、字符流

1. Reader & Writer

2. FileReader & FileWriter

四、缓冲流

1. BufferedInputStream & BufferedOutputStream

2. BufferedReader & BufferedWriter

五、转换流

1. InputStreamReader(字节→字符)

2. OutputStreamWriter(字符→字节)

六、数据流与对象流

1. DataInputStream & DataOutputStream

2. 对象序列化与反序列化

七、其他常用流

1. ByteArrayInputStream & ByteArrayOutputStream

2. PrintStream & PrintWriter

3. 标准输入输出流

八、try-with-resources 与异常处理

1. 传统方式(繁琐)

2. try-with-resources(推荐,Java 7+)

九、设计思想:装饰器模式

十、面试常见问题

1. IO流的分类?

2. 字节流和字符流的区别?

3. BIO、NIO、AIO的区别?

4. 什么是缓冲流?为什么能提高性能?

5. 序列化和反序列化是什么?

总结


Java IO流详解

Java的IO(Input/Output)是处理数据输入输出的核心机制。IO流按照不同维度可以分为:字节流与字符流、输入流与输出流、节点流与处理流。本文将系统梳理Java传统IO流的体系结构和核心用法。


一、IO流体系总览

1. 四大抽象基类

Java IO流的顶层是四个抽象类:

类型 字节流 字符流
输入流 InputStream Reader
输出流 OutputStream Writer

所有IO流类都继承自这四个基类之一。

2. IO流家族图谱

IO流
├── 字节流(处理二进制数据:图片、视频、音频等)
│   ├── InputStream(字节输入流)
│   │   ├── FileInputStream        —— 文件字节输入流
│   │   ├── BufferedInputStream     —— 缓冲字节输入流
│   │   ├── ByteArrayInputStream    —— 字节数组输入流
│   │   ├── DataInputStream         —— 数据输入流
│   │   └── ObjectInputStream       —— 对象输入流(反序列化)
│   └── OutputStream(字节输出流)
│       ├── FileOutputStream        —— 文件字节输出流
│       ├── BufferedOutputStream    —— 缓冲字节输出流
│       ├── ByteArrayOutputStream   —— 字节数组输出流
│       ├── DataOutputStream        —— 数据输出流
│       └── ObjectOutputStream      —— 对象输出流(序列化)
│
└── 字符流(处理文本数据:txt、json、xml等)
    ├── Reader(字符输入流)
    │   ├── FileReader              —— 文件字符输入流
    │   ├── BufferedReader          —— 缓冲字符输入流
    │   ├── InputStreamReader       —— 转换流(字节→字符)
    │   └── StringReader            —— 字符串输入流
    └── Writer(字符输出流)
        ├── FileWriter              —— 文件字符输出流
        ├── BufferedWriter          —— 缓冲字符输出流
        ├── OutputStreamWriter      —— 转换流(字符→字节)
        └── StringWriter            —— 字符串输出流

二、字节流

字节流以字节(8bit)为单位处理数据,可以处理任意类型的文件。

1. InputStream & OutputStream

InputStream 是所有字节输入流的父类,核心方法:

public abstract class InputStream implements Closeable {
    // 读取一个字节,返回0-255的int值,到达末尾返回-1
    public abstract int read() throws IOException;

    // 读取数据到字节数组,返回实际读取的字节数,到达末尾返回-1
    public int read(byte b[]) throws IOException { ... }

    // 读取数据到字节数组的指定位置
    public int read(byte b[], int off, int len) throws IOException { ... }

    // 跳过n个字节
    public long skip(long n) throws IOException { ... }

    // 关闭流
    public void close() throws IOException { ... }
}

OutputStream 是所有字节输出流的父类,核心方法:

public abstract class OutputStream implements Closeable, Flushable {
    // 写出一个字节
    public abstract void write(int b) throws IOException;

    // 写出字节数组
    public void write(byte b[]) throws IOException { ... }

    // 写出字节数组的指定部分
    public void write(byte b[], int off, int len) throws IOException { ... }

    // 刷新缓冲区,强制写出
    public void flush() throws IOException { ... }

    // 关闭流
    public void close() throws IOException { ... }
}

2. FileInputStream & FileOutputStream

文件字节流是最常用的节点流,直接与文件关联。

// 文件复制示例
public class FileCopy {
    public static void main(String[] args) {
        // try-with-resources 自动关闭流
        try (FileInputStream fis = new FileInputStream("source.jpg");
             FileOutputStream fos = new FileOutputStream("copy.jpg")) {

            byte[] buffer = new byte[8192]; // 8KB缓冲区
            int len;
            while ((len = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
            }
            System.out.println("文件复制完成!");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

注意事项:

  • FileOutputStream 默认覆盖模式,构造时传 true 可追加写入:new FileOutputStream("file.txt", true)
  • 每次 write() 后建议调用 flush() 或使用缓冲流
  • 必须关闭流释放系统资源,推荐 try-with-resources

三、字符流

字符流以字符(16bit)为单位处理数据,自动处理字符编码,适合处理文本文件。

1. Reader & Writer

Reader 是所有字符输入流的父类:

public abstract class Reader implements Readable, Closeable {
    // 读取一个字符,返回0-65535的int值,到达末尾返回-1
    public int read() throws IOException { ... }

    // 读取到字符数组
    public int read(char cbuf[]) throws IOException { ... }

    // 读取到字符数组的指定位置
    public abstract int read(char cbuf[], int off, int len) throws IOException;
}

Writer 是所有字符输出流的父类:

public abstract class Writer implements Appendable, Closeable, Flushable {
    // 写出一个字符
    public void write(int c) throws IOException { ... }

    // 写出字符数组
    public void write(char cbuf[]) throws IOException { ... }

    // 写出字符串
    public void write(String str) throws IOException { ... }

    // Writer特有:支持链式调用
    public Writer append(CharSequence csq) throws IOException { ... }
}

2. FileReader & FileWriter

文件字符流,简化了文本文件的读写操作。

// 读取文本文件
public class ReadTextFile {
    public static void main(String[] args) {
        try (FileReader reader = new FileReader("hello.txt")) {
            char[] buffer = new char[1024];
            int len;
            StringBuilder sb = new StringBuilder();
            while ((len = reader.read(buffer)) != -1) {
                sb.append(buffer, 0, len);
            }
            System.out.println(sb.toString());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// 写入文本文件
public class WriteTextFile {
    public static void main(String[] args) {
        try (FileWriter writer = new FileWriter("output.txt")) {
            writer.write("Hello, Java IO!\n");
            writer.write("字符流写入文本非常方便");
            // FileWriter内部使用默认字符编码
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

字节流 vs 字符流选择:

  • 处理二进制文件(图片、视频、压缩包)→ 字节流
  • 处理文本文件(txt、json、xml)→ 字符流
  • 不确定时 → 字节流(更通用)

四、缓冲流

缓冲流为底层流添加缓冲区,减少实际IO次数,显著提升性能。缓冲流是处理流(包装其他流)。

1. BufferedInputStream & BufferedOutputStream

// 使用缓冲字节流复制文件(性能更好)
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("source.mp4"));
     BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.mp4"))) {

    byte[] buffer = new byte[8192];
    int len;
    while ((len = bis.read(buffer)) != -1) {
        bos.write(buffer, 0, len);
    }
    // bos.flush(); // close()会自动调用flush()
}

2. BufferedReader & BufferedWriter

// 按行读取文本文件
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}

// 按行写入文本文件
try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
    bw.write("第一行");
    bw.newLine(); // 跨平台换行
    bw.write("第二行");
    bw.newLine();
}

BufferedReader 还提供了一个常用方法:

// 从控制台读取用户输入(传统方式)
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
System.out.print("请输入: ");
String input = reader.readLine();

缓冲流性能对比:

方式 复制100MB文件耗时(参考值)
无缓冲字节流 ~30秒
8KB缓冲字节流 ~0.1秒
缓冲字节流(默认8KB) ~0.1秒

五、转换流

转换流是字节流和字符流之间的桥梁,可以指定字符编码。

1. InputStreamReader(字节→字符)

// 读取GBK编码的文件
try (InputStreamReader isr = new InputStreamReader(
        new FileInputStream("gbk_file.txt"), "GBK");
     BufferedReader br = new BufferedReader(isr)) {

    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}

2. OutputStreamWriter(字符→字节)

// 以UTF-8编码写入文件
try (OutputStreamWriter osw = new OutputStreamWriter(
        new FileOutputStream("utf8_file.txt"), "UTF-8");
     BufferedWriter bw = new BufferedWriter(osw)) {

    bw.write("你好,世界!");
}

为什么需要转换流?

FileReader / FileWriter 使用平台默认编码,无法指定编码。需要处理特定编码时,必须使用转换流。

// FileReader 等价于(但无法指定编码):
new InputStreamReader(new FileInputStream("file.txt"), Charset.defaultCharset())

六、数据流与对象流

1. DataInputStream & DataOutputStream

数据流可以直接读写Java基本数据类型(int、double、boolean等)。

// 写入基本类型
try (DataOutputStream dos = new DataOutputStream(
        new FileOutputStream("data.bin"))) {
    dos.writeInt(100);
    dos.writeDouble(3.14);
    dos.writeBoolean(true);
    dos.writeUTF("Hello");
}

// 读取基本类型(必须按写入顺序读取)
try (DataInputStream dis = new DataInputStream(
        new FileInputStream("data.bin"))) {
    int i = dis.readInt();       // 100
    double d = dis.readDouble(); // 3.14
    boolean b = dis.readBoolean(); // true
    String s = dis.readUTF();    // "Hello"
}

2. 对象序列化与反序列化

对象流可以将Java对象转换为字节序列(序列化),也可以从字节序列恢复对象(反序列化)。

// 实现Serializable接口
public class User implements Serializable {
    private static final long serialVersionUID = 1L; // 版本号

    private String name;
    private int age;
    private transient String password; // transient:不参与序列化

    // 构造方法、getter、setter省略
}

// 序列化:对象 → 文件
try (ObjectOutputStream oos = new ObjectOutputStream(
        new FileOutputStream("user.dat"))) {
    User user = new User("张三", 25, "123456");
    oos.writeObject(user);
}

// 反序列化:文件 → 对象
try (ObjectInputStream ois = new ObjectInputStream(
        new FileInputStream("user.dat"))) {
    User user = (User) ois.readObject();
    System.out.println(user.getName());     // 张三
    System.out.println(user.getPassword()); // null(被transient修饰)
}

序列化注意事项:

  • 类必须实现 Serializable 接口(标记接口,无方法)
  • 建议显式声明 serialVersionUID,避免版本兼容问题
  • static 字段不参与序列化
  • transient 字段不参与序列化
  • 父类如果没有实现 Serializable,父类的字段不会被序列化

七、其他常用流

1. ByteArrayInputStream & ByteArrayOutputStream

在内存中操作字节数组,常用于数据转换和临时存储。

// 将字节数组转为流处理
byte[] data = "Hello".getBytes();
try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
    int b;
    while ((b = bais.read()) != -1) {
        System.out.print((char) b); // H e l l o
    }
}

// 收集输出到字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write("Hello".getBytes());
baos.write(" World".getBytes());
byte[] result = baos.toByteArray(); // "Hello World"

2. PrintStream & PrintWriter

打印流提供了方便的 print() / println() 方法。

// System.out 就是 PrintStream
System.out.println("Hello");

// 打印到文件
try (PrintWriter pw = new PrintWriter(new FileWriter("log.txt"))) {
    pw.println("日志信息");
    pw.printf("用户: %s, 年龄: %d%n", "张三", 25);
}

3. 标准输入输出流

// 标准输入:System.in(InputStream)
// 标准输出:System.out(PrintStream)
// 标准错误:System.err(PrintStream)

// 重定向标准输出到文件
PrintStream ps = new PrintStream("output.log");
System.setOut(ps); // 之后的System.out输出到文件
System.out.println("这会写入文件");

八、try-with-resources 与异常处理

1. 传统方式(繁琐)

FileInputStream fis = null;
try {
    fis = new FileInputStream("file.txt");
    // 使用流...
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2. try-with-resources(推荐,Java 7+)

// 实现了AutoCloseable接口的资源可以自动关闭
try (FileInputStream fis = new FileInputStream("file.txt");
     FileOutputStream fos = new FileOutputStream("copy.txt")) {
    // 使用流...
} catch (IOException e) {
    e.printStackTrace();
}
// 流自动关闭,无需finally

注意: 多个资源按声明的逆序关闭(先关fos,再关fis)。


九、设计思想:装饰器模式

Java IO流的设计大量使用了装饰器模式

// 节点流(被装饰者):直接操作数据源
FileInputStream fis = new FileInputStream("file.txt");

// 处理流(装饰者):增强功能
BufferedInputStream bis = new BufferedInputStream(fis);  // 添加缓冲
DataInputStream dis = new DataInputStream(bis);           // 添加读取基本类型能力

装饰器模式的好处:

  • 灵活组合功能(缓冲 + 编码转换 + 数据类型)
  • 遵循开闭原则(扩展功能不修改原有代码)
  • 可以嵌套多层装饰
// 实际开发中的典型组合
BufferedReader br = new BufferedReader(
    new InputStreamReader(
        new FileInputStream("data.txt"), "UTF-8"));

十、面试常见问题

1. IO流的分类?

维度 分类
数据单位 字节流、字符流
流向 输入流、输出流
角色 节点流(直接操作数据源)、处理流(包装其他流)

2. 字节流和字符流的区别?

  • 字节流以字节为单位,字符流以字符为单位
  • 字节流可以处理任意文件,字符流只能处理文本
  • 字符流有缓冲区(内部维护char[]),字节流无缓冲区
  • 字符流自动处理编码转换,字节流需要手动处理

3. BIO、NIO、AIO的区别?

  • BIO:同步阻塞,一个连接一个线程,适合连接数少的场景
  • NIO:同步非阻塞,基于Channel/Buffer/Selector,一个线程管理多个连接
  • AIO:异步非阻塞,基于回调通知,适合连接数多且IO操作重的场景

4. 什么是缓冲流?为什么能提高性能?

缓冲流在内部维护一个缓冲区(默认8KB),减少实际的系统IO调用次数。每次read/write先操作缓冲区,缓冲区满或手动flush时才进行真正的IO操作。

5. 序列化和反序列化是什么?

  • 序列化:将对象转换为字节序列,便于存储或传输
  • 反序列化:将字节序列恢复为对象
  • 实现 Serializable 接口,使用 ObjectOutputStream / ObjectInputStream
  • transient 修饰的字段不参与序列化

总结

场景 推荐使用的流
复制文件(二进制) BufferedInputStream + BufferedOutputStream
读写文本文件 BufferedReader + BufferedWriter
指定编码读写 InputStreamReader + OutputStreamWriter
读写基本类型 DataInputStream + DataOutputStream
对象持久化 ObjectOutputStream + ObjectInputStream
控制台输入 BufferedReader(new InputStreamReader(System.in))

核心原则:

  1. 明确数据类型:二进制用字节流,文本用字符流
  2. 使用缓冲流提升性能
  3. 始终用 try-with-resources 确保资源释放
  4. 需要指定编码时使用转换流

更多推荐