网站公告

本次更新:没啥更新的

“钱塘江上潮信起,今日方知我是我”

Skip to content

字符流 ​

字数: 0 字 时长: 0 分钟

字符流 Reader 和 Writer 的故事要从它们的类关系图开始,来看图。

image-20250321211718727

字符流是一种用于读取和写入字符数据的输入输出流。与字节流不同,字符流以字符为单位读取和写入数据,而不是以字节为单位,常用来处理文本信息。

如果用字节流直接读取中文,可能会遇到乱码问题,见下例:

java
// FileInputStream 为操作文件的字符输入流
FileInputStream inputStream =
        new FileInputStream("a.txt");

// 内容为“俄乌战争大概能打几个月?俄罗斯会不会打核武器?”
int len;

while ((len = inputStream.read()) != -1) {
    System.out.print((char) len);
}

来看运行结果:

text
ä¿•ä¹•æ••äº•å¤§æ¦•è•½æ••å• ä¸ªæ••ï¼•ä¿•ç½•æ•¯ä¼•ä¸•ä¼•æ••æ ¸æ•¦å•¨ï¼•

之所以出现乱码,是因为在字节流中,一个字符通常由多个字节组成,而不同的字符编码使用的字节数不同。

如果我们使用了错误的字符编码,或者在读取和写入数据时没有正确处理字符编码的转换,就会导致读取出来的中文字符出现乱码。

例如,当我们使用默认的字符编码读取一个包含中文字符的文本文件时,就会出现乱码。因为默认的字符编码通常是 ASCII 编码,它只能表示英文字符,而不能正确地解析中文字符。

那使用字节流该如何正确地读出中文呢?见下例:

java
try (
        FileInputStream inputStream =
                new FileInputStream("a.txt")
) {

    byte[] bytes =
            new byte[1024];

    int len;

    while (
            (len = inputStream.read(bytes)) != -1
    ) {

        System.out.print(
                new String(
                        bytes,
                        0,
                        len
                )
        );
    }
}

为什么这种方式就可以呢?

因为我们拿 String 类进行了解码,查看:

java
new String(
        byte bytes[],
        int offset,
        int length
)

的源码就可以发现,该构造方法有解码功能:

java
public String(
        byte bytes[],
        int offset,
        int length
) {

    checkBounds(
            bytes,
            offset,
            length
    );

    this.value =
            StringCoding.decode(
                    bytes,
                    offset,
                    length
            );
}

继续追看 StringCoding.decode() 方法调用的 defaultCharset() 方法,会发现默认编码是 UTF-8,代码如下:

java
public static Charset defaultCharset() {

    if (defaultCharset == null) {

        synchronized (Charset.class) {

            if (cs != null) {
                defaultCharset = cs;
            } else {
                defaultCharset =
                        forName("UTF-8");
            }
        }
    }

    return defaultCharset;
}

static char[] decode(
        byte[] ba,
        int off,
        int len
) {

    String csn =
            Charset.defaultCharset().name();

    try {

        // use charset name decode() variant which provides caching.
        return decode(
                csn,
                ba,
                off,
                len
        );

    } catch (
            UnsupportedEncodingException x
    ) {

        warnUnsupportedCharset(csn);
    }
}

在 Java 中,常用的字符编码有:

text
ASCII
ISO-8859-1
UTF-8
UTF-16

其中,ASCII 和 ISO-8859-1 只能表示部分字符,而 UTF-8 和 UTF-16 可以表示所有的 Unicode 字符,包括中文字符。

当我们使用:

java
new String(
        byte bytes[],
        int offset,
        int length
)

将字节流转换为字符串时,Java 会根据 UTF-8 的规则将每 3 个字节解码为一个中文字符,从而正确地解码出中文。

尽管字节流也有办法解决乱码问题,但不够直接,于是就有了字符流,专门用于处理文本文件,音频、图片、视频等属于非文本文件。

从另一个角度来说:

字符流 = 字节流 + 编码表

1. 字符输入流(Reader) ​

java.io.Reader 是字符输入流的超类(父类),它定义了字符输入流的一些共性方法:

  1. close():关闭此流,并释放与此流相关的系统资源。
  2. read():从输入流读取一个字符。
  3. read(char[] cbuf):从输入流中读取一些字符,并将它们存储到字符数组 cbuf 中。

FileReader 是 Reader 的子类,用于从文件中读取字符数据。

