Java IO 流:字节流、字符流、缓冲流一网打尽

学习日期:2026-06-16

难度:⭐⭐⭐⭐ 核心

前言

IO(Input/Output)流是 Java 中处理数据输入输出的核心机制。读文件、写文件、网络传输、控制台交互……凡是涉及数据流动的地方,都有 IO 流的身影。

IO 流类很多、体系复杂,初学者容易绕晕。这篇把 IO 流的核心概念、常用流类、典型用法和常见坑点一次说清楚。

一、IO 流总览

1.1 什么是"流"

流(Stream)是一组有顺序的、有起点和终点的字节集合,是对数据传输的抽象。就像水管里的水一样,数据从一端流向另一端。

1.2 流的分类

表格

分类角度 类型 说明
流向 输入流(Input) 从外部读到内存(读文件、读网络)
输出流(Output) 从内存写到外部(写文件、发数据)
数据单元 字节流 以字节为单位,8 位,可处理任何数据(文本、图片、视频等)
字符流 以字符为单位,16 位,专门处理文本(自动处理编码)
功能 节点流 直接连接数据源(FileInputStream、FileReader)
处理流(装饰器) 包装节点流,提供更强大的功能(BufferedReader、ObjectOutputStream)

1.3 四大基类

所有流都继承自以下四个抽象基类:

plaintext

9

1

2

3

4

输入流 输出流

字节流 InputStream OutputStream

字符流 Reader Writer

💡 命名规律:字节流以 Stream 结尾,字符输入流以 Reader 结尾,字符输出流以 Writer 结尾。

二、File 类

在讲流之前,先认识一下 File 类——它表示文件或目录的路径信息,不负责文件内容的读写。

java

99

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

// 创建 File 对象

File file = new File("test.txt"); // 相对路径

File file2 = new File("D:/docs", "test.txt"); // 父路径 + 子路径

File file3 = new File(new File("D:/docs"), "test.txt");

// 判断方法

file.exists(); // 是否存在

file.isFile(); // 是否是文件

file.isDirectory(); // 是否是目录

file.isHidden(); // 是否隐藏

file.canRead(); // 是否可读

file.canWrite(); // 是否可写

// 获取信息

file.getName(); // 文件名

file.getPath(); // 路径名

file.getAbsolutePath(); // 绝对路径

file.length(); // 文件大小(字节)

file.lastModified(); // 最后修改时间(毫秒)

// 创建和删除

file.createNewFile(); // 创建文件(文件已存在返回false)

file.mkdir(); // 创建单级目录

file.mkdirs(); // 创建多级目录

file.delete(); // 删除文件/空目录

// 目录遍历

File dir = new File("D:/docs");

String[] names = dir.list(); // 获取子文件/目录名数组

File[] files = dir.listFiles(); // 获取子文件/目录File对象数组

// 带过滤器的遍历(只拿 .txt 文件)

File[] txtFiles = dir.listFiles(f -> f.getName().endsWith(".txt"));

⚠️ 常见坑

  • delete() 只能删除空目录或文件,非空目录删不掉
  • listFiles() 如果路径不存在或不是目录,返回 null,要做非空判断
  • 路径分隔符建议用 File.separator 代替硬编码的 /\,保证跨平台

三、字节流

3.1 文件字节流:FileInputStream / FileOutputStream

最基础的文件字节流,直接操作字节。

文件读取

java

99

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

// 方式一:单个字节读(效率低,不推荐)

try (FileInputStream fis = new FileInputStream("test.txt")) {

int data;

while ((data = fis.read()) != -1) { // read() 返回 -1 表示读完

System.out.print((char) data);

}

} catch (IOException e) {

e.printStackTrace();

}

// 方式二:字节数组读(推荐!效率高)

try (FileInputStream fis = new FileInputStream("test.txt")) {

byte[] buffer = new byte[1024]; // 1KB 缓冲区

int len;

while ((len = fis.read(buffer)) != -1) { // len 是实际读到的字节数

String str = new String(buffer, 0, len);

System.out.print(str);

}

} catch (IOException e) {

e.printStackTrace();

}

文件写入

java

