打印流与数据流 | JavaSE
打印流与数据流
一、学习目标
完成本章后,你应该能够:
- 能够解释打印流解决了什么问题。
- 能够区分
PrintStream与PrintWriter。 - 能够使用
print()、println()、printf()输出各种类型的数据。 - 能够理解
System.out为什么可以直接调用println()。 - 能够使用打印流生成日志、报表等文本文件。
- 能够解释数据流解决了什么问题。
- 能够使用
DataOutputStream按 Java 数据类型写出数据。 - 能够使用
DataInputStream按相同顺序和类型读取数据。 - 能够解释为什么数据流生成的文件通常不是普通文本文件。
- 能够区分“打印数据的文本表示”和“保存数据本身的二进制表示”。
二、核心知识
2.1 为什么需要打印流
前面使用字符输出流写数据:
Writer writer = new FileWriter("data.txt");
writer.write("Java");
writer.write("\n");
writer.write("100");
如果要写:
int age = 20;
double score = 95.5;
boolean passed = true;
不能直接:
writer.write(age);
然后期望文件中出现:
20
因为 Writer.write(int) 中的 int 参数表示的是:
要写出的字符编码值。
因此如果我们想把各种 Java 数据:
int
double
boolean
char
String
Object
方便地转换成:
人类可以直接阅读的文本
Java 提供了:
打印流(Print Stream / Print Writer)
核心目的就是:
方便地打印各种数据值的文本表示。
2.2 Java 中两种主要打印流
Java IO 中主要学习:
PrintStream
PrintWriter
二者都支持:
print()
println()
printf()
format()
但是它们所属体系不同。
| 类 | 所属体系 | 特点 |
| ------------- | ---------- | ------------------- |
| PrintStream | 字节输出流 | 继承 OutputStream |
| PrintWriter | 字符输出流 | 继承 Writer |
继承关系可以简化为:
OutputStream
↑
FilterOutputStream
↑
PrintStream
以及:
Writer
↑
PrintWriter
所以:
PrintStream本质上还是字节输出流。
PrintWriter本质上是字符输出流。
2.3 打印流到底“打印”的是什么
假设:
int age = 20;
使用:
ps.println(age);
写入文件后得到的是类似:
20
也就是说:
int 20
↓
转换成文本表示
↓
"20"
↓
编码
↓
字节
↓
文件
所以打印流特别适合:
日志
报表
配置输出
调试信息
控制台信息
人类可读文本
2.4 System.out 本身就是 PrintStream
我们从 Java 第一节课就在使用:
System.out.println("Hello World");
现在终于可以解释它了。
System.out 的类型就是:
PrintStream
所以:
System.out.println()
本质上就是在使用:
打印流
可以理解成:
System
↓
out
↓
PrintStream
↓
标准输出设备
↓
控制台
所以 println() 并不是 Java 语法。
它只是:
PrintStream类提供的普通方法。
2.5 为什么还需要数据流
打印流解决:
把各种数据转换成人类可读文本
但假如需求是:
int age = 20;
double score = 95.5;
boolean passed = true;
我希望保存以后还能:
按照 int 读取
按照 double 读取
按照 boolean 读取
而不是全部变成字符串。
这时就需要:
DataOutputStream
DataInputStream
它们属于:
数据输入输出流(Data Streams)
核心能力:
Java 基本数据类型
↓
二进制形式
↓
文件
读取时:
文件中的二进制数据
↓
按照原始数据类型恢复
↓
Java 基本数据类型
2.6 数据流的两个核心类
DataOutputStream
作用:
将 Java 基本数据类型按照规定的二进制格式写入底层输出流。
例如:
writeByte()
writeBoolean()
writeShort()
writeChar()
writeInt()
writeLong()
writeFloat()
writeDouble()
writeUTF()
DataInputStream
作用:
从底层输入流中读取二进制数据,并恢复成对应 Java 数据类型。
例如:
readByte()
readBoolean()
readShort()
readChar()
readInt()
readLong()
readFloat()
readDouble()
readUTF()
二者通常配套:
DataOutputStream
↓
写出
↓
二进制文件
↓
读取
↓
DataInputStream
三、使用方法
3.1 PrintStream 基础使用
最简单:
import java.io.PrintStream;
public class PrintStreamDemo {
public static void main(String[] args) throws Exception {
try (PrintStream ps = new PrintStream("data.txt")) {
ps.println(97);
ps.println('A');
ps.println("Java");
ps.println(true);
ps.println(99.9);
}
}
}
文件内容类似:
97
A
Java
true
99.9
注意:
ps.println(97);
写入的不是:
ASCII 97 对应的字符 a
而是:
97
因为这是:
println(int)
它打印的是整数值的文本表示。
这正是打印流和普通:
OutputStream.write(int)
最容易混淆的地方。
3.2 print() 与 println()
print()
打印数据,但不主动结束当前行:
ps.print("Java");
ps.print("MySQL");
ps.print("Redis");
结果:
JavaMySQLRedis
println()
打印数据并结束当前行:
ps.println("Java");
ps.println("MySQL");
ps.println("Redis");
结果:
Java
MySQL
Redis
所以:
print
=
打印
println
=
打印 + 换行
3.3 printf() 格式化输出
打印流还支持:
printf()
例如:
String name = "张三";
int age = 20;
double score = 95.567;
ps.printf("姓名:%s,年龄:%d,成绩:%.2f%n",
name, age, score);
结果:
姓名:张三,年龄:20,成绩:95.57
常见格式:
| 格式 | 含义 |
| ------ | ------------ |
| %s | 字符串 |
| %d | 十进制整数 |
| %f | 浮点数 |
| %.2f | 保留两位小数 |
| %n | 平台相关换行 |
这使打印流非常适合:
报表
日志
成绩单
表格化文本
3.4 PrintStream 追加写入
如果直接:
new PrintStream("log.txt")
已有文件通常会被覆盖。
如果希望追加:
PrintStream ps =
new PrintStream(
new FileOutputStream("log.txt", true)
);
例如:
import java.io.FileOutputStream;
import java.io.PrintStream;
public class LogDemo {
public static void main(String[] args) throws Exception {
try (
PrintStream ps =
new PrintStream(
new FileOutputStream("log.txt", true)
)
) {
ps.println("用户登录成功");
}
}
}
这里体现了我们前面反复学习的:
包装流思想
结构:
PrintStream
↓
FileOutputStream
↓
log.txt
3.5 PrintWriter
PrintWriter 与 PrintStream 的打印接口非常相似:
import java.io.PrintWriter;
public class PrintWriterDemo {
public static void main(String[] args) throws Exception {
try (PrintWriter writer =
new PrintWriter("report.txt")) {
writer.println("Java 学习报告");
writer.println(100);
writer.println(95.5);
writer.printf("完成度:%.1f%%%n", 87.5);
}
}
}
3.6 PrintStream 与 PrintWriter 怎么选
简单理解:
PrintStream
→ 字节输出体系
PrintWriter
→ 字符输出体系
如果主要目标是:
输出文本
PrintWriter 的语义更加直接。
如果已经拥有:
OutputStream
或者使用:
System.out
System.err
那么经常会接触 PrintStream。
实际开发中,不要机械理解成:
PrintWriter 一定比 PrintStream 好
应该根据:
底层目标
+
数据类型
+
现有 API
进行选择。
3.7 PrintWriter 包装其他字符流
例如:
PrintWriter writer =
new PrintWriter(
new BufferedWriter(
new FileWriter("result.txt")
)
);
可以理解成:
PrintWriter
负责:
方便打印各种数据
↓
BufferedWriter
负责:
字符缓冲
↓
FileWriter
负责:
写文件
于是:
打印能力
+
缓冲能力
+
文件输出能力
被组合到了一起。
这就是 Java IO 的典型设计方式。
四、数据流的使用
4.1 DataOutputStream
创建方式:
DataOutputStream dos =
new DataOutputStream(
new FileOutputStream("data.bin")
);
完整案例:
import java.io.DataOutputStream;
import java.io.FileOutputStream;
public class DataOutputDemo {
public static void main(String[] args) throws Exception {
try (
DataOutputStream dos =
new DataOutputStream(
new FileOutputStream("data.bin")
)
) {
dos.writeByte(34);
dos.writeUTF("你好");
dos.writeInt(3665);
dos.writeDouble(9.9);
dos.writeBoolean(true);
}
}
}
这里并不是把数据写成:
34
你好
3665
9.9
true
这种普通文本。
而是:
byte
String
int
double
boolean
分别按照数据流规定的二进制格式写入。
因此通常把文件命名成:
data.bin
data.dat
比:
data.txt
更加符合语义。
4.2 DataInputStream
读取:
import java.io.DataInputStream;
import java.io.FileInputStream;
public class DataInputDemo {
public static void main(String[] args) throws Exception {
try (
DataInputStream dis =
new DataInputStream(
new FileInputStream("data.bin")
)
) {
byte value1 = dis.readByte();
String value2 = dis.readUTF();
int value3 = dis.readInt();
double value4 = dis.readDouble();
boolean value5 = dis.readBoolean();
System.out.println(value1);
System.out.println(value2);
System.out.println(value3);
System.out.println(value4);
System.out.println(value5);
}
}
}
输出:
34
你好
3665
9.9
true
4.3 最重要规则:读写顺序必须一致
假设写入:
dos.writeInt(100);
dos.writeDouble(95.5);
dos.writeBoolean(true);
那么读取必须对应:
dis.readInt();
dis.readDouble();
dis.readBoolean();
结构:
写入:
int
↓
double
↓
boolean
读取:
int
↓
double
↓
boolean
如果错误写成:
dis.readDouble();
dis.readInt();
dis.readBoolean();
程序不会知道:
“这里本来应该是 int。”
因为文件中本质只有:
连续的 byte
Java 不会自动在每段数据前附加:
这里是 int
这里是 double
这种类型标签。
4.4 为什么错误顺序会导致错误结果
假设:
dos.writeInt(100);
int 占:
4 byte
随后:
dos.writeDouble(95.5);
double 占:
8 byte
文件大致:
[ int 的4字节 ][ double 的8字节 ]
如果读取时第一步:
dis.readDouble();
它会直接取:
8 byte
于是:
int 的4字节
+
double 前4字节
被错误解释成一个 double。
从这一刻起:
整个读取边界全部错位
所以数据流最重要的规则:
写什么类型,就必须按相同顺序读什么类型。
4.5 DataOutputStream 常用 API
| 写入方法 | 对应类型 |
| ---------------- | -------- |
| writeBoolean() | boolean |
| writeByte() | byte |
| writeShort() | short |
| writeChar() | char |
| writeInt() | int |
| writeLong() | long |
| writeFloat() | float |
| writeDouble() | double |
| writeUTF() | String |
读取:
| 读取方法 | 返回类型 |
| --------------- | -------- |
| readBoolean() | boolean |
| readByte() | byte |
| readShort() | short |
| readChar() | char |
| readInt() | int |
| readLong() | long |
| readFloat() | float |
| readDouble() | double |
| readUTF() | String |
你会发现:
writeXxx
↔
readXxx
基本一一对应。
五、原理与进阶
5.1 打印流与数据流最核心的区别
假设:
int value = 97;
PrintStream
ps.print(value);
保存的是:
"97"
也就是:
数值 97 的文本表示。
DataOutputStream
dos.writeInt(value);
保存的是:
int 97对应的 4 个二进制字节。
因此:
PrintStream
97
↓
"97"
↓
字符编码
↓
文件
DataOutputStream
97
↓
int 二进制表示
↓
文件
这就是两类流最重要的区别。
5.2 文本格式和二进制格式
可以进一步理解成:
文本格式
例如:
1001,张三,95.5
优点:
人类可读
容易查看
容易编辑
跨语言交换方便
缺点:
需要解析字符串
某些数据体积可能更大
类型信息需要协议约定
二进制格式
例如:
DataOutputStream
写出的数据。
优点:
可以按照基本数据类型直接读写
格式紧凑
不需要先转换成人类文本
缺点:
人类无法直接阅读
必须严格遵守数据格式协议
顺序或类型错误容易导致解析失败
5.3 writeUTF() 不是普通文本 UTF-8 文件写入
这是一个很重要的进阶点。
dos.writeUTF("你好");
不要简单理解成:
“使用普通 UTF-8 把你好写入 txt。”
DataOutputStream.writeUTF() 使用的是:
modified UTF-8
也就是:
修改版 UTF-8(Modified UTF-8)
而且它还会携带用于读取字符串长度的信息。
对应:
DataInputStream.readUTF()
进行恢复。
所以:
writeUTF()
和:
Files.writeString(
path,
text,
StandardCharsets.UTF_8
)
不是同一种文本文件格式。
如果目标是普通 UTF-8 文本文件:
优先使用字符流 / Files
而不是:
DataOutputStream.writeUTF()
5.4 PrintStream 的异常处理特点
普通输出流很多方法:
write()
flush()
可能抛出:
IOException
但 PrintStream 的设计比较特殊:
打印方法本身不会把
IOException直接向调用者抛出,而是记录内部错误状态。
可以通过:
ps.checkError()
判断是否发生错误。
PrintWriter 也采用类似设计。
这也是为什么:
System.out.println(...)
几乎从来不用:
try-catch IOException
5.5 自动刷新 autoFlush
PrintStream 和 PrintWriter 都支持某些带:
boolean autoFlush
参数的构造器。
例如:
PrintWriter writer =
new PrintWriter(
outputStream,
true
);
true 表示开启自动刷新。
不过二者触发自动刷新的细节并不完全一致。
学习阶段核心先掌握:
自动刷新不是“任何一次 print 都立即 flush”。
因此不能把:
autoFlush = true
理解成:
完全没有缓冲
六、实践应用
6.1 使用 PrintWriter 生成成绩单
import java.io.PrintWriter;
public class StudentReportDemo {
public static void main(String[] args) throws Exception {
try (PrintWriter writer =
new PrintWriter("students.txt")) {
writer.println("学生成绩表");
writer.println("------------------------");
writer.printf("%-10s %-8s %-8s%n",
"姓名", "Java", "MySQL");
writer.printf("%-10s %-8d %-8d%n",
"张三", 95, 90);
writer.printf("%-10s %-8d %-8d%n",
"李四", 88, 93);
}
}
}
核心价值:
结构化格式
+
人类可读
+
方便导出
6.2 使用打印流写日志
try (
PrintWriter writer =
new PrintWriter(
new FileOutputStream(
"app.log",
true
)
)
) {
writer.println("用户登录:zhangsan");
}
当然,真实企业项目通常会使用:
Logback
Log4j2
SLF4J
等专业日志框架。
这里只是用来理解:
日志最终也是一种输出。
6.3 保存游戏状态
假设一个小游戏有:
int level = 8;
double hp = 72.5;
boolean bossDefeated = false;
String playerName = "星雨";
可以:
try (
DataOutputStream dos =
new DataOutputStream(
new FileOutputStream("save.dat")
)
) {
dos.writeUTF(playerName);
dos.writeInt(level);
dos.writeDouble(hp);
dos.writeBoolean(bossDefeated);
}
读取:
try (
DataInputStream dis =
new DataInputStream(
new FileInputStream("save.dat")
)
) {
String playerName = dis.readUTF();
int level = dis.readInt();
double hp = dis.readDouble();
boolean bossDefeated = dis.readBoolean();
}
关键不是记代码。
而是意识到:
写入协议
=
String → int → double → boolean
读取协议
=
String → int → double → boolean
两边必须完全一致。
七、练习与验收
7.1 知识问答
- [ ] Java 中两种主要打印流分别是什么?
- [ ]
PrintStream属于字节流还是字符流? - [ ]
PrintWriter属于字节流还是字符流? - [ ] 打印流最大的价值是什么?
- [ ]
print()与println()有什么区别? - [ ]
printf()有什么作用? - [ ]
System.out的真实类型是什么? - [ ]
OutputStream.write(97)与PrintStream.println(97)有什么本质区别? - [ ]
DataInputStream与DataOutputStream分别有什么作用? - [ ] 为什么数据流读写顺序必须一致?
- [ ]
writeInt()后能否使用readDouble()对应读取?为什么? - [ ] 数据流写出的文件为什么通常不适合使用记事本查看?
- [ ] 打印流与数据流在数据表示方式上有什么根本区别?
- [ ]
writeUTF()写出的字符串是否等价于普通 UTF-8 文本文件?
7.2 代码阅读
不运行:
try (PrintStream ps = new PrintStream("a.txt")) {
ps.println(97);
ps.print('A');
ps.print(true);
}
回答:
- [ ] 文件中
97是字符a还是字符串形式的97? - [ ]
A后面会不会自动换行? - [ ]
true会以什么形式写入?
阅读:
dos.writeInt(100);
dos.writeDouble(95.5);
int a = dis.readInt();
double b = dis.readDouble();
回答:
- [ ] 为什么这样读取是正确的?
- [ ] 如果交换两个
read方法会发生什么?
7.3 手写代码
关闭 AI 自动补全,独立完成:
- [ ] 使用
PrintStream写入姓名、年龄、成绩。 - [ ] 使用
PrintWriter生成一个学生成绩表。 - [ ] 使用
printf()输出格式化成绩。 - [ ] 使用追加模式向日志文件写入三条日志。
- [ ] 使用
DataOutputStream写出String + int + double。 - [ ] 使用
DataInputStream按相同顺序读取。 - [ ] 保存一个简单游戏角色状态,再完整恢复。
7.4 Debug
下面代码:
try (
DataOutputStream dos =
new DataOutputStream(
new FileOutputStream("data.dat")
)
) {
dos.writeInt(100);
dos.writeDouble(66.6);
}
读取:
try (
DataInputStream dis =
new DataInputStream(
new FileInputStream("data.dat")
)
) {
System.out.println(dis.readDouble());
System.out.println(dis.readInt());
}
要求:
- [ ] 判断逻辑是否正确。
- [ ] 解释根本原因。
- [ ] 修复读取代码。
- [ ] 画出字节边界错位过程。
7.5 综合训练
开发一个:
学生成绩二进制存档程序
每名学生包含:
姓名 String
年龄 int
Java成绩 double
是否及格 boolean
要求:
- 使用
DataOutputStream写入 3 名学生。 - 使用
DataInputStream完整读取。 - 明确设计文件协议。
- 先写出:
字段顺序
字段类型
读写对应 API
例如:
name → writeUTF → readUTF
age → writeInt → readInt
score → writeDouble→ readDouble
passed → writeBoolean→readBoolean
再编写代码。
7.6 本章验收
不查看资料,能够口述:
PrintStream
PrintWriter
DataOutputStream
DataInputStream
四个类分别解决什么问题。
能够准确解释:
println(97)
和:
writeInt(97)
为什么完全不是一回事。
最后能够独立完成:
PrintWriter → 生成人类可读报表
DataOutputStream → 保存类型化二进制数据
DataInputStream → 按相同协议恢复数据
达到以上标准,本章核心内容即掌握。