【深入理解Java IO流0x04】Java字符流Reader&Writer

1. 引言

字符流是一种用于读取和写入字符数据的输入输出流。与字节流不同,字符流以字符为单位读取和写入数据,而不是以字节为单位。常用来处理文本信息。
如果用字节流直接读取中文,可能会遇到乱码问题,见下例:

@Test
public void test008() throws IOException{
    FileInputStream fis = new FileInputStream("test.txt");
    int len=0;
    while((len=fis.read())!=-1){
        System.out.print((char)len);
    }
    fis.close();
}

image.png
之所以出现乱码是因为在字节流中,一个字符通常由多个字节组成,而不同的字符编码使用的字节数不同。如果我们使用了错误的字符编码,或者在读取和写入数据时没有正确处理字符编码的转换,就会导致读取出来的中文字符出现乱码。
例如,当我们使用默认的字符编码(见上例)读取一个包含中文字符的文本文件时,就会出现乱码。因为默认的字符编码通常是 ASCII 编码,它只能表示英文字符,而不能正确地解析中文字符。
那使用字节流该如何正确地读出中文呢?见下例。

@Test
public void test009() throws IOException{
    FileInputStream fis = new FileInputStream("test.txt");
    byte[] buf = new byte[1024];
    int len=0;
    while((len=fis.read(buf))!=-1){
        System.out.print(new String(buf,0,len));
    }
}
----------------------------------------------------
output:
我爱喝可乐,你呢?
Hello World.

为什么这种方式就可以呢?
因为我们拿 String 类进行了解码,查看new String(byte bytes[], int offset, int length)的源码就可以发现,该构造方法有解码功能:

public String(byte bytes[], int offset, int length) {
    checkBounds(bytes, offset, length);
    this.value = StringCoding.decode(bytes, offset, length);
}

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

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 中,常用的字符编码有 ASCII、ISO-8859-1、UTF-8、UTF-16 等。其中,ASCII 和 ISO-8859-1 只能表示部分字符,而 UTF-8 和 UTF-16 可以表示所有的 Unicode 字符,包括中文字符。
当我们使用new String(byte bytes[], int offset, int length)将字节流转换为字符串时,Java 会根据 UTF-8 的规则将每 3 个字节解码为一个中文字符,从而正确地解码出中文。
尽管字节流也有办法解决乱码问题,但不够直接,于是就有了字符流,专门用于处理文本文件。
从另一角度来说:字符流 = 字节流 + 编码表

2. 字符输入流:Reader&FileReader

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

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

FileReader是Reader的最简单的子类,用于从文件中读取字符数据。它的主要特点如下:

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

2.1 构造方法

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

实战代码:

File file = new File("test.txt");
FileReader fr = new FileReader(file);
// 或者
FileReader fr = new FileReader("test.txt")

2.2 常用方法

一、读取字符
read方法,每次可以读取一个字符,返回读取的字符(转为 int 类型),当读取到文件末尾时,返回-1。代码示例如下:

@Test
public void test010() throws IOException{
    FileReader fr = new FileReader("test.txt");
    int b=0;
    while((b=fr.read())!=-1){
        System.out.println(b+"->"+(char)b);
    }
    fr.close();
}
---------------------------------------------
output:
25105->29233->21917->21487->20048->65292->20320->21602->65311->13->
10->

72->H
101->e
108->l
108->l
111->o
32-> 
87->W
111->o
114->r
108->l
100->d
46->.

二、读取指定长度的字符
read(char[] cbuf, int off, int len)其中,cbuf 表示存储读取结果的字符数组,off 表示存储结果的起始位置,len 表示要读取的字符数。代码示例如下:

@Test
public void test010() throws IOException{
    FileReader fr = new FileReader("test.txt");
    char[] buf = new char[1024];
    int len=0;
    while((len=fr.read(buf))!=-1){
        System.out.println(new String(buf,0,len));
    }
    fr.close();
}
------------------------------------------------
output:
我爱喝可乐,你呢?
Hello World.

在这个例子中,使用 FileReader 从文件中读取字符数据,并将其存储到一个大小为 1024 的字符数组中。每次读取 len 个字符,然后使用 String 构造方法将其转换为字符串并输出。

3. 字符输出流:Writer&FileWriter

java.io.Writer是字符输出流类的超类,可以将指定的字符信息写入到目的地,来看它定义的一些共性方法:

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

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

3.1 构造方法

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

代码示例如下:

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

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

3.2 常用方法

一、写入字符
write(int b)方法,每次可以写出一个字符,代码示例如下:

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码
    fw.write(21487); // 写入字符"可"
    fw.write(20048); // 写入字符"乐"
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        if (fw != null) {
            fw.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

我们可以看到,FileReader的write(int b)方法是能够写入中文的,比如"可"对应的int为21487,但显然在实际中我们不会这么用。
二、写入字符数组
write(char[] cbuf)方法,将指定字符数组写入输出流。代码示例如下:

@Test
public void test011() throws IOException{
    FileWriter fw = null;
    try {
        fw = new FileWriter("output.txt");
        char[] chs = new char[]{'H','e','l','l','o','我','是','谁','?'};
        fw.write(chs); // 写入字符'H'的ASCII码
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (fw != null) {
                fw.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

三、写入字符串
write(String str) 方法,将指定字符串写入输出流。代码示例如下:

fw = new FileWriter("output.txt");
String str = "我爱喝可乐";
fw.write(str); // 将字符串写入文件

3.3 关闭close与刷新flush

关闭与刷新这两个概念比较重要,所以我们这里单独开一小章。
因为 FileWriter 内置了缓冲区 ByteBuffer,所以如果不关闭输出流,就无法把字符写入到文件中。
但是关闭了流对象,就无法继续写数据了。如果我们既想写入数据,又想继续使用流,就需要 flush 方法了。

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

flush()这个方法是清空缓存的意思,用于清空缓冲区的数据流,进行流的操作时,数据先被读到内存中,然后再把数据写到文件中。

// 使用文件名称创建流对象
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();

4. 实战:使用字符流复制文本

直接看代码:

@Test
public void test012() throws IOException{
    FileReader fr = new FileReader("test.txt");
    FileWriter fw = new FileWriter("output.txt");

    char[] chs = new char[1024];
    int len=0;
    while((len=fr.read(chs))!=-1){
        fw.write(chs,0,len);
    }
    fr.close();
    fw.close();
}

5. 小结

Writer 和 Reader 是 Java I/O 中用于字符输入输出的抽象类,它们提供了一系列方法用于读取和写入字符数据。它们的区别在于 Writer 用于将字符数据写入到输出流中,而 Reader 用于从输入流中读取字符数据。
Writer 和 Reader 的常用子类有 FileWriter、FileReader,可以将字符流写入和读取到文件中。
在使用 Writer 和 Reader 进行字符输入输出时,需要注意字符编码的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值