99

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

// 覆盖写入(每次写都会清空原有内容)

try (FileOutputStream fos = new FileOutputStream("test.txt")) {

fos.write("Hello Java IO!".getBytes());

fos.write('\n');

byte[] data = "你好".getBytes();

fos.write(data, 0, data.length); // 写数组的一部分

fos.flush(); // 刷新缓冲区,强制写出

} catch (IOException e) {

e.printStackTrace();

}

// 追加写入(第二个参数 true 表示追加)

try (FileOutputStream fos = new FileOutputStream("test.txt", true)) {

fos.write("追加内容".getBytes());

} catch (IOException e) {

e.printStackTrace();

}

⚠️ 重要:用完流一定要 close() 释放资源!上面的写法用了 try-with-resources(Java 7+),会自动关闭流,不用手动写 finally。

3.2 文件拷贝实战

public class FileCopy {

public static void main(String[] args) {

String src = "source.jpg";

String dest = "copy.jpg";

try (FileInputStream fis = new FileInputStream(src);

FileOutputStream fos = new FileOutputStream(dest)) {

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) {

System.err.println("拷贝失败:" + e.getMessage());

}

}

}

3.3 字节缓冲流:BufferedInputStream / BufferedOutputStream

内部有一个缓冲区数组(默认 8KB),读写时先操作缓冲区,减少 IO 次数,大幅提升性能。

try (BufferedInputStream bis = new BufferedInputStream(

new FileInputStream("src.mp4"));

BufferedOutputStream bos = new BufferedOutputStream(

new FileOutputStream("dest.mp4"))) {

byte[] buffer = new byte[1024];

int len;

while ((len = bis.read(buffer)) != -1) {

bos.write(buffer, 0, len);

}

} catch (IOException e) {

e.printStackTrace();

}

💡 装饰器模式:缓冲流是处理流,需要包装一个节点流。它不改变流的本质,只是在外面套了一层缓冲功能。

3.4 数据流:DataInputStream / DataOutputStream

可以直接读写基本数据类型和字符串,不用自己转字节。

// 写入

try (DataOutputStream dos = new DataOutputStream(

new FileOutputStream("data.dat"))) {

dos.writeInt(100);

dos.writeDouble(3.14);

dos.writeBoolean(true);

dos.writeUTF("你好"); // 写入字符串(UTF-8编码)

} catch (IOException e) {

e.printStackTrace();

}

// 读取(顺序必须和写入一致!)

try (DataInputStream dis = new DataInputStream(

new FileInputStream("data.dat"))) {

int i = dis.readInt();

double d = dis.readDouble();

boolean b = dis.readBoolean();

String s = dis.readUTF();

System.out.println(i + " " + d + " " + b + " " + s);

} catch (IOException e) {

e.printStackTrace();

}

3.5 对象流:ObjectInputStream / ObjectOutputStream

可以把 Java 对象直接写入文件或从文件读取,这就是序列化反序列化

序列化的前提:对象所属的类必须实现 Serializable 接口(标记接口,没有方法)。

// 实体类必须实现 Serializable

class Person implements Serializable {

// 序列化版本号(建议显式声明,避免类修改后反序列化失败)

private static final long serialVersionUID = 1L;

private String name;

private int age;

// transient 关键字:修饰的属性不参与序列化

private transient String password;

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

}

// 序列化(写对象到文件)

try (ObjectOutputStream oos = new ObjectOutputStream(

new FileOutputStream("person.dat"))) {

oos.writeObject(new Person("张三", 20, "123456"));

} catch (IOException e) {

e.printStackTrace();

}

// 反序列化(从文件读对象)

try (ObjectInputStream ois = new ObjectInputStream(

new FileInputStream("person.dat"))) {

Person p = (Person) ois.readObject();

System.out.println(p.getName() + " " + p.getAge());

// password 是 transient,反序列化后为 null

} catch (IOException | ClassNotFoundException e) {

e.printStackTrace();

}

