本文纲要
- 为什么需要字符流?字节流处理文本的乱码问题
- 字符编码与解码
2.1 编码表概览
2.2String的编码与解码方法 - 字节流读取中文乱码的原因
- 字符流原理
- 字符输出流
FileWriter
5.1 创建对象与写出数据
5.2 写出数据的注意事项
5.3flush与close方法 - 字符输入流
FileReader - 练习:保存键盘录入数据
- 字符缓冲流
8.1 字符缓冲输入流BufferedReader
8.2 字符缓冲输出流BufferedWriter
8.3 特有方法newLine与readLine - 练习:读取文件数据排序后写回
IO流小结
为什么需要字符流?字节流处理文本的乱码问题
字节流可以操作所有类型的文件,但直接用字节流读取文本文件中的中文时,很可能会出现乱码。同理,用字节流将中文写入文本文件也可能导致乱码。
下面是一个简单的字节流读取文本文件的例子,文件 a.txt 中包含英文和中文:
项目结构:
charstream/
src/
com/wb/charstream1/
CharStreamDemo1.java
a.txt
package com.wb.charstream1;
import java.io.FileInputStream;
import java.io.IOException;
public class CharStreamDemo1 {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("charstream\\a.txt");
int b;
while ((b = fis.read()) != -1) {
System.out.println((char) b);
}
fis.close();
}
}
运行结果:英文字母 ABC 可以正常显示,但后面的中文却变成了乱码。为什么会这样?根本原因在于编码方式与解码方式不匹配。
字符编码与解码
1 ) 编码表概览
计算机中所有信息都以二进制形式存储。当字符存入计算机或从计算机读出时,会涉及两个概念:
- 编码:按照某种规则将字符转换成二进制存入计算机。
- 解码:按照相同的规则将二进制数据解析成字符展示出来。
如果编码和解码使用的码表(字符集)不一致,就会出现乱码。
常见的码表有:
| 码表(字符集) | 说明 |
|---|---|
| ASCII | 美国信息交换标准码,包含数字、大小写字母及常见英文标点,不含中文。 |
| GBK | 中国标准,兼容ASCII,包含21003个汉字及部分日韩文字,一个中文占2个字节,是Windows系统默认码表。 |
| Unicode | 国际标准万国码,涵盖世界大多数文字,通常搭配 UTF‑8 等编码格式,一个中文占3个字节。 |
重点记忆:
- Windows默认码表:GBK,一个中文2字节。
- IDEA及企业开发中通常默认使用:Unicode 的 UTF‑8 编码格式,一个中文3字节。
编码和解码的流程可以用下图表示:
中文字符在Unicode中的处理过程:
2 ) String的编码与解码方法
在Java的 String 类中,提供了编码和解码的方法:
| 方法 | 说明 |
|---|---|
byte[] getBytes() | 使用平台默认字符集将字符串编码为字节数组 |
byte[] getBytes(String charsetName) | 使用指定字符集编码 |
new String(byte[] bytes) | 使用平台默认字符集解码字节数组为字符串 |
new String(byte[] bytes, String charsetName) | 使用指定字符集解码 |
编码示例:
// 编码方法演示
String s = "黑马程序员";
// 利用Idea默认的UTF-8将中文编码为一系列的字节
byte[] bytes1 = s.getBytes();
System.out.println(Arrays.toString(bytes1));
// 输出:[23, -69, -111, ...] 共15个字节,一个中文3字节
// 指定GBK编码
byte[] bytes2 = s.getBytes("GBK");
System.out.println(Arrays.toString(bytes2));
// 输出:[-70, -38, -62, -19, ...] 共10个字节,一个中文2字节
解码示例:
// UTF-8字节数据
byte[] bytes1 = {-23, -69, -111, -23, -87, -84, -25, -88, -117, -27, -70, -113, -27, -111, -104};
// GBK字节数据
byte[] bytes2 = {-70, -38, -62, -19, -77, -52, -48, -14, -44, -79};
// 利用默认的UTF-8进行解码
String s1 = new String(bytes1);
System.out.println(s1); // 黑马程序员
// 利用指定的GBK进行解码
String s2 = new String(bytes2, "gbk");
System.out.println(s2); // 黑马程序员
字节流读取中文乱码的原因
字节流一次只能读取一个字节,而一个中文在 GBK 中占2个字节,在 UTF‑8 中占3个字节。字节流每次只读到中文的一部分,无法正确组装,因此出现乱码。
以 UTF‑8 为例:文件内容为 a黑马,对应的字节如下:
字节流读取过程:
每次只读到三分之一或二分之一的中文,查码表自然找不到对应的字符,于是出现乱码。
字符流原理
字符流的底层其实是 字节流 + 编码表。无论哪种码表,中文字符的第一个字节一定是负数。字符流在读取时仍是逐字节读取,但当它遇到一个负数时,就知道读到了一个中文的起始字节,随后会根据当前使用的编码格式(如 UTF‑8)一次性读取剩余的字节,组成一个完整的中文再转换为字符。
使用结论:
- 文件拷贝一律使用字节流或字节缓冲流。
- 将文本文件数据读取到内存中,使用字符输入流。
- 将内存数据写入文本文件,使用字符输出流。
- Window默认码表 GBK,一个中文2字节;IDEA默认 UTF‑8,一个中文3字节
字符输出流 FileWriter
1 ) 创建对象与写出数据
字符输出流 FileWriter 的写入步骤与字节流类似:
- 创建
FileWriter对象 - 写出数据
- 释放资源
构造方法可以接收 File 对象或字符串路径。写出数据提供了5个常用方法:
| 方法 | 说明 |
|---|---|
write(int c) | 写出一个字符(传入字符对应的整数) |
write(char[] cbuf) | 写出一个字符数组 |
write(char[] cbuf, int off, int len) | 写出字符数组的一部分 |
write(String str) | 写一个字符串(最常用) |
write(String str, int off, int len) | 写一个字符串的一部分 |
完整示例:
package com.wb.charstream1;
import java.io.FileWriter;
import java.io.IOException;
public class CharStreamDemo3 {
public static void main(String[] args) throws IOException {
// 创建字符输出流的对象
FileWriter fw = new FileWriter("charstream\\a.txt");
// 1. 写一个字符 —— write(int c)
fw.write(97); // 字符 'a'
fw.write(98); // 字符 'b'
fw.write(99); // 字符 'c'
// 2. 写出一个字符数组 —— write(char[] cbuf)
char[] chars = {97, 98, 99, 100, 101}; // a,b,c,d,e
fw.write(chars);
// 3. 写出字符数组的一部分 —— write(char[] cbuf, int off, int len)
fw.write(chars, 0, 3); // 写出 a,b,c
// 4. 写一个字符串 —— write(String str)
String line = "黑马程序员abc";
fw.write(line);
// 5. 写一个字符串的一部分 —— write(String str, int off, int len)
fw.write(line, 0, 2); // 写出 "黑马"
// 释放资源
fw.close();
}
}
2 ) 写出数据的注意事项
| 注意事项 | 说明 |
|---|---|
| 文件不存在 | 会自动创建,但父级路径必须存在,否则会抛出异常 |
| 文件已存在 | 会清空原文件内容 |
write(int) | 传入整数时,实际写入的是该整数在码表中对应的字符,而非数字本身 |
write(String) | 原样写出字符串内容 |
示例:
FileWriter fw = new FileWriter("charstream\\a.txt");
// write(97) 写出的是字符 'a',而不是数字 97
fw.write(97);
// 如果想真正写出数字 97,应该写字符串
fw.write("97");
fw.close();
3 ) flush 与 close 方法
| 方法 | 说明 |
|---|---|
flush() | 刷新流,将缓冲区数据强制写出到文件,之后仍可继续写数据 |
close() | 关闭流,先刷新缓冲区,然后释放资源,关闭后不能再用 |
对比测试:
FileWriter fw = new FileWriter("charstream\\a.txt");
fw.write("黑马程序员");
fw.flush(); // 刷新,数据写入文件,但流未关闭
fw.write("666");
fw.flush(); // 仍然可以继续写数据
fw.close(); // 关闭流
// fw.write("aaa"); // ❌ 报错:Stream closed
如果只写数据不调用 flush 或 close,数据可能停留在内存缓冲区中,文件内容为空。
字符输入流 FileReader
字符输入流 FileReader 用于从文本文件中读取字符到内存。其读取方式与字节流类似,但操作的是字符。
| 方法 | 说明 |
|---|---|
read() | 一次读取一个字符,返回字符的整数形式,末尾返回 -1 |
read(char[] cbuf) | 一次读取多个字符,存入字符数组,返回实际读取的字符个数 |
一次读取一个字符:
FileReader fr = new FileReader("charstream\\a.txt");
int ch;
while ((ch = fr.read()) != -1) {
System.out.println((char) ch);
}
fr.close();
一次读取多个字符(批量读取):
FileReader fr = new FileReader("charstream\\a.txt");
char[] chars = new char[1024];
int len;
while ((len = fr.read(chars)) != -1) {
System.out.println(new String(chars, 0, len));
}
fr.close();
这里 read(chars) 会将读取到的字符填充到数组中,返回值 len 是本次实际读取到的字符个数。
练习:保存键盘录入数据
需求:将用户键盘录入的用户名和密码保存到本地文件,用户名独占一行,密码独占一行,实现永久化存储。
分析步骤:
- 使用
Scanner录入用户名和密码。 - 使用
FileWriter分别将它们写出到文件,并在用户名后写出换行符。
package com.wb.charstream1;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Scanner;
public class CharStreamDemo8 {
public static void main(String[] args) throws IOException {
// 1. 键盘录入用户名和密码
Scanner sc = new Scanner(System.in);
System.out.println("请录入用户名");
String username = sc.next();
System.out.println("请录入密码");
String password = sc.next();
// 2. 写到本地文件
FileWriter fw = new FileWriter("charstream\\a.txt");
fw.write(username);
// 写出回车换行符,不同系统换行符不同:
// Windows: \r\n MacOS: \r Linux: \n
fw.write("\r\n");
fw.write(password);
fw.flush();
fw.close();
}
}
字符缓冲流
与字节缓冲流类似,字符流也提供了缓冲流来提高读写效率:
| 类 | 说明 |
|---|---|
BufferedReader | 字符缓冲输入流,提供高效读取 |
BufferedWriter | 字符缓冲输出流,提供高效写入 |
它们的构造方法不再直接接收文件路径,而是接收对应的字符流对象 (FileReader / FileWriter)。
底层内置了一个默认大小为 8192 的缓冲区。
流继承关系:
1 ) 字符缓冲输入流 BufferedReader
// 创建对象
BufferedReader br = new BufferedReader(new FileReader("charstream\\a.txt"));
// 一次读取多个字符
char[] chars = new char[1024];
int len;
while ((len = br.read(chars)) != -1) {
System.out.println(new String(chars, 0, len));
}
br.close();
2 ) 字符缓冲输出流 BufferedWriter
写出数据的几个方法与 FileWriter 相同,同样支持写字符、字符数组、字符串等。
BufferedWriter bw = new BufferedWriter(new FileWriter("charstream\\a.txt"));
// 写出一个字符(97对应 'a')
bw.write(97);
bw.write("\r\n");
// 写出字符数组
char[] chars = {97, 98, 99, 100, 101};
bw.write(chars);
bw.write("\r\n");
// 写出字符数组的一部分(前3个:a,b,c)
bw.write(chars, 0, 3);
bw.write("\r\n");
// 写出字符串
bw.write("黑马程序员");
bw.write("\r\n");
// 写出字符串的一部分
String line = "abcdefg";
bw.write(line, 0, 5); // abcde
bw.flush();
bw.close();
3 ) 特有方法 newLine 与 readLine
这两个方法是缓冲流特有的,非常实用:
| 类 | 方法 | 说明 |
|---|---|---|
BufferedWriter | newLine() | 写一个跨平台的换行符(Windows: \r\n,Linux: \n,Mac: \r) |
BufferedReader | readLine() | 一次读取一整行文本,读到回车换行为止(不包含换行符),读到末尾返回 null |
newLine 示例:
BufferedWriter bw = new BufferedWriter(new FileWriter("charstream\\a.txt"));
bw.write("黑马程序员666");
bw.newLine(); // 跨平台换行
bw.write("abcdef");
bw.newLine();
bw.write("-------------");
bw.flush();
bw.close();
readLine 示例:
// 创建对象
BufferedReader br = new BufferedReader(new FileReader("charstream\\a.txt"));
// 一行行读取
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
注意:readLine() 读到末尾时返回 null,而不是 -1。
readLine() 不会将回车换行符读入字符串中。
练习:读取文件数据排序后写回
需求:从文件 sort.txt 中读取一行由空格分隔的数字,将其排序后再写回原文件(覆盖)。
文件内容示例:
9 1 2 5 3 10 4 6 7 8
分析步骤:
- 用
BufferedReader读取整行字符串。 - 按空格切割得到
String数组。 - 将
String数组转换为int数组。 - 调用
Arrays.sort()排序。 - 用
BufferedWriter将排序后的数字写回文件(每个数字后跟一个空格)。
实现代码:
package com.wb.charstream1;
import java.io.*;
import java.util.Arrays;
public class CharStreamDemo14 {
public static void main(String[] args) throws IOException {
// 1. 读取文件中的数据
BufferedReader br = new BufferedReader(new FileReader("charstream\\sort.txt"));
// 注意:输出流不能放在这里创建,否则会清空文件导致读不到内容
String line = br.readLine();
System.out.println("读取到的数据为:" + line);
br.close();
// 2. 按空格切割
String[] split = line.split(" "); // 得到 {"9","1","2",...}
// 3. 转换为int数组
int[] arr = new int[split.length];
for (int i = 0; i < split.length; i++) {
arr[i] = Integer.parseInt(split[i]);
}
// 4. 排序
Arrays.sort(arr);
System.out.println("排序后:" + Arrays.toString(arr));
// 5. 写回本地文件
BufferedWriter bw = new BufferedWriter(new FileWriter("charstream\\sort.txt"));
for (int i = 0; i < arr.length; i++) {
bw.write(arr[i] + " ");
bw.flush();
}
bw.close();
}
}
易错点:如果在读取文件之前就创建了输出流(new FileWriter(...)),该文件会被立即清空,导致后面的读取操作得到的是空内容。因此输出流一定要在读取完成后再创建。
IO流小结
目前所学的 IO 流可以根据用途归纳为两大类:
| 类别 | 流 | 适用场景 | 备注 |
|---|---|---|---|
| 字节流 | FileInputStream / FileOutputStream | 文件拷贝 | 可操作所有文件 |
| 字节缓冲流 | BufferedInputStream / BufferedOutputStream | 提高拷贝效率 | 内置8192缓冲区 |
| 字符流 | FileReader / FileWriter | 读写文本文件 | 避免中文乱码 |
| 字符缓冲流 | BufferedReader / BufferedWriter | 高效读写文本 | 提供 readLine() 和 newLine() |
编码表速记:Windows 默认 GBK(中文2字节);IDEA 及项目开发常用 UTF‑8(中文3字节)。
总结
通过字符流的学习,我们掌握了如何正确、高效地处理文本文件中的中文字符,也理解了字节流与字符流各自最适合的应用场景。后续学习其他 IO 流时,这些原理和方法同样适用。

1万+

被折叠的 条评论
为什么被折叠?



