第一章:I/O 流核心概念与体系结构

1.1 什么是流 (Stream)

在Java中, 是一个抽象的概念,代表了数据的有序序列。可以想象成连接程序与数据源(如文件、网络连接、内存块等)的“管道”,数据通过这个管道“流动”。

  • 输入流:数据从外部(如文件)流入 程序。
  • 输出流:数据从程序流出 到外部(如文件)。
1.2 流的顶级分类:字节流 vs. 字符流

这是Java I/O最根本的分类,决定了我们处理数据的“基本单位”。

特性 字节流 字符流
数据单位 字节 (byte, 8bit) 字符 (char, 16bit)
用途 处理所有类型的数据,如图片、视频、音频等二进制文件 专门处理文本数据
顶层抽象类 InputStream, OutputStream Reader, Writer
优势 通用性强,能处理任何数据 自动处理字符编码,方便处理文本

重要原则:如果是文本文件(如.txt, .java, .html),优先使用字符流,可以避免乱码问题。如果是二进制文件(如.jpg, .mp3, .class),必须使用字节流

1.3 I/O 流的类体系结构

Java I/O 使用了“装饰者模式”,通过组合不同的流类来增强功能。其类结构主要围绕四个抽象类展开:

  • InputStream (字节输入流)
  • OutputStream (字节输出流)
  • Reader (字符输入流)
  • Writer (字符输出流)

所有具体的流类都是这四个类的子类。


第二章:字节流 (Byte Streams)

字节流用于处理二进制数据,核心类是 InputStreamOutputStream

2.1 文件字节流 (FileInputStream & FileOutputStream)

这是最基础的流,用于直接与文件进行字节交互。

示例:文件复制(一次读取一个字节)

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileCopyByte {
    public static void main(String[] args) {
        // 声明在try()括号中,JDK7+会自动关闭资源 (自动调用close方法)
        try (FileInputStream fis = new FileInputStream("source.jpg");
             FileOutputStream fos = new FileOutputStream("target.jpg")) {

            int byteData;
            // read() 一次读取一个字节,返回int值,读到末尾返回-1
            while ((byteData = fis.read()) != -1) {
                // write() 一次写入一个字节
                fos.write(byteData);
            }
            System.out.println("文件复制完成!");

        } catch (IOException e) {
            e.printStackTrace();
        }
        // 无需手动调用 fis.close() 和 fos.close(), try-with-resources 会自动处理
    }
}

示例:使用缓冲区提高效率(一次读取多个字节)