⚠️ 序列化注意事项

  1. 要序列化的类必须实现 Serializable 接口
  2. 类的所有属性也必须是可序列化的(基本类型和 String 默认可以)
  3. transient 修饰的属性不参与序列化,反序列化后是默认值
  4. 建议显式声明 serialVersionUID,否则类修改后自动生成的 ID 会变,导致反序列化失败
  5. 序列化保存的是对象的状态,不保存类的方法

四、字符流

字节流处理文本时可能会有乱码问题(因为一个字符可能占多个字节),字符流专门用来处理文本。

4.1 字符编码

  • ASCII:英文用 1 个字节
  • GBK:中文 2 字节,英文 1 字节,Windows 默认
  • UTF-8:中文 3 字节,英文 1 字节,互联网通用标准
  • UTF-16:所有字符都是 2 字节(Java char 用的就是这个)

💡 乱码的本质:编码和解码用的字符集不一致

4.2 文件字符流:FileReader / FileWriter

底层还是字节流,只是自动帮你做了字符集转换。默认使用平台字符集(中文 Windows 是 GBK)。

// 读取文件

try (FileReader fr = new FileReader("test.txt")) {

char[] buffer = new char[1024];

int len;

while ((len = fr.read(buffer)) != -1) {

System.out.print(new String(buffer, 0, len));

}

} catch (IOException e) {

e.printStackTrace();

}

// 写入文件

try (FileWriter fw = new FileWriter("test.txt")) {

fw.write("Hello 你好");

fw.write('\n');

fw.append("追加内容"); // append 和 write 效果差不多

fw.flush();

} catch (IOException e) {

e.printStackTrace();

}

4.3 字符缓冲流:BufferedReader / BufferedWriter

字符缓冲流除了有缓冲功能,还提供了按行读写的便利方法。

// 按行读取

try (BufferedReader br = new BufferedReader(

new FileReader("test.txt"))) {

String line;

while ((line = br.readLine()) != null) { // 读一行,返回 null 表示读完

System.out.println(line);

}

} catch (IOException e) {

e.printStackTrace();

}

// 按行写入

try (BufferedWriter bw = new BufferedWriter(

new FileWriter("test.txt"))) {

bw.write("第一行");

bw.newLine(); // 换行(跨平台,比直接写 \n 好)

bw.write("第二行");

bw.newLine();

} catch (IOException e) {

e.printStackTrace();

}

💡 小技巧:Java 11+ 新增了更简单的方式:

java

9

1

2

3

4

5

6

7

8

9

// 读取整个文件为字符串

String content = Files.readString(Path.of("test.txt"), StandardCharsets.UTF_8);

// 读取所有行

List<String> lines = Files.readAllLines(Path.of("test.txt"));

// 写字符串到文件

Files.writeString(Path.of("test.txt"), "Hello World");

4.4 转换流:InputStreamReader / OutputStreamWriter

字符流和字节流之间的桥梁,可以指定字符集。

// 从文件读取,指定 UTF-8 编码

try (BufferedReader br = new BufferedReader(

new InputStreamReader(

new FileInputStream("test.txt"),

StandardCharsets.UTF_8))) { // 指定编码

String line;

while ((line = br.readLine()) != null) {

System.out.println(line);

}

} catch (IOException e) {

e.printStackTrace();

}

// 写入文件,指定 GBK 编码

try (OutputStreamWriter osw = new OutputStreamWriter(

new FileOutputStream("gbk.txt"),

Charset.forName("GBK"))) {

osw.write("你好");

} catch (IOException e) {

e.printStackTrace();

}

⚠️ FileReader/FileWriter 的局限:它们只能用默认字符集,要指定编码必须用转换流。

五、标准输入输出流

Java 提供了三个标准流:

表格

变量 类型 说明
System.in InputStream 标准输入流,默认从键盘输入
System.out PrintStream 标准输出流,默认输出到控制台
System.err PrintStream 标准错误流,默认输出到控制台(红色)

从控制台读取输入

// 方式一:用 Scanner(最方便,推荐)

Scanner scanner = new Scanner(System.in);

System.out.print("请输入姓名:");

String name = scanner.nextLine(); // 读一行

System.out.print("请输入年龄:");

int age = scanner.nextInt(); // 读整数

System.out.println(name + " " + age);

