Java基础快速入门: 字符流操作详解

本文纲要

  1. 为什么需要字符流?字节流处理文本的乱码问题
  2. 字符编码与解码
    2.1 编码表概览
    2.2 String的编码与解码方法
  3. 字节流读取中文乱码的原因
  4. 字符流原理
  5. 字符输出流 FileWriter
    5.1 创建对象与写出数据
    5.2 写出数据的注意事项
    5.3 flushclose方法
  6. 字符输入流 FileReader
  7. 练习:保存键盘录入数据
  8. 字符缓冲流
    8.1 字符缓冲输入流 BufferedReader
    8.2 字符缓冲输出流 BufferedWriter
    8.3 特有方法 newLinereadLine
  9. 练习:读取文件数据排序后写回
  10. 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字节。

编码和解码的流程可以用下图表示:

编码

解码

字符(如:'A')

查码表得数字(97)

转二进制存储

文件中二进制

二进制转数字(97)

查码表得字符('A')

中文字符在Unicode中的处理过程:

编码

解码

中文字符(如:'黑')

查Unicode码表得数字

通过UTF‑8编码转为二进制

存储到文件

二进制通过UTF‑8解码

查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黑马,对应的字节如下:

a (97)

黑字节1 (-23)

黑字节2 (-69)

黑字节3 (-111)

马字节1 (-23)

马字节2 (-87)

马字节3 (-84)

字节流读取过程:

读第1个字节:97

转成字符 'a' ✅

读第2个字节:-23

中文字节的一部分 ❌

读第3个字节:-69

中文字节的一部分 ❌

读第4个字节:-111

中文字节的一部分 ❌

每次只读到三分之一或二分之一的中文,查码表自然找不到对应的字符,于是出现乱码。

字符流原理

字符流的底层其实是 字节流 + 编码表。无论哪种码表,中文字符的第一个字节一定是负数。字符流在读取时仍是逐字节读取,但当它遇到一个负数时,就知道读到了一个中文的起始字节,随后会根据当前使用的编码格式(如 UTF‑8)一次性读取剩余的字节,组成一个完整的中文再转换为字符。

字节流逐个字节读

字节 < 0?

转为英文字符

根据编码格式一次性读完整个中文的字节

将完整字节转为中文

使用结论:

  • 文件拷贝一律使用字节流或字节缓冲流。
  • 将文本文件数据读取到内存中,使用字符输入流。
  • 将内存数据写入文本文件,使用字符输出流。
  • Window默认码表 GBK,一个中文2字节;IDEA默认 UTF‑8,一个中文3字节

字符输出流 FileWriter

1 ) 创建对象与写出数据

字符输出流 FileWriter 的写入步骤与字节流类似:

  1. 创建 FileWriter 对象
  2. 写出数据
  3. 释放资源

构造方法可以接收 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 ) flushclose 方法

方法说明
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 

如果只写数据不调用 flushclose,数据可能停留在内存缓冲区中,文件内容为空。

字符输入流 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 是本次实际读取到的字符个数。

练习:保存键盘录入数据

需求:将用户键盘录入的用户名和密码保存到本地文件,用户名独占一行,密码独占一行,实现永久化存储。

分析步骤:

  1. 使用 Scanner 录入用户名和密码。
  2. 使用 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 的缓冲区。

流继承关系:

包装

包装

«abstract»

Reader

«abstract»

Writer

InputStreamReader

BufferedReader

FileReader

OutputStreamWriter

BufferedWriter

FileWriter

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 ) 特有方法 newLinereadLine

这两个方法是缓冲流特有的,非常实用:

方法说明
BufferedWriternewLine()写一个跨平台的换行符(Windows: \r\n,Linux: \n,Mac: \r)
BufferedReaderreadLine()一次读取一整行文本,读到回车换行为止(不包含换行符),读到末尾返回 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

分析步骤:

  1. BufferedReader 读取整行字符串。
  2. 按空格切割得到 String 数组。
  3. String 数组转换为 int 数组。
  4. 调用 Arrays.sort() 排序。
  5. 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 流可以根据用途归纳为两大类:

IO流

字节流 用于文件拷贝

字符流 用于文本文件读写

FileInputStream

FileOutputStream

BufferedInputStream

BufferedOutputStream

FileReader

FileWriter

BufferedReader

BufferedWriter

特有方法: readLine()

特有方法: newLine()

类别适用场景备注
字节流FileInputStream / FileOutputStream文件拷贝可操作所有文件
字节缓冲流BufferedInputStream / BufferedOutputStream提高拷贝效率内置8192缓冲区
字符流FileReader / FileWriter读写文本文件避免中文乱码
字符缓冲流BufferedReader / BufferedWriter高效读写文本提供 readLine()newLine()

编码表速记:Windows 默认 GBK(中文2字节);IDEA 及项目开发常用 UTF‑8(中文3字节)。

总结

通过字符流的学习,我们掌握了如何正确、高效地处理文本文件中的中文字符,也理解了字节流与字符流各自最适合的应用场景。后续学习其他 IO 流时,这些原理和方法同样适用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值