IO流3(字符流)

字符流

在IO流2中我们说过,出现乱码的原因有两个:

1.读取数据时没有读完整个汉字

2.编码与解码的方式不统一

第二点是好解决的,我们在开发中统一使用UTF-8即可。第一点如何解决呢?

我们希望存在这么一个流:默认也是读取一个字节,但当遇到中文时,可以一次读取多个字节。字符流应运而生。字符流实际上是字节流+字符集。对于字符输入流,其特点是一次读一个字节,遇到中文时一次读多个字节。具体是几个字节,取决于字符集。字符输出流也是一样,底层会把数据按照指定的编码方式进行编码,变成字节再写到文件中。所以它很适合操作纯文本文件。

继承关系

IO流有两个儿子:字节流与字符流。字节流有两个儿子InputStream(字节输入流)与OutputStream(字节输出流),字符流有两个儿子Reader(字符输入流)和Writer(字符输出流)。但是这四个都是抽象类,不能直接创建它们的对象,需要看它们的子类。如果想要从纯文本文件读取数据,就使用FileReader,与FileInputStream类似。前面表示作用,后面表示继承对象。

FileReader

创建字符输入流对象:

成员方法 说明
public FileReader(File file) 创建字符输入流关联本地文件
public FileReader(String pathName) 创建字符输入流关联本地文件

需要注意,如果文件不存在,就直接报错。

读取数据:

成员方法 说明
public int read() 读取数据,读到末尾返回-1
public int read(char[] buffer) 读取多个数据,读到末尾返回-1

buffer用来存放读取到的数据。

释放资源依然是close()

无参read

我们将a.txt的内容改成"轻音少女"+轻音部五个成员的名字,即内容为:

复制代码
轻音少女
平泽唯
秋山澪
田井中律
琴吹紬
中野梓
ini 复制代码
        FileReader fr = new FileReader("io/a.txt");
        int ch;
        while((ch=fr.read())!=-1) {
            System.out.print(ch);
        }
        fr.close();

我们发现输出的是一串数字。原因很简单。上文我们说过字符流的底层还是字节流,默认是以一个字节为单位读取数据的,只不过遇到中文时会一次读取多个字节。read方法读取到中文后,解码并转换为十进制,最后将这个十进制数字赋值给ch,而我们直接打印ch,就必然只会输出数字。要想看到中文,对这些数字进行强转即可。

ini 复制代码
        FileReader fr = new FileReader("io/a.txt");
        int ch;
        while((ch=fr.read())!=-1) {
            System.out.print((char)ch);
        }
        fr.close();
有参read
ini 复制代码
        FileReader fr = new FileReader("io/a.txt");
        char[] chars = new char[2];
        int len;
        while((len = fr.read(chars))!=-1) {
            System.out.println(new String(chars,0,len));
        }
        fr.close();

有参read的返回值是读取到的长度。之所以需要单独用一个变量记录,是因为最后一次读取的时候可能没有填充完数组,为了不将不必要的东西输出到控制台,读了多少我们就输出多少。输出结果如下:

轻音 少女

平泽 唯

秋 山澪

田井 中律

琴吹 紬

中 野梓

为什么会这样呢?是因为在原来的a.txt中我们是有换行的,而在Windows中换行为\r\n.我们模拟一下这个过程:

轻音 -> 少女 -> \r\n -> 平泽 -> 唯 -> \r -> \n秋 -> 山澪 -> \r\n -> 田井 -> 中律 -> \r\n -> 琴吹 -> 紬\r -> \n中 -> 野梓

\r\n只是换到下一行,为什么有些会空两行,有些会空一行?这是因为我们使用的是println,打印完\r\n后还会再换一次行。

所以,如果我们想和文本保持一致,只需要将println改为print即可。

可见,空参和有参看似只有参数的区别,实际上还有很多不同。对于空参的read方法,它仅仅走到将二进制转换为十进制就结束了,因此返回的就是这个十进制数字,后续文字的输出还需要我们进行类型强转。而有参的read方法将读取数据、解码、强转三步结合在一起了,并把强转之后的字符放到buffer中,返回读取数据的长度。

FileWriter

构造方法