scanner.close();

// 方式二:用 BufferedReader(传统写法)

BufferedReader br = new BufferedReader(

new InputStreamReader(System.in));

String s = br.readLine();

// 方式三:System.in 直接读(太底层,麻烦)

int b = System.in.read(); // 只读一个字节

六、打印流:PrintStream / PrintWriter

打印流只负责输出,功能很强大:可以打印各种类型、自动刷新、永不抛 IOException。

// PrintStream(字节打印流)

try (PrintStream ps = new PrintStream(

new FileOutputStream("log.txt"), true)) { // true=自动刷新

ps.println("Hello");

ps.println(123);

ps.println(3.14);

ps.printf("Name: %s, Age: %d%n", "张三", 20); // 格式化输出

} catch (FileNotFoundException e) {

e.printStackTrace();

}

// PrintWriter(字符打印流,更常用)

try (PrintWriter pw = new PrintWriter(

new FileWriter("output.txt"), true)) {

pw.println("第一行");

pw.printf("数值:%d%n", 100);

} catch (IOException e) {

e.printStackTrace();

}

💡 System.out 就是一个 PrintStream。

七、常用 IO 流选择指南

表格

需求 推荐流
读取文本文件 BufferedReader + FileReader / InputStreamReader
写入文本文件 BufferedWriter + FileWriter / OutputStreamWriter
读取二进制文件(图片、视频、音频) BufferedInputStream + FileInputStream
写入二进制文件 BufferedOutputStream + FileOutputStream
读写基本数据类型 DataInputStream / DataOutputStream
读写 Java 对象 ObjectInputStream / ObjectOutputStream
按行读文本 BufferedReader.readLine()
格式化输出文本 PrintWriter / PrintStream
需要指定字符编码 InputStreamReader / OutputStreamWriter
简单文件读写(Java 11+) Files.readString() / Files.writeString()

八、实战练习

练习1:文件拷贝工具类

/**

* 文件拷贝工具类(支持任意文件类型)

*/

public class FileCopyUtils {

private static final int BUFFER_SIZE = 8192; // 8KB

/**

* 拷贝文件

* @param srcPath 源文件路径

* @param destPath 目标文件路径

* @return 是否拷贝成功

*/

public static boolean copyFile(String srcPath, String destPath) {

File src = new File(srcPath);

if (!src.exists() || !src.isFile()) {

System.err.println("源文件不存在");

return false;

}

// 确保目标目录存在

File dest = new File(destPath);

File parentDir = dest.getParentFile();

if (parentDir != null && !parentDir.exists()) {

parentDir.mkdirs();

}

try (BufferedInputStream bis = new BufferedInputStream(

new FileInputStream(src));

BufferedOutputStream bos = new BufferedOutputStream(

new FileOutputStream(dest))) {

byte[] buffer = new byte[BUFFER_SIZE];

int len;

while ((len = bis.read(buffer)) != -1) {

bos.write(buffer, 0, len);

}

return true;

} catch (IOException e) {

System.err.println("拷贝异常:" + e.getMessage());

return false;

}

}

public static void main(String[] args) {

boolean success = copyFile("source.jpg", "backup/copy.jpg");

System.out.println(success ? "拷贝成功" : "拷贝失败");

}

}

练习2:统计 Java 文件的代码行数

public class CodeLineCounter {

public static int countLines(String filePath) {

int count = 0;

try (BufferedReader br = new BufferedReader(

new FileReader(filePath))) {

while (br.readLine() != null) {

count++;

}

} catch (IOException e) {

e.printStackTrace();

}

return count;

}

// 统计目录下所有 Java 文件的总行数

public static int countLinesInDir(String dirPath) {

int total = 0;

File dir = new File(dirPath);

if (!dir.exists() || !dir.isDirectory()) {

return 0;

}

File[] files = dir.listFiles(f ->

f.isDirectory() || f.getName().endsWith(".java"));

if (files == null) return 0;

for (File file : files) {

if (file.isFile()) {

int lines = countLines(file.getAbsolutePath());

System.out.println(file.getName() + ": " + lines + " 行");

total += lines;

} else {

total += countLinesInDir(file.getAbsolutePath());

}

}

return total;

}

public static void main(String[] args) {

int total = countLinesInDir("src");

System.out.println("总代码行数:" + total);

}

}

