聊聊中文乱码的那些事儿:从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解码,那肯定就对不上号,乱码跑不掉。
朴素策略的问题暴露出来了:
- 编码不一致:写和读用的编码没统一,文件里存的字节和解析时的预期不匹配。
- 平台依赖:默认编码跟操作系统挂钩,换个环境就可能翻车。
- 字节处理粗糙:直接读写字节数组,没考虑字符边界,可能把多字节字符拆得七零八落。
从朴素到复杂:怎么解决乱码?
咱们一步步优化,逼近现在的主流方案。
第一步:指定编码,统一读写
最简单粗暴的办法,读写都显式指定编码,比如UTF-8:
fos.write(content.getBytes("UTF-8"));
String result = new String(buffer, 0, len, "UTF-8");
这样确实能解决问题,只要读写用的编码一致,乱码就没了。但这还不够稳,因为你得保证所有人、所有地方都记得写UTF-8,一不小心漏了又完蛋。
第二步:引入字符流,少碰字节
FileInputStream和FileOutputStream是字节流,操作的是原始字节,太底层了。咱们可以用字符流,比如FileReader和FileWriter,直接处理字符:
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,带编码控制
主流方案来了,咱们用InputStreamReader和OutputStreamWriter,包装字节流,指定编码,再加个缓冲层提高效率:
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操作,性能更好。
优化方向:跟主流接轨
从朴素的字节流到带编码的字符流,咱们发现问题根源是编码没管好。优化方向其实跟现在主流方案高度一致:
- 标准化编码:用UTF-8作为默认编码,兼容性强又省空间,跟Web和大部分现代系统对齐。
- 抽象封装:别直接操作字节流,用更高层的工具(像
Files类,Java 7引入的)简化代码,减少出错机会。比如:Files.writeString(Path.of("test.txt"), "hello 中文", StandardCharsets.UTF_8); String result = Files.readString(Path.of("test.txt"), StandardCharsets.UTF_8); - 异常处理:加点try-catch,防止文件操作翻车,跟生产环境保持一致。
- BOM检测:有些文件开头有BOM(字节顺序标记),得识别并跳过,跟现代文本处理工具看齐。
总结一下
中文乱码这事儿,说白了就是编码没对齐。通过最朴素的FileInputStream和FileOutputStream,咱们看到了问题:默认编码不靠谱,字节处理太随意。一步步改进,先统一编码,再换字符流,最后用缓冲流加显式编码,乱码没了,代码也更稳了。优化方向跟主流方案不谋而合:标准化、封装、健壮性,一个都不能少。以后写代码,记得编码这根弦儿绷紧点,乱码自然就跟你说拜拜啦!