PrintStream 和 PrintWriter 有什么区别?System.out 是什么、为什么它是「字节流」却能打印字符?
简化版
PrintStream 和 PrintWriter 都是「能方便地打印各种数据(print/println/printf)」的处理流,区别主要在:PrintStream 是字节流(OutputStream 的子类),PrintWriter 是字符流(Writer 的子类)。它们的共同特点:① 提供 print/println/printf/format 等便利方法,能直接打印 int、String、对象等(自动转成文本);② 不抛 IO 异常——内部把异常吞掉,用 checkError() 查错(所以打印日志/控制台不会因为 IO 异常中断程序)。主要区别:PrintWriter 是字符流、更适合处理文本(编码控制更清晰)、是更推荐的选择;PrintStream 主要因为历史原因存在(System.out/System.err 就是 PrintStream)。System.out 是什么:它是一个 PrintStream,连到「标准输出」(控制台)。它虽然是「字节流」,但能打印字符——因为 PrintStream 内部会用某个字符编码(默认平台编码)把字符转成字节再输出。关键区别点:PrintStream 用平台默认编码(可能导致跨平台乱码),PrintWriter 可以显式指定编码、更可控——所以写文件优先用 PrintWriter 并指定 UTF-8。
详细版
PrintStream vs PrintWriter:
| 维度 | PrintStream | PrintWriter |
|---|---|---|
| 继承 | OutputStream(字节流) | Writer(字符流) |
| 便利方法 | print/println/printf/format | 相同 |
| 异常 | 不抛 IO 异常(checkError 查) | 同样 |
| 编码控制 | 较弱(易用平台默认编码) | 更清晰(可指定 Charset) |
| 自动刷新 | 可选(println 时可 flush) | 可选(println 时可 flush) |
| 典型 | System.out/System.err | 写文本文件、Servlet 响应 |
// PrintStream:System.out 就是它
System.out.println("Hello"); // println 打印并换行
System.out.printf("%d + %d = %d%n", 1, 2, 3); // 格式化打印
// PrintWriter:写文本文件(推荐,指定编码)
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream("out.txt"), StandardCharsets.UTF_8))) {
pw.println("第一行");
pw.printf("数值: %.2f%n", 3.14159);
}
// 便捷构造(Java 会自动包装,但注意编码)
PrintWriter pw2 = new PrintWriter("out.txt", "UTF-8"); // 指定编码
// checkError 查错(PrintStream/PrintWriter 不抛 IO 异常)
System.out.println(...);
if (System.out.checkError()) { /* 打印出错了 */ }
⚠️
PrintStream(尤其System.out)的一个隐藏坑是「编码」——它是字节流,打印字符时要把字符转成字节,用的是「平台默认编码」(Windows 中文环境常是 GBK,Linux 常是 UTF-8)。这就导致:同一段中文,在 Windows 控制台和 Linux 控制台、或写到文件时,可能因为编码不同而乱码。而PrintWriter允许你显式指定编码(new PrintWriter(file, "UTF-8")),把编码这件事握在自己手里。所以规律是:控制台调试打印用System.out(PrintStream)无所谓;但写文件、生成文本内容,优先用PrintWriter并显式指定 UTF-8,避免「本地不乱码、换个环境就乱码」。JDK 18+ 起,System.out的默认编码逐步改为 UTF-8(file.encoding相关变化),但显式指定编码仍是最稳妥的做法。
完整版教学
一、print 系列:便利的打印方法
先理解 PrintStream/PrintWriter 提供的便利:
普通流写数据很原始:
OutputStream.write(byte[]) 只能写字节
想写 "count=42" → 要自己把字符串、数字转成字节
PrintStream/PrintWriter 提供便利的打印方法:
print(x) 打印 x(int/long/double/boolean/char/String/Object 都行)
println(x) 打印 x 并换行
printf(fmt, args) 格式化打印(类似 C 的 printf)
format(...) 同 printf
→ 自动把各种类型转成文本输出,不用手动转
例:
pw.print(42); // "42"
pw.println("hello"); // "hello\n"
pw.printf("%s: %d%n", "count", 42); // "count: 42\n"
→ %n 是平台无关的换行符(比 \n 更好,跨平台)
所以 print 系列 = "方便地把各种数据打印成文本"
PrintStream/PrintWriter 提供便利的打印方法:print(x)(打印各种类型)、println(x)(打印并换行)、printf(fmt, args)/format(格式化打印,类似 C 的 printf)——自动把各种类型转成文本输出,不用手动转。%n 是平台无关的换行符(比 \n 好)。理解「print 系列便利方法:print/println/printf/format 自动把各种类型转文本、%n 平台无关换行、免去手动转字节」,就理解了它们的便利。
二、字节流 vs 字符流:核心区别
PrintStream 和 PrintWriter 的核心区别是「字节流 vs 字符流」:
PrintStream extends FilterOutputStream extends OutputStream
→ 是"字节流"
PrintWriter extends Writer
→ 是"字符流"
字节流 vs 字符流的意义:
字节流:底层处理字节,打印字符时要"字符→字节"的编码转换
字符流:底层处理字符,专门为文本设计,编码处理更清晰
为什么这个区别重要——编码:
PrintStream(字节流):字符转字节时,用"平台默认编码"
→ 不同平台可能不同(Windows GBK、Linux UTF-8)→ 易乱码
PrintWriter(字符流):可以在构造时明确指定编码
→ 编码可控,不依赖平台默认
所以推荐:
PrintWriter 是更现代、更推荐的选择(字符流、编码可控)
PrintStream 主要因历史存在(System.out 用它)
核心区别是「字节流 vs 字符流」:PrintStream 继承 OutputStream(字节流)、PrintWriter 继承 Writer(字符流)。意义在编码——字节流打印字符要「字符→字节」转换用平台默认编码(不同平台不同易乱码),字符流专为文本设计、编码更可控(构造时可指定)。所以 PrintWriter 是更推荐的选择(字符流、编码可控),PrintStream 主要因历史存在。理解「PrintStream 字节流(继承 OutputStream)vs PrintWriter 字符流(继承 Writer)、区别在编码(字节流用平台默认易乱码/字符流可指定编码)、PrintWriter 更推荐」,就抓住了两者的核心区别。
三、不抛 IO 异常:checkError
PrintStream/PrintWriter 有个特殊行为——「不抛 IO 异常」:
普通流的 write 会抛 IOException:
out.write(...); // 可能抛 IOException,要 try-catch 或声明
PrintStream/PrintWriter 的 print/println 不抛 IOException:
System.out.println(...); // 不用 try-catch!
→ 它内部把 IOException 吞掉了
为什么这样设计:
打印(尤其控制台输出、日志)是"辅助性"的
如果每次 println 都要处理 IO 异常,代码会很啰嗦
→ 吞掉异常,让打印代码简洁(println 到处用不用 try-catch)
那出错了怎么知道?
checkError():返回 true 表示之前的操作出过错
System.out.println(...);
if (System.out.checkError()) { /* 出错了 */ }
权衡:
✓ 好处:打印代码简洁(不用处理异常)
✗ 代价:异常被静默吞掉,出错不明显(要主动 checkError)
→ 对"关键的、不能丢的输出",别只依赖 PrintWriter(可能静默失败)
PrintStream/PrintWriter 的特殊行为是「不抛 IO 异常」——print/println 内部把 IOException 吞掉(所以 System.out.println 不用 try-catch)。设计原因:打印是辅助性的,每次都处理异常太啰嗦。出错了用 checkError() 查(返回 true 表示出过错)。权衡:好处是打印代码简洁、代价是异常被静默吞掉(关键输出别只依赖它,可能静默失败)。理解「PrintStream/PrintWriter 不抛 IO 异常(内部吞掉,println 不用 try-catch)、设计因打印是辅助性、出错用 checkError 查、代价是异常静默吞掉」,就理解了这个特殊行为。
四、System.out:标准输出
System.out 是最常见的 PrintStream,理解它:
System 类的三个标准流:
System.out:标准输出(控制台正常输出)—— PrintStream
System.err:标准错误(控制台错误输出,通常红色)—— PrintStream
System.in:标准输入(键盘)—— InputStream
System.out 是一个 PrintStream:
连到"标准输出"(默认是控制台)
System.out.println(...) → 打印到控制台
为什么 System.out(字节流)能打印中文字符:
PrintStream 内部:把字符用某编码转成字节再输出
→ 所以字节流也能"打印字符"(内部做了字符→字节转换)
→ 但用的是平台默认编码 → 跨平台可能乱码(前面讲的坑)
可以重定向:
System.setOut(new PrintStream("output.log"))
→ 把标准输出重定向到文件(原来打印到控制台的,现在写文件)
System.err vs System.out:
err 用于错误信息(通常不缓冲、立即输出、控制台标红)
out 用于正常输出
→ 分开便于区分正常输出和错误、便于分别重定向
System.out 是连到「标准输出(控制台)」的 PrintStream(System.err 标准错误、System.in 标准输入)。它是字节流却能打印中文——因为 PrintStream 内部把字符用某编码转成字节再输出(但用平台默认编码、跨平台可能乱码)。可以 System.setOut() 重定向到文件。System.err(错误、通常不缓冲立即输出、标红)和 System.out(正常输出)分开便于区分和分别重定向。理解「System.out 是连到控制台的 PrintStream、字节流能打印字符因内部字符转字节(用平台默认编码可能乱码)、可 setOut 重定向、err 和 out 分开便于区分」,就理解了 System.out。
五、自动刷新
PrintStream/PrintWriter 可以配置「自动刷新」:
自动刷新(autoFlush):
构造时可指定 autoFlush=true
new PrintWriter(out, true) // 自动刷新
new PrintStream(out, true)
autoFlush=true 时:
PrintStream:写入 byte[]、写 \n、或调 println 时自动 flush
PrintWriter:只有调 println/printf/format 时自动 flush
→ 保证输出及时可见(不会攒在缓冲区里)
为什么控制台输出要自动刷新:
System.out 是自动刷新的
→ println 后立即显示(否则调试时看不到输出,攒在缓冲区)
写文件时:
如果没自动刷新,要记得 flush/close(否则缓冲区数据丢失)
用 try-with-resources(close 会 flush)最稳妥
权衡:
自动刷新:输出及时,但频繁 flush 可能影响性能(大量输出时)
不自动刷新:性能好,但要自己管 flush/close
PrintStream/PrintWriter 可配置自动刷新(autoFlush)——构造时指定 true:PrintStream 在写 \n/println 时自动 flush、PrintWriter 在 println/printf 时自动 flush,保证输出及时可见。System.out 是自动刷新的(println 后立即显示)。写文件时若没自动刷新要记得 flush/close(try-with-resources 最稳妥)。权衡:自动刷新输出及时但频繁 flush 影响性能、不自动刷新性能好但要自己管。理解「自动刷新 autoFlush 在 println 时自动 flush 保证输出及时、System.out 是自动刷新的、写文件没自动刷新要 flush/close、权衡及时性和性能」,就掌握了自动刷新。
六、实践建议
总结 PrintStream/PrintWriter 的实践建议:
使用建议:
① 控制台调试打印 → System.out(PrintStream)
简单、自动刷新,不用管编码(调试用无所谓)
② 写文本文件 → PrintWriter,显式指定 UTF-8
new PrintWriter(new OutputStreamWriter(
new FileOutputStream(f), StandardCharsets.UTF_8))
→ 编码可控,避免跨平台乱码
③ Servlet 响应输出 → response.getWriter()(是 PrintWriter)
④ 关键输出 → 注意异常被吞(用 checkError 或别只依赖它)
编码规律(重要):
写文件/生成文本 → 一定显式指定编码(UTF-8)
别用依赖平台默认编码的方式(PrintStream 默认、PrintWriter(file) 默认)
→ "本地不乱码、换环境就乱码"的坑就是这么来的
选择:
字符输出、要控制编码 → PrintWriter(推荐)
System.out/err → PrintStream(历史,用就行)
一句话:调试用 System.out,写文件用 PrintWriter + 显式 UTF-8
实践建议:控制台调试打印用 System.out(简单、自动刷新、不用管编码);写文本文件用 PrintWriter 显式指定 UTF-8(编码可控避免跨平台乱码);Servlet 响应用 response.getWriter()(是 PrintWriter);关键输出注意异常被吞(用 checkError)。编码规律:写文件/生成文本一定显式指定 UTF-8(别用依赖平台默认编码的方式,否则「本地不乱码换环境就乱码」)。理解「实践:调试用 System.out、写文件用 PrintWriter+显式 UTF-8、Servlet 用 getWriter、关键输出注意异常被吞;编码规律写文件一定指定 UTF-8」,就掌握了实践建议。
记忆钩子:「PrintStream vs PrintWriter 都提供便利打印(print/println/printf 自动转文本)、都不抛 IO 异常(内部吞掉,println 不用 try-catch,出错用 checkError 查);核心区别:PrintStream 是字节流(继承 OutputStream)、PrintWriter 是字符流(继承 Writer);区别在编码-PrintStream 打印字符用平台默认编码(跨平台易乱码)、PrintWriter 可指定编码(更可控,推荐);System.out 是连控制台的 PrintStream,字节流能打印字符因内部字符转字节(用平台默认编码);autoFlush 在 println 时自动刷新(System.out 是自动刷新的);★实践:调试用 System.out、写文件用 PrintWriter+显式 UTF-8(避免跨平台乱码)」。
七、常见误区与追问
- 误区:System.out.println 会抛 IOException,要 try-catch。 不会——PrintStream/PrintWriter 的 print/println 内部把 IOException 吞掉了,所以 println 不用 try-catch;出错了用 checkError() 查(返回 true 表示出过错);代价是异常被静默吞掉,关键输出别只依赖它。
- 误区:System.out 是字节流,不能打印中文字符。 能打印——PrintStream 内部会把字符用某编码转成字节再输出,所以字节流也能「打印字符」;只是用的是平台默认编码(Windows GBK、Linux UTF-8),跨平台可能乱码。
- 误区:PrintStream 和 PrintWriter 只是名字不同、功能一样。 核心区别是字节流 vs 字符流——PrintStream 继承 OutputStream(字节流,编码用平台默认易乱码)、PrintWriter 继承 Writer(字符流,编码可显式指定更可控);写文本推荐 PrintWriter 并指定 UTF-8。
- 误区:写文件用 PrintWriter 不用管编码。 要管——PrintWriter(file) 这种便捷构造用的是平台默认编码,仍可能跨平台乱码;应显式指定 UTF-8:new PrintWriter(new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)),或 new PrintWriter(file, “UTF-8”)。
- 追问:为什么 System.out(字节流)能打印字符? PrintStream 虽然是字节流(底层写字节),但它的 print/println 方法内部会先把字符(String/char)用某个字符编码转换成字节,再写到底层的字节输出流;用的编码是平台默认编码(JDK 18+ 逐步改为 UTF-8),所以字节流也能「打印字符」,只是编码不可控时跨平台可能乱码。
- 追问:PrintStream/PrintWriter 不抛 IO 异常,怎么知道输出出错了? 用 checkError() 方法——它返回 boolean,true 表示之前的某次输出操作发生过错误(且被内部吞掉了);因为 print/println 为了简洁不抛 IOException,出错信息只能通过 checkError 主动查询;对关键的、不能丢的输出,要么 checkError,要么用会抛异常的底层流。
- 追问:写文本文件为什么优先用 PrintWriter 并指定 UTF-8? 因为 PrintWriter 是字符流、能显式指定编码,把编码握在自己手里;如果用 PrintStream 或不指定编码,会用平台默认编码(Windows GBK、Linux UTF-8),导致「本地不乱码、换个环境就乱码」;显式指定 UTF-8 保证任何环境下编码一致、不乱码。
八、加强记忆
PrintStream 和 PrintWriter 都提供便利的打印方法(print/println/printf/format,自动把各种类型转成文本)、都不抛 IO 异常(内部吞掉,println 不用 try-catch,出错用 checkError() 查——代价是异常静默吞掉)。核心区别是字节流 vs 字符流:PrintStream 继承 OutputStream(字节流,打印字符用平台默认编码、跨平台易乱码);PrintWriter 继承 Writer(字符流,编码可显式指定、更可控,是更推荐的选择)。System.out 是连到标准输出(控制台)的 PrintStream(System.err 标准错误、System.in 标准输入)——它是字节流却能打印字符,因为 PrintStream 内部把字符用某编码转成字节再输出(用平台默认编码,可能乱码;可 System.setOut() 重定向)。可配置自动刷新(autoFlush)(println 时自动 flush 保证输出及时,System.out 是自动刷新的)。编码规律(重要):写文件/生成文本一定显式指定 UTF-8(new PrintWriter(new OutputStreamWriter(new FileOutputStream(f), UTF_8))),别用依赖平台默认编码的方式,避免「本地不乱码换环境就乱码」。一句话「PrintStream(字节流,平台默认编码易乱码)vs PrintWriter(字符流,编码可指定更推荐),都提供 print/println/printf 自动转文本、都不抛 IO 异常(checkError 查);System.out 是连控制台的 PrintStream,字节流能打印字符因内部字符转字节;写文件用 PrintWriter+显式 UTF-8 避免跨平台乱码」。