构造方法 说明
public FileWriter(File file) 创建字符输出流关联本地文件
public FileWriter(String pathname) 创建字符输出流关联本地文件
public FileWriter(File file, boolean append) 创建字符输出流关联本地文件,续写
public FileWriter(String pathname, boolean append) 创建字符输出流关联本地文件,续写

如果文件不存在,会创建一个新的文件,但要保证父级路径是存在的。如果文件已经存在,则会清空文件,如果不想清空可以打开续写开关。

成员方法

成员方法 说明
void write(int c) 写出一个字符
void write(String str) 写出一个字符串
void write(String str, int off, int len) 写出字符串的一部分
void write(char[] cbuf) 写出一个字符数组
void write(char[] cbuf, int off, int len) 写出字符数组的一部分

如果write的参数是一个整数,则写入到本地文件上的实际是整数在字符集上对应的字符。

字符输入流的底层原理

我们假设数据源的内容为"a我",编码规则是UTF-8,现在我们使用字符输入流将数据读到内存中。创建FileReader对象后,相当于在数据源与内存之间建立了一条通道。

但与此同时,它还在内存中创建了一个长度为8192的字节数组,我们称这个数组为缓冲区。

ini 复制代码
int ch;
ch = fr.read();
System.out.println((char)ch);
ch = fr.read();
System.out.println((char)ch);
ch = fr.read();
System.out.println((char)ch);

字符输入流对象调用read方法时,它会先判断缓冲区中是否有数据可以读。如果没有,就先从文件中尽可能多地将数据读入缓冲区(尽可能多,指的是能塞满缓冲区就塞满),然后再从缓冲区中读数据。如果缓冲区中已经有数据了,就从缓冲区中读数据。这么一来其效率就会更高,减少了频繁从硬盘中读取数据的操作。

第一次调用read方法,此时缓冲区为空,会先从文件中读取数据到缓冲区,因为一个中文汉字是3个字节,所以会读取4个字节到缓冲区。接着,从缓冲区中读数据,读到的是第一个字节,把第一个字节按照UTF-8规则进行解码,转换为十进制,赋值给ch,所以ch记录的就是97,强转之后打印的就是小写a.

第二次调用read方法,此时缓冲区还有3个字节,直接从缓冲区读取字节。因为是中文汉字,所以一次性读取3个字节,将其按照UTF-8规则进行解码,转换为十进制,赋值给ch,所以ch记录的就是25105,强转之后打印的就是"我"。

第三次调用read方法,此时缓冲区为空,那么它就会从文件中尽可能多地将数据读入缓冲区,但是此时文件已经到末尾了,所以会返回-1,并赋值给ch.

需要区分:创建字节流对象时,是不会创建缓冲区的。

我们可以验证一下。将a.txt中的内容改成"ab我":

ini 复制代码
        FileReader fr = new FileReader("io/a.txt");
        int b1 = fr.read();
        System.out.println(b1);
        int b2 = fr.read();
        System.out.println(b2);
        int b3 = fr.read();
        System.out.println(b3);
        System.out.println((char)b3);
        int b4 = fr.read();
        System.out.println(b4);
        
        fr.close();

我们在创建字符输入流对象那一行打断点,右键选择Debug

可以看到那一行变成了蓝色,说明它准备执行。点击下一步即可。

可以看到fr对象成功创建,它有很多数据,找到bb,它就是我们前面所说的缓冲区。从下面的hb后面的信息中我们可以看到,它是byte[]类型的,容量为8192.现在缓冲区中还没有数据。

继续点击下一步。

可见,调用read方法后,数据被写入到了缓冲区。并且97 98的确是字母a和b在ASCII中对应的数字。后面的-26 -120 -111对应的是汉字"我",因为UTF-8中一个中文汉字使用3个字节表示。它也的确识别出来这是一个中文汉字,并给出了其十进制数字25105.

如果文件的数据比较多,超出了8192,那么超出的部分就会覆盖缓冲区的头部。

如果我们先创建了FileReader,并调用了一次read方法,再创建了与相同文件进行关联的FileWriter(只有一个参数,也就是不续写,则会清空文件内容),那么再调用read方法,还能读到数据吗?是可以的。因为第一次调用read方法时,缓冲区会拿到数据。清空文件内容,也仅仅只是清空文件,并不清空缓冲区。而后续调用read方法时,是先判断缓冲区是否有数据,而现在缓冲区有数据,所以会直接从缓冲区中读取数据。

