乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

转换流 详解

场景:飞翔科技的新人小崔刚入职不久,就接到了一个棘手的任务——把财务部积累了三年的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)将字节序列按某种规则还原为字符序列的过程
CharsetJDK中表示字符集的类,常用别名如 "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: 如何正确读取一个未知编码的文本文件?

答:

  1. 如果文件有BOM头:UTF-8 BOM为 EF BB BF,UTF-16 LE BOM为 FF FE,UTF-16 BE BOM为 FE FF。可以先读取前几个字节判断BOM,然后用对应编码解码。
  2. 如果无BOM:使用字符编码检测库(如 Mozilla 的 juniversalchardet、ICU4J 的 CharsetDetector)进行统计分析。
  3. 如果是自己掌控的文件:统一使用UTF-8编码,在文件头或元数据中记录编码信息。
  4. 终极方案:先用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 系统属性决定。
  • 乱码场景:
    1. IDE控制台编码与JVM file.encoding 不一致:例如IDE控制台设置为UTF-8,但Windows下JVM默认file.encoding=GBK,输出的GBK字节被IDE以UTF-8解码 → 乱码。
    2. 重定向到文件:java MyApp > output.txt,如果程序输出中文但终端编码不对 → 文件乱码。
    3. Docker/CI环境:容器默认locale为POSIX,file.encoding 变为 ANSI_X3.4-1968,导致中文无法编码。
  • 解决方案:
    • 启动时添加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();
      

Q5: GBK与UTF-8互转时有什么坑?

答:

  1. GBK字符集范围:GBK包含所有GB2312字符 + 扩展汉字,约2.1万个汉字。但Unicode(UTF-8)范围更大,某些生僻字GBK无法表示,编码时会变成 ?。
  2. 不可逆转换:UTF-8 → GBK → UTF-8 不一定是无损的。如果源文本包含GBK不支持的字符(如emoji、某些繁体字变体),会丢失信息。
  3. Java中的表现:当用GBK编码一个不支持的字符时,getBytes("GBK") 会将其替换为 0x3F(即 ? 的字节码),数据不可逆丢失。
  4. 最佳实践:统一使用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!"

上一页
缓冲流
下一页
序列化 详解