聊聊中文乱码的那些事儿:从FileInputStream和FileOutputStream说起

205 阅读5分钟

聊聊中文乱码的那些事儿:从FileInputStream和FileOutputStream说起

先从字符编码的基础聊起

说到中文乱码,咱们得先搞清楚字符编码是个啥。简单来说,计算机只认识0和1,字母、数字、汉字这些人类能看懂的东西,都得通过某种规则转成二进制,这规则就是字符编码。常见的编码格式有那么几种,咱们慢慢捋一捋:

  • ASCII:老祖宗级别的编码,美国人搞出来的,主要用来表示英文字符、数字和一些符号。用7位(后来扩展到8位)就能搞定,128个字符足够用。但中文?抱歉,完全没戏。
  • GBK:这是咱们国家自己弄的编码,基于GB2312扩展来的,支持简体中文。GBK用1到2个字节表示一个字符,能覆盖几万个汉字和符号。不过,它只管中文,其他语言的字符基本不认。
  • Unicode:这家伙是个大一统的梦想,目标是把全世界所有字符都装进来。它给每个字符分配一个唯一的编号(叫码点),比如“中”的码点是U+4E2D。但Unicode只是个映射表,具体怎么存到计算机里,还得靠下面的实现。
  • UTF-8:Unicode的一种实现方式,特别聪明。用1到4个字节表示字符,英文就用1个字节(跟ASCII兼容),中文一般用3个字节。省空间又灵活,现在网上几乎都用它。
  • UTF-16:也是Unicode的实现,用2个或4个字节表示字符。好处是大部分常用字符固定2字节,处理起来快,但英文只占1字节的场景下就有点浪费空间了。

这些编码格式各有千秋,但要是用错了,或者没对齐,就会冒出乱码。比如你用GBK去读UTF-8编码的文件,中文就可能变成一堆“???”或者“鍒?”这种莫名其妙的东西。


用FileInputStream和FileOutputStream看乱码是怎么冒出来的

咱们直接上代码,用最朴素的方式读写文件,看看问题出在哪儿。假设有个文件test.txt,里面写了个“hello 中文”,保存的时候用的是UTF-8编码。

import java.io.*;

public class SimpleFileDemo {
    public static void main(String[] args) throws IOException {
        // 写文件
        FileOutputStream fos = new FileOutputStream("test.txt");
        String content = "hello 中文";
        fos.write(content.getBytes()); // 默认用平台编码
        fos.close();

        // 读文件
        FileInputStream fis = new FileInputStream("test.txt");
        byte[] buffer = new byte[1024];
        int len = fis.read(buffer);
        String result = new String(buffer, 0, len); // 默认用平台编码
        System.out.println(result);
        fis.close();
    }
}

跑一下代码,结果可能正常,也可能是一堆乱码。为什么呢?因为getBytes()new String()没指定编码,默认用的是你操作系统的编码。比如Windows上可能是GBK,Linux可能是UTF-8。如果写的时候用GBK把“中文”转成字节,读的时候却用UTF-8解码,那肯定就对不上号,乱码跑不掉。

朴素策略的问题暴露出来了

  1. 编码不一致:写和读用的编码没统一,文件里存的字节和解析时的预期不匹配。
  2. 平台依赖:默认编码跟操作系统挂钩,换个环境就可能翻车。
  3. 字节处理粗糙:直接读写字节数组,没考虑字符边界,可能把多字节字符拆得七零八落。

从朴素到复杂:怎么解决乱码?

咱们一步步优化,逼近现在的主流方案。

第一步:指定编码,统一读写

最简单粗暴的办法,读写都显式指定编码,比如UTF-8:

fos.write(content.getBytes("UTF-8"));
String result = new String(buffer, 0, len, "UTF-8");

这样确实能解决问题,只要读写用的编码一致,乱码就没了。但这还不够稳,因为你得保证所有人、所有地方都记得写UTF-8,一不小心漏了又完蛋。

第二步:引入字符流,少碰字节

FileInputStreamFileOutputStream是字节流,操作的是原始字节,太底层了。咱们可以用字符流,比如FileReaderFileWriter,直接处理字符:

FileWriter fw = new FileWriter("test.txt");
fw.write("hello 中文");
fw.close();

FileReader fr = new FileReader("test.txt");
char[] buffer = new char[1024];
int len = fr.read(buffer);
String result = new String(buffer, 0, len);
System.out.println(result);
fr.close();

字符流的好处是把字节转字符的工作交给它自己处理,但它也有个坑:默认还是用平台编码。要彻底解决问题,得用带编码参数的版本,或者直接上更高级的工具。

第三步:用BufferedReader和PrintWriter,带编码控制

主流方案来了,咱们用InputStreamReaderOutputStreamWriter,包装字节流,指定编码,再加个缓冲层提高效率:

import java.io.*;

public class BetterFileDemo {
    public static void main(String[] args) throws IOException {
        // 写文件
        FileOutputStream fos = new FileOutputStream("test.txt");
        OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
        BufferedWriter bw = new BufferedWriter(osw);
        bw.write("hello 中文");
        bw.close();

        // 读文件
        FileInputStream fis = new FileInputStream("test.txt");
        InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
        BufferedReader br = new BufferedReader(isr);
        String line = br.readLine();
        System.out.println(line);
        br.close();
    }
}

这方案靠谱多了:

  • 编码显式控制:读写都强制用UTF-8,彻底告别平台依赖。
  • 字符边界安全InputStreamReader会正确处理多字节字符,不会被截断。
  • 效率提升:缓冲流减少直接IO操作,性能更好。

优化方向:跟主流接轨

从朴素的字节流到带编码的字符流,咱们发现问题根源是编码没管好。优化方向其实跟现在主流方案高度一致:

  1. 标准化编码:用UTF-8作为默认编码,兼容性强又省空间,跟Web和大部分现代系统对齐。
  2. 抽象封装:别直接操作字节流,用更高层的工具(像Files类,Java 7引入的)简化代码,减少出错机会。比如:
    Files.writeString(Path.of("test.txt"), "hello 中文", StandardCharsets.UTF_8);
    String result = Files.readString(Path.of("test.txt"), StandardCharsets.UTF_8);
    
  3. 异常处理:加点try-catch,防止文件操作翻车,跟生产环境保持一致。
  4. BOM检测:有些文件开头有BOM(字节顺序标记),得识别并跳过,跟现代文本处理工具看齐。

总结一下

中文乱码这事儿,说白了就是编码没对齐。通过最朴素的FileInputStreamFileOutputStream,咱们看到了问题:默认编码不靠谱,字节处理太随意。一步步改进,先统一编码,再换字符流,最后用缓冲流加显式编码,乱码没了,代码也更稳了。优化方向跟主流方案不谋而合:标准化、封装、健壮性,一个都不能少。以后写代码,记得编码这根弦儿绷紧点,乱码自然就跟你说拜拜啦!