字符输出流的底层原理

创建字符输出流对象,相当于在内存与目的地(UTF-8)之间建立了一条通道。同时,也创建了一个长度为8192的字节数组,即缓冲区。当我们调用write方法时,实际上会先将数据写入到缓冲区。那么,什么时候数据才能真正去到目的地呢?有三种情况:1.缓冲区容量不够;2.手动刷新(flush方法);3.释放资源(close)

调用完flush方法后,还能继续往文件中写入数据。

lua 复制代码
        FileWriter fw = new FileWriter("io/b.txt");
        fw.write("平泽唯");
        fw.write("秋山澪");
        fw.write("田井中律");
        fw.write("琴吹紬");
        fw.write("中野梓");
        fw.write(97);
        fw.close();

我们同样在创建对象那一行打断点,debug

可以看到的确存在缓冲区。当我们写入数据的时候,实际上是将这些数据根据UTF-8规则编码成字节,再将这些字节放到缓冲区中。

写入"平泽唯"之后,我们可以看到缓冲区确实发生了变化,而且3个中文汉字,对应9个字节,实践与理论相符。下面的也是一样。

不过,我们发现,五个人的名字都write了之后,a.txt中并没有任何数据。我们验证一下上文的理论是否正确,即当:

1.缓冲区容量不够

2.手动刷新

3.释放资源

时才会将缓冲区的数据写入到文件中。

对于1,我们使用一个循环即可判断。

ini 复制代码
        FileWriter fw = new FileWriter("io/a.txt");
        for (int i = 0; i < 8192; i++) {
            fw.write(97);
        }
        System.out.println("---");
        fw.close();

注:键入8192.fori再按回车就可以得到完整的for(int i=0; i<8192; i++)

下面之所以多了一行输出,是因为如果我们将断点打在创建对象那一行,下面的for循环就要点很多次。因此我们在其下方新增一行无关紧要的内容,将断点打在这一行上,这样调试的时候上面的代码就已经全部执行完毕了。

可以看到缓冲区里全部都是97,但是a.txt仍然没有内容,因为此时缓冲区容量刚好够。我们将循环次数从8192改成8193:可以看到运行完后,a.txt内已经有很多个a了。右键a.txt,选到"打开于",选择资源管理器。右键后点击属性,可以看到其字节数确实为8193:

对于2:我们在中间调用flush,并且不关流

lua 复制代码
        FileWriter fw = new FileWriter("io/a.txt");
        fw.write("平泽唯");
        fw.write("秋山澪");
        fw.flush();
        fw.write("田井中律");
        fw.write("琴吹紬");
        fw.write("中野梓");
        fw.write(97);

可以看到文件中只有平泽唯和秋山澪。因为律、紬和梓的名字都在缓冲区中。

对于3:我们在上面的例子中关流,运行后可以看到五个人的名字都在文件中,说明释放资源确实能够将缓冲区的内容输出到文件中。

相关推荐
鱼跃鹰飞1 天前
设计模式系列:工厂模式
java·设计模式·系统架构
a努力。1 天前
国家电网Java面试被问:混沌工程在分布式系统中的应用
java·开发语言·数据库·git·mysql·面试·职场和发展
Yvonne爱编码1 天前
Java 四大内部类全解析:从设计本质到实战应用
java·开发语言·python
J2虾虾1 天前
SpringBoot和mybatis Plus不兼容报错的问题
java·spring boot·mybatis
毕设源码-郭学长1 天前
【开题答辩全过程】以 基于springboot 的豪华婚车租赁系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
Tao____1 天前
通用性物联网平台
java·物联网·mqtt·低代码·开源
曹轲恒1 天前
SpringBoot整合SpringMVC(上)
java·spring boot·spring
JH30731 天前
Java Spring中@AllArgsConstructor注解引发的依赖注入异常解决
java·开发语言·spring
码农水水1 天前
米哈游Java面试被问:机器学习模型的在线服务和A/B测试
java·开发语言·数据库·spring boot·后端·机器学习·word
2601_949575861 天前
Flutter for OpenHarmony二手物品置换App实战 - 表单验证实现
android·java·flutter