它的主要特点如下:

  • 可以通过构造方法指定要读取的文件路径。
  • 每次可以读取一个或多个字符。
  • 可以读取 Unicode 字符集中的字符,通过指定字符编码来实现字符集的转换。

1.1 FileReader 构造方法 ​

  1. FileReader(File file):创建一个新的 FileReader,参数为 File 对象。
  2. FileReader(String fileName):创建一个新的 FileReader,参数为文件名。

代码示例如下:

java
// 使用 File 对象创建流对象
File file =
        new File("a.txt");

FileReader fr =
        new FileReader(file);

1.2 FileReader 读取字符数据 ​

1.2.1 读取字符 ​

read() 方法每次可以读取一个字符,返回读取的字符(转换为 int 类型)。

当读取到文件末尾时,返回:

text
-1

代码示例如下:

java
// 使用文件名称创建流对象
FileReader fr =
        new FileReader("abc.txt");

// 定义变量,保存数据
int b;

// 循环读取
while (
        (b = fr.read()) != -1
) {

    System.out.println(
            (char) b
    );
}

// 关闭资源
fr.close();

1.2.2 读取指定长度的字符 ​

使用:

java
read(
        char[] cbuf,
        int off,
        int len
)

将读取到的字符存储到字符数组中。

其中:

  • cbuf:存储读取结果的字符数组。
  • off:存储结果的起始位置。
  • len:要读取的字符数。

代码示例如下:

java
File textFile =
        new File(
                "docs/约定.md"
        );

// 给一个 FileReader 的示例
// try-with-resources FileReader
try (
        FileReader reader =
                new FileReader(textFile)
) {

    // read(char[] cbuf)
    char[] buffer =
            new char[1024];

    int len;

    while (
            (len = reader.read(
                    buffer,
                    0,
                    buffer.length
            )) != -1
    ) {

        System.out.print(
                new String(
                        buffer,
                        0,
                        len
                )
        );
    }
}

在这个例子中,使用 FileReader 从文件中读取字符数据,并将其存储到一个大小为 1024 的字符数组中。

每次读取 len 个字符,然后使用 String 构造方法将其转换为字符串并输出。

FileReader 实现了 AutoCloseable 接口,因此可以使用 try-with-resources 语句自动关闭资源,避免手动关闭资源的繁琐操作。

2. 字符输出流(Writer) ​

java.io.Writer 是字符输出流类的超类(父类),可以将指定的字符信息写入到目的地。

它定义了一些共性方法:

  1. write(int c):写入单个字符。
  2. write(char[] cbuf):写入字符数组。
  3. write(char[] cbuf, int off, int len):写入字符数组的一部分,off 为开始索引,len 为字符个数。
  4. write(String str):写入字符串。
  5. write(String str, int off, int len):写入字符串的一部分,off 指定要写入的子串在 str 中的起始位置,len 指定要写入的子串长度。
  6. flush():刷新该流的缓冲。
  7. close():关闭此流,但要先刷新它。

java.io.FileWriter 类是 Writer 的子类,用来将字符写入到文件。

2.1 FileWriter 构造方法 ​

  • FileWriter(File file):创建一个新的 FileWriter,参数为要读取的 File 对象。
  • FileWriter(String fileName):创建一个新的 FileWriter,参数为要读取的文件名称。

代码示例如下:

java
// 第一种:使用 File 对象创建流对象
File file =
        new File("a.txt");

FileWriter fw =
        new FileWriter(file);

// 第二种:使用文件名称创建流对象
// FileWriter fw = new FileWriter("b.txt");

2.2 FileWriter 写入数据 ​

2.2.1 写入字符 ​

使用:

java
write(int b)

方法,每次可以写出一个字符。

代码示例如下:

java
FileWriter fw = null;

try {

    fw =
            new FileWriter(
                    "output.txt"
            );

    fw.write(72);  // 写入字符 'H' 的 ASCII 码
    fw.write(101); // 写入字符 'e' 的 ASCII 码
    fw.write(108); // 写入字符 'l' 的 ASCII 码
    fw.write(108); // 写入字符 'l' 的 ASCII 码
    fw.write(111); // 写入字符 'o' 的 ASCII 码

} catch (
        IOException e
) {

    e.printStackTrace();

} finally {

    try {

        if (
                fw != null
        ) {

            fw.close();
        }

    } catch (
            IOException e
    ) {

        e.printStackTrace();
    }
}

在这个示例代码中,首先创建一个 FileWriter 对象 fw,并指定要写入的文件路径 "output.txt"。

