面试题:说一下字符流
作为一个 Java 开发者,字符流这个话题,说实话,不止一次让我感慨“你要说它简单吧,是挺简单的,但你真要用好了,还真得绕几圈。”特别是你在处理中文的时候,一不留神就来个“乱码翻车”,像极了在会议上被老板灵魂拷问“这乱码是什么?乱码是你写的代码吗?”
我就从最常见的开发场景开始聊起,看看为啥 Java 会搞出“字节流”和“字符流”这两个流派。
都是流,怎么还分个“字节”和“字符”?
咱们先把话说清楚:不管你是读文件还是读网络传输的数据,最底层其实都是“字节”,也就是 byte
,这是 Java 世界里的“搬砖单位”。所以 Java 的 InputStream
和 OutputStream
就是字节流,它们操作的就是 byte。
那为啥还要搞一个字符流呢?很简单,因为我们人类用的是“字符”——中文、英文、符号、表情包……统统是字符。计算机读这些东西的时候,不光要知道它的“内容”,还要知道它的“语言”(也就是编码格式),否则就像你拿着西班牙语的歌词去唱京剧,字虽然在,但调子全错了。
Java 是站在开发者角度考虑的,反正你要处理的是字符,我就帮你把字节和字符之间的转换封装一下,你直接用字符流不香吗?
乱码的真相,都是编码惹的祸
说到字符流,必须提一句大家最头疼的事:乱码。
这个东西就像早上赶地铁没抢到座,虽然不至于致命,但心情真的不太好。
为什么会乱码?说白了,就是你用错误的方式解读了字节。比如,你以为某一串字节是 UTF-8 编码的中文,结果它其实是 GBK,那你读出来的自然是一堆“问号”、“火星文”或者莫名其妙的符号。
来看个例子:
FileInputStream inputStream = new FileInputStream("input.txt");
int content;
while ((content = inputStream.read()) != -1) {
System.out.print((char) content);
}
假如 input.txt
里写的是中文,这段代码就大概率翻车。为啥?因为 FileInputStream 是字节流,不懂编码,你直接把 byte 强转成 char,输出就容易出错。
这时候该谁上场?当然是字符流。
InputStreamReader:翻译官上线
Java 提供了 InputStreamReader
,它是个桥梁,连接字节流和字符流。
你可以把它理解成一个自动带字幕的翻译器,负责把字节(byte)转成字符(char),关键是,它还支持你告诉它“字幕语言”。
比如:
InputStreamReader reader = new InputStreamReader(
new FileInputStream("input.txt"), "UTF-8"
);
你一说“UTF-8”,它就乖乖按 UTF-8 来解读字节,避免乱码。
而如果你用 FileReader
呢?它内部其实就是 InputStreamReader
,不过默认用系统编码(比如 Windows 上是 GBK,Mac 上可能是 UTF-8),所以用起来虽然简单,但遇到跨平台就容易出事。
最稳妥的方案就是直接用 InputStreamReader
,自己指定编码,不给乱码留一点机会。
Reader 的日常操作,不止读那么简单
说起字符流读文件,Reader
是整个家族的老大,它的子类有很多,常用的就是 FileReader
和 BufferedReader
。
我们来一个简单例子:
try (FileReader reader = new FileReader("input.txt")) {
int ch;
while ((ch = reader.read()) != -1) {
System.out.print((char) ch);
}
} catch (IOException e) {
e.printStackTrace();
}
这个代码能运行,但不一定完美。比如里面的 read()
是一个一个字符地读,对于长文件来说,效率就不行了。
所以推荐更高级一点的玩法,比如用字符数组缓存一下:
char[] buffer = new char[1024];
int len;
while ((len = reader.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, len));
}
更懒一点的开发者会直接用 BufferedReader
,它内置缓冲区,还能按行读:
BufferedReader br = new BufferedReader(new FileReader("input.txt"));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
是不是比 read()
那种逐字输出的方式舒服多了?
Writer 家族:输出字符的主力军
说完读,得说写。你想把“你好,世界”写进文件,总不能一个 byte 一个 byte 地拼吧?那多像小学抄课文。
这时候,Writer
就来了,它跟 Reader
是一对好兄弟,一个负责读,一个负责写。
我们用最简单的 FileWriter
:
try (Writer writer = new FileWriter("output.txt")) {
writer.write("你好,世界!");
} catch (IOException e) {
e.printStackTrace();
}
注意了,FileWriter
默认也是用系统编码,如果你想精确控制编码,还是用“桥梁式”写法更稳:
OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream("output.txt"), "UTF-8"
);
writer.write("你好,世界!");
writer.close();
这样不管你在哪个操作系统写,读出来都不会有乱码。
flush() 和 close(),别让内容卡在路上
这里得提一个点,很多初学者会忘了 flush()
和 close()
。
Java 的 Writer 是有缓冲区的,也就是说你写进去了字符,它可能先“存在兜里”,没马上写进文件。
这时候,如果你没手动 flush()
或 close()
,有些内容可能就永远“卡在半路”。
我的建议是:尽量用 try-with-resources,也就是上面例子里的写法,它会自动调用 close()
,写完就关门走人,不留隐患。
最后聊聊选择题:字节流 VS 字符流
很多人一开始懵:这两个到底啥时候该用哪个?
一句话定乾坤:
-
你处理的是“文字”:用字符流
Reader
/Writer
。 -
你处理的是“图片、音频、视频、压缩包”:用字节流
InputStream
/OutputStream
。
举个栗子,如果你读的是一张 .jpg
图片,别傻傻用字符流,不然不仅乱码,可能直接文件都读挂了。
总结一下
字符流和字节流,一个更底层,一个更贴近“人”。Java 把它们做了层次上的区分,其实是为了开发者更方便地处理不同的数据类型。
-
如果你看到“乱码”,十有八九是编码没处理好;
-
如果你看到“输出为空”,有可能是你没
flush()
; -
如果你在纠结“用哪个流”,看你处理的是人能读的内容,还是机器能懂的二进制。
技术这东西,说简单也简单,说深也深。关键是,别死记硬背,得知道背后的道理和使用场景。