练习3:用户登录系统(对象序列化)

class User implements Serializable {

private static final long serialVersionUID = 1L;

private String username;

private String password;

public User(String username, String password) {

this.username = username;

this.password = password;

}

public String getUsername() { return username; }

public boolean checkPassword(String pwd) {

return password.equals(pwd);

}

}

public class UserSystem {

private static final String DATA_FILE = "users.dat";

private Map<String, User> users = new HashMap<>();

// 注册

public boolean register(String username, String password) {

if (users.containsKey(username)) {

return false;

}

users.put(username, new User(username, password));

saveUsers();

return true;

}

// 登录

public boolean login(String username, String password) {

User user = users.get(username);

return user != null && user.checkPassword(password);

}

// 保存用户数据到文件

private void saveUsers() {

try (ObjectOutputStream oos = new ObjectOutputStream(

new FileOutputStream(DATA_FILE))) {

oos.writeObject(users);

} catch (IOException e) {

e.printStackTrace();

}

}

// 从文件加载用户数据

@SuppressWarnings("unchecked")

private void loadUsers() {

File file = new File(DATA_FILE);

if (!file.exists()) return;

try (ObjectInputStream ois = new ObjectInputStream(

new FileInputStream(file))) {

users = (Map<String, User>) ois.readObject();

} catch (IOException | ClassNotFoundException e) {

e.printStackTrace();

}

}

public static void main(String[] args) {

UserSystem system = new UserSystem();

system.loadUsers();

Scanner sc = new Scanner(System.in);

while (true) {

System.out.println("1.注册 2.登录 3.退出");

int choice = sc.nextInt();

sc.nextLine();

if (choice == 3) break;

System.out.print("用户名:");

String username = sc.nextLine();

System.out.print("密码:");

String password = sc.nextLine();

if (choice == 1) {

if (system.register(username, password)) {

System.out.println("注册成功!");

} else {

System.out.println("用户名已存在!");

}

} else if (choice == 2) {

if (system.login(username, password)) {

System.out.println("登录成功!欢迎 " + username);

} else {

System.out.println("用户名或密码错误!");

}

}

}

sc.close();

}

}

九、总结速查表

流的分类速记

表格

类型 输入 输出 缓冲区版本
字节(文件) FileInputStream FileOutputStream BufferedInputStream / BufferedOutputStream
字符(文件) FileReader FileWriter BufferedReader / BufferedWriter
字节数组 ByteArrayInputStream ByteArrayOutputStream -
字符数组 CharArrayReader CharArrayWriter -
字符串 StringReader StringWriter -
管道 PipedInputStream PipedOutputStream -
数据 DataInputStream DataOutputStream -
对象 ObjectInputStream ObjectOutputStream -
打印 - PrintStream / PrintWriter -
转换 InputStreamReader OutputStreamWriter -

编码习惯

  1. 优先用缓冲流:BufferedXxx 比裸流快很多
  2. 用 try-with-resources:自动关闭流,不用写 finally
  3. 文本用字符流,二进制用字节流:别搞混
  4. 指定字符集:避免平台默认编码导致的乱码
  5. read() 返回 -1 表示结束:这是约定,记住它
  6. 用完 flush + close:尤其是输出流,避免数据留在缓冲区没写入

常见问题

表格

问题 原因 解决
中文乱码 编码解码字符集不一致 统一用 UTF-8,用转换流指定编码
FileNotFoundException 文件路径错了,或权限不够 检查路径、文件是否存在、权限
反序列化失败 serialVersionUID 不匹配 显式声明 serialVersionUID
大文件内存溢出 一次性把整个文件读进内存 用缓冲流分批次读写
文件夹删不掉 目录非空 先递归删子文件,再删目录

IO 流是 Java 的基础但重要的内容,记住"四大基类 + 装饰器模式 + 缓冲流提升性能"这条主线,再多写几个练习,很快就能熟练掌握。

更多推荐