转换流 详解
场景:飞翔科技的新人小崔刚入职不久,就接到了一个棘手的任务——把财务部积累了三年的GBK编码工资报表,全部转成UTF-8格式导入新系统。小崔一拍脑袋直接用FileReader读,结果全是乱码。白歌端着咖啡路过,瞄了一眼屏幕:"字符编码都不指定,你这是在赌运气啊。"大翔哈哈一笑:"别慌,让白歌给你讲讲转换流的门道,这一关过了,编码问题以后就是小菜一碟。"Frank推了推眼镜补充道:"And don't forget, System.in is also a byte stream, you'll need a bridge to read Chinese from the console."
一、定义表
| 概念 | 定义 |
|---|---|
| 转换流 | 字节流与字符流之间的桥梁,负责将字节按指定字符集编解码为字符(或反向操作) |
| InputStreamReader | 字节输入流 → 字符输入流的桥梁,读取字节并用指定charset解码为字符 |
| OutputStreamWriter | 字符输出流 → 字节输出流的桥梁,将字符用指定charset编码为字节后写出 |
| 字符编码 (Encode) | 将字符序列按某种规则映射为字节序列的过程 |
| 字符解码 (Decode) | 将字节序列按某种规则还原为字符序列的过程 |
| Charset | JDK中表示字符集的类,常用别名如 "UTF-8"、"GBK"、"ISO-8859-1" |
| BOM (Byte Order Mark) | 某些编码格式(如UTF-8 BOM、UTF-16)在文件头部放置的标记字节序列,用于标识编码方式 |
| System.in / System.out | 标准输入(InputStream)和标准输出(PrintStream),均为字节流,控制台交互时通常需要转换流桥接 |
二、Mermaid 流程图
三、InputStreamReader 详解
3.1 核心构造方法
// 使用系统默认字符集
InputStreamReader(InputStream in)
// 使用指定字符集名称
InputStreamReader(InputStream in, String charsetName)
// 使用 Charset 对象
InputStreamReader(InputStream in, Charset cs)
3.2 字符集支持检测
import java.nio.charset.Charset;
import java.util.Map;
// 查看JVM支持的字符集
Map<String, Charset> charsets = Charset.availableCharsets();
System.out.println("JDK 8 支持的字符集数量: " + charsets.size());
四、OutputStreamWriter 详解
4.1 核心构造方法
// 使用系统默认字符集
OutputStreamWriter(OutputStream out)
// 使用指定字符集名称
OutputStreamWriter(OutputStream out, String charsetName)
// 使用 Charset 对象
OutputStreamWriter(OutputStream out, Charset cs)
4.2 关键注意事项
- OutputStreamWriter 内部维护了一个
StreamEncoder,负责实际的编码转换。 - 调用
flush()会将缓冲区中的编码后字节刷到底层OutputStream,但不会刷新底层流本身的缓冲。 - 关闭 OutputStreamWriter 时会自动调用
flush(),然后关闭底层的 OutputStream。
五、完整代码示例
示例1:飞翔科技 GBK 员工工资文件转 UTF-8
场景:财务部朱璐丢给小崔一个
salary_gbk.csv,是GBK编码的,要求转成UTF-8供新系统导入。文件名格式为工号,姓名,部门,基本工资,绩效奖金。
import java.io.*;
/**
* 飞翔科技 - GBK编码薪资文件转UTF-8
* 场景:财务部朱璐要求将旧系统的GBK编码薪资报表转换为UTF-8
* 作者:大翔指导 / 小崔编码
*/
public class SalaryFileEncodingConverter {
public static void main(String[] args) {
String inputFile = "d:/feixiang/salary_gbk.csv";
String outputFile = "d:/feixiang/salary_utf8.csv";
// 第一步:先在本地准备测试文件(实际环境中文件已存在)
prepareTestFile(inputFile);
// 第二步:执行 GBK → UTF-8 转换
convertGbkToUtf8(inputFile, outputFile);
// 第三步:验证转换结果
verifyOutput(outputFile);
}
/**
* 准备测试用的 GBK 编码文件
*/
public static void prepareTestFile(String filePath) {
// 确保父目录存在
File file = new File(filePath);
file.getParentFile().mkdirs();
try (OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream(filePath), "GBK")) {
writer.write("1001,张三,研发部,15000,3000\n");
writer.write("1002,李四,财务部,12000,2000\n");
writer.write("1003,王五,市场部,13000,2500\n");
writer.write("1004,大翔,架构部,20000,5000\n");
writer.write("1005,白歌,研发部,18000,4000\n");
System.out.println("[小崔] 测试GBK文件已准备好: " + filePath);
} catch (IOException e) {
System.err.println("[小崔] 准备测试文件失败: " + e.getMessage());
}
}
/**
* 核心方法:GBK → UTF-8 转换
* 使用 InputStreamReader 指定源编码为 GBK,
* 使用 OutputStreamWriter 指定目标编码为 UTF-8
*/
public static void convertGbkToUtf8(String srcPath, String destPath) {
// try-with-resources 自动关闭所有流
try (InputStreamReader isr = new InputStreamReader(
new FileInputStream(srcPath), "GBK"); // 以GBK解码读入
OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream(destPath), "UTF-8"); // 以UTF-8编码写出
BufferedReader reader = new BufferedReader(isr);
BufferedWriter writer = new BufferedWriter(osw)) {
String line;
int lineCount = 0;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
lineCount++;
}
System.out.println("[大翔] 转换完成! 共处理 " + lineCount + " 行数据");
System.out.println("[大翔] 源编码: GBK → 目标编码: UTF-8");
} catch (UnsupportedEncodingException e) {
System.err.println("[白歌] 不支持的编码格式: " + e.getMessage());
} catch (FileNotFoundException e) {
System.err.println("[小崔] 文件未找到: " + e.getMessage());
} catch (IOException e) {
System.err.println("[白歌] IO异常: " + e.getMessage());
}
}
/**
* 验证输出文件:以UTF-8读取并打印前几行
*/
public static void verifyOutput(String filePath) {
System.out.println("\n========== 转换结果验证 (UTF-8读取) ==========");
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(filePath), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(" " + line);
}
} catch (IOException e) {
System.err.println("[小崔] 验证失败: " + e.getMessage());
}
}
}
控制台输出:
[小崔] 测试GBK文件已准备好: d:/feixiang/salary_gbk.csv
[大翔] 转换完成! 共处理 5 行数据
[大翔] 源编码: GBK → 目标编码: UTF-8
========== 转换结果验证 (UTF-8读取) ==========
1001,张三,研发部,15000,3000
1002,李四,财务部,12000,2000
1003,王五,市场部,13000,2500
1004,大翔,架构部,20000,5000
1005,白歌,研发部,18000,4000
示例2:飞翔科技控制台中文输入交互系统
场景:Frank表示海外团队也需要用中文和总部系统交互。大翔写了一个控制台交互工具,使用
System.in配合InputStreamReader正确处理中文输入。小崔发现直接用Scanner在很多环境下也能读中文——白歌解释了系统默认编码的陷阱。
import java.io.*;
/**
* 飞翔科技 - 控制台中文交互系统
* 场景:总部需要一个支持中文输入输出的控制台工具
* 关键:System.in 是字节流,必须通过 InputStreamReader 桥接为字符流
* 作者:大翔
*/
public class ConsoleChineseInteraction {
public static void main(String[] args) {
// 方法1:System.in + InputStreamReader(推荐,明确指定编码)
method1_WithExplicitEncoding();
// 方法2:查看当前系统默认编码
method2_CheckDefaultCharset();
// 方法3:对比不同编码读取效果
method3_EncodingComparison();
}
/**
* 方法1:显式指定编码的控制台读取
* 这是最可靠的方式
*/
public static void method1_WithExplicitEncoding() {
System.out.println("=== 飞翔科技 控制台中文交互 (系统默认编码) ===");
System.out.print("[白歌] 请输入员工姓名: ");
// System.in 是 InputStream (字节流)
// InputStreamReader 将字节流桥接为字符流
// 不显式指定编码时使用系统默认编码 (中文Windows一般为GBK)
try (BufferedReader consoleReader = new BufferedReader(
new InputStreamReader(System.in))) {
String name = consoleReader.readLine();
System.out.println("[大翔] 您输入的姓名是: " + name);
System.out.println("[大翔] 姓名长度(字符数): " + name.length());
System.out.println("[白歌] System.in读取成功,使用系统默认编码: "
+ System.getProperty("file.encoding"));
} catch (IOException e) {
System.err.println("[小崔] 读取失败: " + e.getMessage());
}
}
/**
* 方法2:查看JVM和系统的编码信息
*/
public static void method2_CheckDefaultCharset() {
System.out.println("\n=== 飞翔科技 编码环境信息 ===");
System.out.println("[Frank] JVM file.encoding: "
+ System.getProperty("file.encoding"));
System.out.println("[Frank] JVM sun.jnu.encoding: "
+ System.getProperty("sun.jnu.encoding"));
System.out.println("[白歌] java.nio.charset.Charset.defaultCharset(): "
+ java.nio.charset.Charset.defaultCharset());
// JDK 8 中 Charset.defaultCharset() 的取值逻辑:
// 1. 如果设置了 file.encoding 系统属性,使用该值
// 2. 否则根据操作系统语言环境决定(中文Windows→GBK,Linux→UTF-8)
}
/**
* 方法3:演示错误编码导致乱码
* 用GBK编码写入的中文,用ISO-8859-1解码 → 乱码
*/
public static void method3_EncodingComparison() {
System.out.println("\n=== 编码/解码不匹配导致的乱码演示 ===");
String original = "飞翔科技-FeiXiangTech";
try {
// 1. 用GBK编码
byte[] gbkBytes = original.getBytes("GBK");
System.out.println("[大翔] 原始字符串: " + original);
System.out.println("[大翔] GBK编码后字节长度: " + gbkBytes.length);
// 2. 用UTF-8编码
byte[] utf8Bytes = original.getBytes("UTF-8");
System.out.println("[大翔] UTF-8编码后字节长度: " + utf8Bytes.length);
// 3. 错误解码:用ISO-8859-1解读GBK字节
String wrongDecode = new String(gbkBytes, "ISO-8859-1");
System.out.println("[小崔] 用ISO-8859-1解码GBK字节: " + wrongDecode);
System.out.println("[白歌] ↑ 这就是乱码的根源——编解码字符集不一致!");
// 4. 恢复方案:先按ISO-8859-1取回字节,再用GBK重建
byte[] recovered = wrongDecode.getBytes("ISO-8859-1");
String restored = new String(recovered, "GBK");
System.out.println("[大翔] 恢复后: " + restored);
} catch (UnsupportedEncodingException e) {
System.err.println("[小崔] 编码异常: " + e.getMessage());
}
}
}
控制台输出(运行在中文Windows环境下):
=== 飞翔科技 控制台中文交互 (系统默认编码) ===
[白歌] 请输入员工姓名: 大翔
[大翔] 您输入的姓名是: 大翔
[大翔] 姓名长度(字符数): 2
[白歌] System.in读取成功,使用系统默认编码: GBK
=== 飞翔科技 编码环境信息 ===
[Frank] JVM file.encoding: GBK
[Frank] JVM sun.jnu.encoding: GBK
[白歌] java.nio.charset.Charset.defaultCharset(): GBK
=== 编码/解码不匹配导致的乱码演示 ===
[大翔] 原始字符串: 飞翔科技-FeiXiangTech
[大翔] GBK编码后字节长度: 22
[大翔] UTF-8编码后字节长度: 27
[小崔] 用ISO-8859-1解码GBK字节: ·ÉÏè¿Æ¼¼-FeiXiangTech
[白歌] ↑ 这就是乱码的根源——编解码字符集不一致!
[大翔] 恢复后: 飞翔科技-FeiXiangTech
示例3:飞翔科技多编码格式日志文件自动检测与读取
场景:白歌发现运维部收集的服务器日志来自不同系统,有的是UTF-8,有的是GBK,甚至还有带BOM头的UTF-8。她写了一个自动检测编码的读取工具。
import java.io.*;
import java.nio.charset.Charset;
/**
* 飞翔科技 - 多编码日志文件自动检测与读取
* 场景:服务器日志来源于不同系统,编码不统一
* 作者:白歌
*/
public class MultiEncodingLogReader {
public static void main(String[] args) {
// 准备不同编码的测试文件
prepareLogFiles();
// 读取并显示各文件内容
readWithAutoDetection("d:/feixiang/logs/server_utf8.log");
readWithAutoDetection("d:/feixiang/logs/server_gbk.log");
readWithAutoDetection("d:/feixiang/logs/server_bom.log");
}
/**
* 准备测试日志文件:分别用 UTF-8 / GBK / UTF-8 BOM 编码
*/
public static void prepareLogFiles() {
new File("d:/feixiang/logs").mkdirs();
String logContent = "2024-03-15 10:30:45 [INFO] 飞翔科技服务器启动成功\n"
+ "2024-03-15 10:30:46 [INFO] 大翔登录了管理系统\n"
+ "2024-03-15 10:31:00 [WARN] 内存使用率达到80%\n";
try {
// UTF-8 无BOM
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("d:/feixiang/logs/server_utf8.log"), "UTF-8")) {
osw.write(logContent);
}
// GBK
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("d:/feixiang/logs/server_gbk.log"), "GBK")) {
osw.write(logContent);
}
// UTF-8 BOM (手动写入BOM头)
try (FileOutputStream fos = new FileOutputStream("d:/feixiang/logs/server_bom.log")) {
fos.write(0xEF); // BOM: EF BB BF
fos.write(0xBB);
fos.write(0xBF);
fos.write(logContent.getBytes("UTF-8"));
}
System.out.println("[白歌] 测试日志文件已准备好");
} catch (IOException e) {
System.err.println("[小崔] 准备文件失败: " + e.getMessage());
}
}
/**
* 自动检测编码并读取(简化版检测逻辑)
* 实际项目中可使用第三方库如 juniversalchardet
*/
public static void readWithAutoDetection(String filePath) {
System.out.println("\n========== " + new File(filePath).getName() + " ==========");
// 策略1: 先尝试UTF-8读取
if (tryReadWithEncoding(filePath, "UTF-8")) {
return;
}
// 策略2: UTF-8失败则尝试GBK
if (tryReadWithEncoding(filePath, "GBK")) {
return;
}
// 策略3: 最后尝试系统默认编码
tryReadWithEncoding(filePath, Charset.defaultCharset().name());
}
/**
* 尝试用指定编码读取文件
* @return true 表示读取成功且无乱码嫌疑
*/
private static boolean tryReadWithEncoding(String filePath, String charset) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(filePath), charset))) {
StringBuilder content = new StringBuilder();
String line;
int lineCount = 0;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
lineCount++;
}
// 简单乱码检测:包含常见中文字符且无替换字符
boolean likelyValid = !content.toString().contains("\uFFFD") // Unicode替换字符
&& content.length() > 0;
System.out.println("[白歌] 使用编码 " + charset + " 读取成功, "
+ lineCount + " 行, 疑似乱码: " + !likelyValid);
System.out.print(content.toString());
return true;
} catch (UnsupportedEncodingException e) {
System.out.println("[白歌] 编码 " + charset + " 不被支持");
return false;
} catch (IOException e) {
System.out.println("[小崔] 使用 " + charset + " 读取失败");
return false;
}
}
}
控制台输出:
[白歌] 测试日志文件已准备好
========== server_utf8.log ==========
[白歌] 使用编码 UTF-8 读取成功, 3 行, 疑似乱码: false
2024-03-15 10:30:45 [INFO] 飞翔科技服务器启动成功
2024-03-15 10:30:46 [INFO] 大翔登录了管理系统
2024-03-15 10:31:00 [WARN] 内存使用率达到80%
========== server_gbk.log ==========
[白歌] 使用编码 UTF-8 读取成功, 3 行, 疑似乱码: true
2024-03-15 10:30:45 [INFO] 缈婚飞绉戞妧链嶅姟鍣ㄥ惎锷ㄦ垚锷?
[白歌] 使用编码 GBK 读取成功, 3 行, 疑似乱码: false
2024-03-15 10:30:45 [INFO] 飞翔科技服务器启动成功
... (后续行省略)
六、易错场景
易错1:直接用 FileReader/FileWriter 读取非系统默认编码的文件
// ❌ 错误做法:FileReader 使用系统默认编码(中文Windows为GBK)
// 如果文件是UTF-8编码,中文会出现乱码
FileReader reader = new FileReader("utf8_encoded_file.txt");
BufferedReader br = new BufferedReader(reader);
String line = br.readLine(); // 如果文件是UTF-8,中文乱码!
// ✅ 正确做法:明确指定编码
BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("utf8_encoded_file.txt"), "UTF-8"));
原因:FileReader 内部使用 FileInputStream + 系统默认编码的 InputStreamReader,无法指定编码。JDK文档明确指出:FileReader 的构造方法不接受字符集参数,要指定编码必须使用 InputStreamReader。
易错2:忘记刷新 OutputStreamWriter 导致数据丢失
// ❌ 错误做法:写入后未flush或未close,数据滞留在缓冲区
OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream("data.txt"), "UTF-8");
writer.write("重要数据");
// 程序异常退出 → 缓冲区数据丢失!
// 此处没有 writer.close() 或 writer.flush()
// ✅ 正确做法:使用 try-with-resources 自动关闭
try (OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream("data.txt"), "UTF-8")) {
writer.write("重要数据");
writer.flush(); // 显式刷新(try-with-resources的close也会flush)
}
原因:OutputStreamWriter 内部有一个 StreamEncoder 缓冲区,字符需要经过编码转换后才能写入底层字节流。不及时flush会导致缓冲区数据在程序异常终止时丢失。
易错3:在循环中重复创建 InputStreamReader
// ❌ 错误做法:每次调用都创建新的转换流
public void readLines(String filePath) {
for (int i = 0; i < 100; i++) {
// 每次都打开/关闭文件,性能极差
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(filePath), "UTF-8"));
String line = reader.readLine();
reader.close();
}
}
// ✅ 正确做法:一次打开,批量处理
public void readLines(String filePath) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(filePath), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
processLine(line);
}
}
}
易错4:使用错误的字符集名称
// ❌ 错误:字符集名称区分大小写,且必须是JDK支持的标准名称
InputStreamReader isr = new InputStreamReader(fis, "utf-8"); // 标准名称可用
InputStreamReader isr2 = new InputStreamReader(fis, "UTF8"); // 无连字符也可
InputStreamReader isr3 = new InputStreamReader(fis, "Utf-8"); // 大小写不敏感一般也可
// 但以下会抛 UnsupportedEncodingException:
// new InputStreamReader(fis, "UTF-9") // 不存在的编码
// new InputStreamReader(fis, "GBK2312") // 应为 "GB2312"
// ✅ 最佳实践:使用 StandardCharsets (JDK 7+) 或 Charset.forName()
InputStreamReader isr = new InputStreamReader(fis,
java.nio.charset.StandardCharsets.UTF_8); // 编译期安全,无拼写错误风险
易错5:混淆 close() 的级联关闭行为
// ❌ 坑:关闭外层转换流会自动关闭底层字节流
InputStreamReader isr = new InputStreamReader(
new FileInputStream("a.txt"), "UTF-8");
// ... 读操作 ...
isr.close(); // 底层的 FileInputStream 也被关闭了!
// 如果之后还想用同一个 FileInputStream 读其他内容,已经不行了
// 如需独立关闭,需要自己管理:
// ✅ 方案:如果不希望级联关闭,需要在构造前独立持有引用
// 但通常推荐使用 try-with-resources 按嵌套顺序自动关闭
七、面试考点
Q1: FileReader 和 InputStreamReader 的区别是什么?
答:
FileReader是InputStreamReader的子类,专用于读取文件字符流,构造时只能传入文件路径或File对象,无法指定字符编码,始终使用系统默认编码。InputStreamReader是通用的字节→字符转换桥梁,可以包装任意InputStream(文件、网络、System.in等),可以显式指定字符编码。- 面试官追问:为什么JDK设计FileReader时不加编码参数?——历史原因。JDK 1.1引入Reader体系时的设计缺陷,后来JDK 11+才给FileReader补充了带Charset参数的构造方法。JDK 8中必须用InputStreamReader代替。
Q2: 如何正确读取一个未知编码的文本文件?
答:
- 如果文件有BOM头:UTF-8 BOM为
EF BB BF,UTF-16 LE BOM为FF FE,UTF-16 BE BOM为FE FF。可以先读取前几个字节判断BOM,然后用对应编码解码。 - 如果无BOM:使用字符编码检测库(如 Mozilla 的
juniversalchardet、ICU4J 的CharsetDetector)进行统计分析。 - 如果是自己掌控的文件:统一使用UTF-8编码,在文件头或元数据中记录编码信息。
- 终极方案:先用UTF-8尝试解码,检查是否出现
\uFFFD(Unicode替换字符);如果UTF-8失败再用GBK尝试;最后用ISO-8859-1兜底(该编码不会抛异常,任何字节都有对应字符)。
Q3: 解释 String.getBytes() 和 new String(byte[]) 与转换流的关系
答:
String.getBytes(charset)本质上是"字符→字节"的编码过程,与OutputStreamWriter编码路径一致。内部调用StringCoding.encode()。new String(byte[], charset)本质上是"字节→字符"的解码过程,与InputStreamReader解码路径一致。内部调用StringCoding.decode()。- 转换流中的
StreamEncoder/StreamDecoder底层最终也是调用这同一套StringCoding机制。 - 关键结论:
getBytes()+new String()是一次性的内存编解码,适用于小量数据;转换流是流式编解码,适用于大文件和网络传输。
Q4: System.out.println() 的中文为什么有时会乱码?
答:
System.out是一个PrintStream(字节输出流),内部持有一个OutputStreamWriter。- 当
System.out初始化时,使用的编码由file.encoding系统属性决定。 - 乱码场景:
- IDE控制台编码与JVM file.encoding 不一致:例如IDE控制台设置为UTF-8,但Windows下JVM默认
file.encoding=GBK,输出的GBK字节被IDE以UTF-8解码 → 乱码。 - 重定向到文件:
java MyApp > output.txt,如果程序输出中文但终端编码不对 → 文件乱码。 - Docker/CI环境:容器默认locale为POSIX,
file.encoding变为ANSI_X3.4-1968,导致中文无法编码。
- IDE控制台编码与JVM file.encoding 不一致:例如IDE控制台设置为UTF-8,但Windows下JVM默认
- 解决方案:
- 启动时添加JVM参数
-Dfile.encoding=UTF-8 - 或者不使用
System.out,改用PrintWriter包装OutputStreamWriter显式指定UTF-8:PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out, "UTF-8")); pw.println("中文输出"); pw.flush();
- 启动时添加JVM参数
Q5: GBK与UTF-8互转时有什么坑?
答:
- GBK字符集范围:GBK包含所有GB2312字符 + 扩展汉字,约2.1万个汉字。但Unicode(UTF-8)范围更大,某些生僻字GBK无法表示,编码时会变成
?。 - 不可逆转换:UTF-8 → GBK → UTF-8 不一定是无损的。如果源文本包含GBK不支持的字符(如emoji、某些繁体字变体),会丢失信息。
- Java中的表现:当用GBK编码一个不支持的字符时,
getBytes("GBK")会将其替换为0x3F(即?的字节码),数据不可逆丢失。 - 最佳实践:统一使用UTF-8,如需与遗留GBK系统交互,确保存储前验证字符集兼容性。
八、类层次结构速览
java.io.Reader (抽象类)
└── java.io.InputStreamReader
├── 构造: InputStreamReader(InputStream in)
├── 构造: InputStreamReader(InputStream in, String charsetName)
├── 构造: InputStreamReader(InputStream in, Charset cs)
├── read(): int // 读取单个字符
├── read(char[] cbuf, int off, int len): int
├── getEncoding(): String // 返回当前使用的编码名称
└── close(): void
└── java.io.FileReader (子类,无编码参数构造)
java.io.Writer (抽象类)
└── java.io.OutputStreamWriter
├── 构造: OutputStreamWriter(OutputStream out)
├── 构造: OutputStreamWriter(OutputStream out, String charsetName)
├── 构造: OutputStreamWriter(OutputStream out, Charset cs)
├── write(int c): void
├── write(char[] cbuf, int off, int len): void
├── write(String str, int off, int len): void
├── flush(): void
├── getEncoding(): String
└── close(): void
└── java.io.FileWriter (子类,无编码参数构造)
九、System.in / System.out 详解
9.1 System.in
// System.in 的类型
public static final InputStream in = null; // 在System类中声明
// 实际运行时由JVM初始化为 BufferedInputStream(包装了底层FileDescriptor.in)
// 典型用法:包装为字符流读取控制台
BufferedReader console = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8));
String input = console.readLine();
注意事项:
System.in是按行缓冲的,在输入回车之前数据不会传给程序。- 不要关闭
System.in,因为它是全局共享的,关闭后程序中其他读取操作将失败。 - IDE中运行程序时,
System.in可能重定向到了IDE的控制台面板。
9.2 System.out
// System.out 的类型
public static final PrintStream out = null;
// 实际运行时初始化为 PrintStream(包装了BufferedOutputStream → FileDescriptor.out)
// System.out.println() 内部使用的是系统默认编码
// 可以通过反射或System.setOut()来重定向,但一般不需要
// 如果需要指定编码的控制台输出:
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(System.out, StandardCharsets.UTF_8), true);
pw.println("中文输出 - UTF-8编码");
十、最佳实践总结
| 场景 | 推荐做法 | 理由 |
|---|---|---|
| 读取文本文件 | new BufferedReader(new InputStreamReader(new FileInputStream(f), "UTF-8")) | 明确编码,避免平台差异 |
| 写入文本文件 | new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f), "UTF-8")) | 同上 |
| 控制台输入 | new BufferedReader(new InputStreamReader(System.in, Charset.defaultCharset())) | 适配系统编码 |
| 编码常量 | 使用 StandardCharsets.UTF_8 而非字符串 "UTF-8" | 编译期安全,JDK 7+ |
| 资源管理 | try-with-resources | 自动关闭,防止资源泄漏 |
| 大文件处理 | 配合BufferedReader/BufferedWriter | 减少系统调用次数 |
白歌总结:"转换流的本质就是字节和字符之间的桥梁。记住一句话:字节流是搬运工,字符流是翻译官,转换流就是那个让搬运工和翻译官能沟通的中间人。搞懂了编码,IO流才算真正入门。"大翔点头:"对,下次面试问IO流的时候,把转换流讲清楚,面试官就知道你不是只会背API的程序员。"Frank笑道:"And always remember — when in doubt, use UTF-8!"