try (FileInputStream fis = new FileInputStream("source.jpg");
     FileOutputStream fos = new FileOutputStream("target.jpg")) {

    byte[] buffer = new byte[1024]; // 创建一个1KB的缓冲区
    int length;
    // read(buffer) 将数据读入字节数组,返回读取的字节数
    while ((length = fis.read(buffer)) != -1) {
        // write(buffer, 0, length) 将缓冲区中从0到length的数据写入
        fos.write(buffer, 0, length);
    }
    System.out.println("文件复制完成(使用缓冲区)!");

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

使用缓冲区的意义:单字节读写涉及大量底层的系统调用,效率极低。使用缓冲区后,减少了I/O操作次数,性能提升巨大。

2.2 缓冲字节流 (BufferedInputStream & BufferedOutputStream)

这是处理流/包装流的一种,它们本身不直接连接数据源,而是“包装”在另一个节点流(如FileInputStream)之上,提供缓冲功能,是更高效的用法。

示例:使用缓冲流进行文件复制

import java.io.*;

public class FileCopyBuffered {
    public static void main(String[] args) {
        // 在文件流的外层再包裹一层缓冲流
        try (FileInputStream fis = new FileInputStream("source.jpg");
             BufferedInputStream bis = new BufferedInputStream(fis); // 包装
             FileOutputStream fos = new FileOutputStream("target.jpg");
             BufferedOutputStream bos = new BufferedOutputStream(fos)) { // 包装

            int byteData;
            // 此时 bis.read() 会先从内部缓冲区读取,缓冲区空了才去底层文件流取一大块数据
            while ((byteData = bis.read()) != -1) {
                bos.write(byteData); // bos.write() 会先写入内部缓冲区,满了再一次性写入底层文件流
            }
            // bos.flush(); // 在close()时会自动调用flush(),强制清空缓冲区并写入磁盘
            System.out.println("文件复制完成(使用缓冲流)!");

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

第三章:字符流 (Character Streams)

字符流用于处理文本数据,核心类是 ReaderWriter。它们内部会自动进行字节与字符的编码解码转换。

3.1 文件字符流 (FileReader & FileWriter)

用于直接读写文本文件。

示例:文本文件复制

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class TextFileCopy {
    public static void main(String[] args) {
        // 注意:FileReader/FileWriter 使用平台默认编码(如GBK, UTF-8),可能产生乱码
        try (FileReader reader = new FileReader("source.txt");
             FileWriter writer = new FileWriter("target.txt")) {

            int charData;
            // read() 一次读取一个字符(两个字节)
            while ((charData = reader.read()) != -1) {
                writer.write(charData);
            }
            System.out.println("文本文件复制完成!");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
3.2 缓冲字符流 (BufferedReader & BufferedWriter)

这是处理字符流时最常用、最高效的类。特别是 BufferedReader 提供了 readLine() 方法,可以方便地按行读取文本。

示例:按行读写文本文件

import java.io.*;

public class ReadWriteByLine {
    public static void main(String[] args) {
        try (FileReader fr = new FileReader("source.txt");
             BufferedReader br = new BufferedReader(fr); // 包装
             FileWriter fw = new FileWriter("target.txt");
             BufferedWriter bw = new BufferedWriter(fw)) { // 包装

            String line;
            // readLine() 读取一行文本(不包含换行符),返回null表示结束
            while ((line = br.readLine()) != null) {
                bw.write(line); // 写入一行
                bw.newLine();   // 写入一个与系统相关的换行符
            }
            System.out.println("按行复制完成!");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
3.3 字节流与字符流的桥梁:InputStreamReader & OutputStreamWriter

当需要指定字符编码时,就必须使用这两个转换流。它们是连接字节世界和字符世界的桥梁。

  • InputStreamReader: 将一个字节输入流 InputStream 转换为字符输入流 Reader
  • OutputStreamWriter: 将一个字符输出流 Writer 转换为字节输出流 OutputStream

示例:指定UTF-8编码读取文本文件

import java.io.*;

public class SpecifyEncoding {
    public static void main(String[] args) {
        // 用字节流打开文件,再用InputStreamReader指定编码转换为字符流
        try (FileInputStream fis = new FileInputStream("source_utf8.txt");
             // 关键:将字节流 fis 转换为字符流 isr,并指定编码为 "UTF-8"
             InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
             BufferedReader br = new BufferedReader(isr)) {

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

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

第四章:高级话题与标准I/O

4.1 打印流 (PrintStream & PrintWriter)

这是最方便的向目的地“写”数据的流,提供了丰富的 print(), println(), printf() 方法。System.outSystem.err 就是 PrintStream 对象。

示例:使用PrintWriter写入文件

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

public class PrintWriterDemo {
    public static void main(String[] args) {
        try (FileWriter fw = new FileWriter("log.txt");
             PrintWriter pw = new PrintWriter(fw)) { // 包装

            pw.println("=== 程序日志 ===");
            pw.printf("当前时间:%tF %<tT%n", System.currentTimeMillis());
            pw.print("状态:正常结束");
            // println 会自动换行,printf 和 print 不会

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
4.2 数据流 (DataInputStream & DataOutputStream)

用于读写Java基本数据类型(如int, double, boolean)和String,保持其数据的原始格式,而不是字符串形式。

示例:读写基本数据类型

import java.io.*;

public class DataStreamDemo {
    public static void main(String[] args) {
        // 写入数据
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.bin"))) {
            dos.writeInt(100);
            dos.writeDouble(3.14159);
            dos.writeBoolean(true);
            dos.writeUTF("Hello, 数据流!"); // 采用修改版UTF-8编码写入字符串
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 读取数据 (必须按写入的顺序和类型读取)
        try (DataInputStream dis = new DataInputStream(new FileInputStream("data.bin"))) {
            int i = dis.readInt();
            double d = dis.readDouble();
            boolean b = dis.readBoolean();
            String s = dis.readUTF();

            System.out.printf("读取的数据:%d, %f, %b, %s%n", i, d, b, s);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
4.3 对象流与序列化 (ObjectInputStream & ObjectOutputStream)

用于直接将对象写入流或从流中读取对象,这个过程称为序列化(写)和反序列化(读)。被序列化的类必须实现 java.io.Serializable 标记接口。

示例:对象的序列化与反序列化

import java.io.*;

// 1. 实现 Serializable 接口
class Person implements Serializable {
    private static final long serialVersionUID = 1L; // 建议显式声明序列化ID
    private String name;
    private int age;

    // 构造器、getter/setter、toString...
    public Person(String name, int age) { this.name = name; this.age = age; }
    @Override
    public String toString() { return "Person{name='" + name + "', age=" + age + '}'; }
}

public class ObjectStreamDemo {
    public static void main(String[] args) {
        // 序列化对象到文件
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.bin"))) {
            Person person = new Person("张三", 25);
            oos.writeObject(person); // 将对象写入流
            System.out.println("对象序列化完成");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 从文件反序列化对象
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.bin"))) {
            Person restoredPerson = (Person) ois.readObject(); // 从流中读取对象
            System.out.println("反序列化得到的对象:" + restoredPerson);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
4.4 标准输入/输出流

Java提供了三个预定义的流对象:

  • System.inInputStream 类型,代表标准输入(通常是键盘)。
  • System.outPrintStream 类型,代表标准输出。
  • System.errPrintStream 类型,代表标准错误输出。

示例:从标准输入读取

import java.util.Scanner; // 更常用的方式,包装了System.in

public class StandardIO {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入您的姓名: ");
        String name = scanner.nextLine();
        System.out.println("你好, " + name + "!");
        scanner.close();
    }
}

总结

场景 推荐使用的流
二进制文件(图片、视频等)复制 BufferedInputStream + BufferedOutputStream
文本文件读写(使用默认编码) BufferedReader + BufferedWriter
文本文件读写(需指定编码) InputStreamReader + OutputStreamWriter
按行读取文本 BufferedReader.readLine()
方便地输出格式化的文本 PrintWriter / PrintStream
读写基本数据类型 DataInputStream + DataOutputStream
读写Java对象 ObjectInputStream + ObjectOutputStream (需实现Serializable)

最佳实践

  1. 明确数据类型:先判断是二进制还是文本。
  2. 使用缓冲:几乎在所有情况下,都应该使用缓冲流来包装底层节点流。
  3. 使用try-with-resources:确保流被正确关闭,避免资源泄漏。
  4. 注意字符编码:在跨平台或国际化应用中,务必指定正确的字符编码(如UTF-8)。
Logo

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

更多推荐