然后使用 fw.write() 方法将字节写入文件中,这里分别写入字符 'H'、'e'、'l'、'l'、'o' 的 ASCII 码。

最后在 finally 块中关闭 FileWriter 对象,释放资源。

需要注意的是,使用:

java
write(int b)

方法写入的是一个字节,而不是一个字符。

如果需要写入字符,可以使用:

java
write(char cbuf[])

或者:

java
write(String str)

方法。

2.2.2 写入字符数组 ​

使用:

java
write(char[] cbuf)

方法,将指定字符数组写入输出流。

代码示例如下:

java
FileWriter fw = null;

try {

    fw =
            new FileWriter(
                    "output.txt"
            );

    char[] chars = {
            'H',
            'e',
            'l',
            'l',
            'o'
    };

    // 将字符数组写入文件
    fw.write(chars);

} catch (
        IOException e
) {

    e.printStackTrace();

} finally {

    try {

        if (
                fw != null
        ) {

            fw.close();
        }

    } catch (
            IOException e
    ) {

        e.printStackTrace();
    }
}

2.2.3 写入指定字符数组 ​

使用:

java
write(
        char[] cbuf,
        int off,
        int len
)

方法,将指定字符数组的一部分写入输出流。

代码示例如下,重复的部分就不写了,参照上面的部分:

java
FileWriter fw = null;

fw =
        new FileWriter(
                "output.txt"
        );

char[] chars = {
        'H',
        'e',
        'l',
        'l',
        'o',
        ',',
        ' ',
        'W',
        'o',
        'r',
        'l',
        'd',
        '!'
};

fw.write(
        chars,
        0,
        10
); // 将字符数组的前 5 个字符写入文件

fw.close();

使用 fw.write() 方法将字符数组的前 5 个字符写入文件中。

2.2.4 写入字符串 ​

使用:

java
write(String str)

方法,将指定字符串写入输出流。

代码示例如下:

java
FileWriter fw = null;

fw =
        new FileWriter(
                "output.txt"
        );

String str =
        "kerwim";

fw.write(str);

// 关闭资源
fw.close();

2.2.5 写入指定字符串 ​

使用:

java
write(
        String str,
        int off,
        int len
)

方法,将指定字符串的一部分写入输出流。

代码示例如下,使用 try-with-resources:

java
String str =
        "kerwim 真的 niuma!";

try (
        FileWriter fw =
                new FileWriter(
                        "output.txt"
                )
) {

    // 将字符串的前 5 个字符写入文件
    fw.write(
            str,
            0,
            5
    );

} catch (
        IOException e
) {

    e.printStackTrace();
}

【注意】如果不关闭资源,数据只是保存到缓冲区,并未保存到文件中。

2.3 关闭 close 和刷新 flush ​

因为 FileWriter 内置了缓冲区 ByteBuffer,所以如果不关闭输出流,就无法把字符写入到文件中。

image-20250321214213061

但是关闭了流对象,就无法继续写数据了。

如果我们既想写入数据,又想继续使用流,就需要 flush() 方法。

flush():

刷新缓冲区,流对象可以继续使用。

close():

先刷新缓冲区,然后通知系统释放资源,流对象不可以再被使用。

flush() 还是比较有趣的,来段代码体会一下:

java
// 源,也就是输入流【读取流】,读取 a.txt 文件

// 必须要存在 abc.txt 文件,否则报 FileNotFoundException 异常
FileReader fr =
        new FileReader(
                "abc.txt"
        );

// 目的地,也就是输出流
// 系统会自动创建 b.txt,因为它是输出流
FileWriter fw =
        new FileWriter(
                "b.txt"
        );

int len;

while (
        (len = fr.read()) != -1
) {

    fw.write(len);
}

// 注意这里没有使用 close 关闭流
// 开发中不能这样做,但是为了更好地体会 flush 的作用

注意:这里没有使用 close() 关闭流,开发中不能这样做,但是为了更好地体会 flush() 的作用。

运行效果是怎么样的呢?

答案是 b.txt 文件中依旧是空的,并没有任何东西。

image-20250321214551293

在以上代码中再添加下面三句代码,b.txt 文件就能复制到源文件的数据了:

java
fr.close();

fw.flush();

fw.close();

flush() 这个方法是清空缓存的意思,用于清空缓冲区的数据流。

进行流操作时,数据先被读到内存中,然后再把数据写到文件中。

你可以使用下面的代码示例再体验一下:

java
// 使用文件名称创建流对象
FileWriter fw =
        new FileWriter(
                "fw.txt"
        );

// 写出数据,通过 flush
fw.write('刷'); // 写出第 1 个字符
fw.flush();

fw.write('新'); // 继续写出第 2 个字符,写出成功
fw.flush();

// 写出数据,然后 close
fw.write('关'); // 写出第 1 个字符
fw.close();

fw.write('闭'); // 继续写出第 2 个字符,【报错】java.io.IOException: Stream closed
fw.close();

注意,即便是 flush() 方法写出了数据,操作的最后还是要调用 close() 方法释放系统资源。

当然,也可以使用 try-with-resources 的方式。

2.4 FileWriter 的续写和换行 ​

续写和换行操作类似于 FileOutputStream,直接上代码:

java
// 使用文件名称创建流对象,可以续写数据
FileWriter fw =
        new FileWriter(
                "fw.txt",
                true
        );

// 写出字符串
fw.write("kerwim");

// 写出换行
fw.write("\r\n");

// 写出字符串
fw.write("是傻 X");

// 关闭资源
fw.close();

输出结果如下:

text
kerwim
是傻 X

2.5 文本文件复制 ​

直接上代码:

java
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class CopyFile {

    public static void main(
            String[] args
    ) throws IOException {

        // 创建输入流对象
        FileReader fr =
                new FileReader(
                        "a.txt"
                );
        // 文件不存在会抛出 java.io.FileNotFoundException

        // 创建输出流对象
        FileWriter fw =
                new FileWriter(
                        "copyaa.txt"
                );

        /*
         * 创建输出流做的工作:
         * 1、调用系统资源创建了一个文件
         * 2、创建输出流对象
         * 3、把输出流对象指向文件
         */

        // 文本文件复制,一次读一个字符
        copyMethod1(
                fr,
                fw
        );

        // 文本文件复制,一次读一个字符数组
        copyMethod2(
                fr,
                fw
        );

        fr.close();
        fw.close();
    }

    public static void copyMethod1(
            FileReader fr,
            FileWriter fw
    ) throws IOException {

        int ch;

        while (
                (ch = fr.read()) != -1
        ) {

            // 读数据
            fw.write(ch);
        }

        fw.flush();
    }

    public static void copyMethod2(
            FileReader fr,
            FileWriter fw
    ) throws IOException {

        char chs[] =
                new char[1024];

        int len = 0;

        while (
                (len = fr.read(chs)) != -1
        ) {

            // 写数据
            fw.write(
                    chs,
                    0,
                    len
            );
        }

        fw.flush();
    }
}

我们在学习的过程中,可能习惯把异常抛出,而实际开发中建议使用:

text
try...catch...finally

代码块处理异常。

格式代码如下:

java
// 声明变量
FileWriter fw = null;

try {

    // 创建流对象
    fw =
            new FileWriter(
                    "fw.txt"
            );

    // 写出数据
    fw.write(
            "kerwim 真的帅"
    ); // kerwim 真的帅

} catch (
        IOException e
) {

    e.printStackTrace();

} finally {

    try {

        if (
                fw != null
        ) {

            fw.close();
        }

    } catch (
            IOException e
    ) {

        e.printStackTrace();
    }
}

或者直接使用 try-with-resources 的方式:

java
try (
        FileWriter fw =
                new FileWriter(
                        "fw.txt"
                )
) {

    // 写出数据
    fw.write(
            "kerwim 真的帅"
    ); // kerwim 真的帅

} catch (
        IOException e
) {

    e.printStackTrace();
}

在这个代码中,try-with-resources 会在 try 块执行完毕后自动关闭 FileWriter 对象 fw,不需要手动关闭流。

如果在 try 块中发生了异常,也会自动关闭流并抛出异常。

因此,使用 try-with-resources 可以让代码更加简洁、安全和易读。

3. 小结 ​

Writer 和 Reader 是 Java IO 中用于字符输入输出的抽象类,它们提供了一系列方法用于读取和写入字符数据。

它们的区别在于:

text
Writer
    ↓
将字符数据写入输出流

Reader
    ↓
从输入流读取字符数据

Writer 和 Reader 的常用子类有:

text
FileWriter
FileReader

可以将字符流写入和读取到文件中。

在使用 Writer 和 Reader 进行字符输入输出时,需要注意字符编码的问题。

Noctiflorous Garden · MIT License