【Java】2.0 Java核心之IO流(二)——生猛理解字符流
0.0 为什么要写这个。
- 在当初学
java语言
的时候,其实感觉这算是最难的基础部分内容之一,因为字符流和字节流
的存在,还有缓冲字节流、字符流
,选择太多,不同的限制,导致使用的时候根本不知道:
1. 到底什么情况下怎么写
2. 什么方案和代码,写会没有什么大的问题。
- 所以在这里写下相关知识点,所谓
授人以渔
,更要授人以鳞、 鲤、 鲫、 鲸、 鳍、 鳌、 鳃、 鳜、 鲈、 鲑、 鲧、 鲲、 鲱、 鲰、 鲵、 鲋、 鳟、 鳒、 鲥、 鲝、 鲹、 鲭、 鲉、 鲽、 鳀、 鲐、 鲠、 鳑、 鳛、 鲞、 鲬、 鳇、 鲢、 鲮、 鳐、 鲔……
3.本来一个内容就打算写一篇的,简书说我写得太长了,不许我发布,所以拆成两篇,查阅本篇的朋友请结合另一篇一同参考,谢谢。
链接如下:
【Java】1.0 Java核心之IO流(一)——生猛理解字节流
8.0 字符流
字符流内容用的机会相当少,接着往下看你就会发现,也不太方便用,所以尽量少地讲解,但是会有几个有趣的算法实现,通过单独的一篇文章记录,也算是对IO流的一些综合运用。
【链接暂空】
8.1.字符流是什么
- 字符流是可以直接读写字符的IO流
- 字符流读取字符, 就要先读取到字节数据, 然后转为字符。 如果要写出字符, 需要把字符转为字节再写出.
8.2 首先要普及一下中文字符的相关知识点
举个例子,我们中文用的码表,一般是GBK码表(GBK码表是java平台默认的中文编码表,我们常用的UTF-8码表中通常1个中文字符代表3个字节)
- GBK码表是1个中文分为2个字节,第1个字节一定是个负数,第2个字节是正数
- 计算机读取的时候,虽然也是1个字节1个字节地读取,但是它会读到下一个负数字节,停顿,然后两个字节、两个字节的拼接在一起读取,这就是字符流读取数据的原理。
- 别试图去百度GBK码表和UTF-8码表的原理,光介绍的那一两千字讲晕你怀疑人生信不信。
8.3 字符流的抽象父类:
Reader
Writer
我们从这里开始讲起,其实正常的出招方式如下代码:
FileReader fr = new FileReader("aaa.txt"); //创建输入流对象,关联aaa.txt
int ch;
while((ch = fr.read()) != -1) { //将读到的字符赋值给ch
System.out.println((char)ch); //将读到的字符强转后打印
}
fr.close(); //关流
这里aaa.txt默认创建就好,里面随便写些中文,别设置成utf-8编码方式,小心乱码怀疑人生。
写出的出招方式如下:
FileWriter fw = new FileWriter("aaa.txt");
fw.write("大家好,我是渣渣辉。");
//97输出后,在aaa.txt文件中会变成a,
//因为字符流先处理字符问题,再转成字节流输出,所以它同样经历了字节流的处理
fw.write(97);
fw.close();
拷贝的出招方式(就是上面的合在一起,先读取后再写出)如下:
FileReader fr = new FileReader("a.txt");
FileWriter fw = new FileWriter("b.txt");
int ch;
while((ch = fr.read()) != -1) {
fw.write(ch);
}
fr.close();
fw.close();
一看就知道,还是熟悉的配方还是熟悉的套路。但是!
划线重点来了。
8.31 原理,分析原理。
举个例子,字符流的抽象父类 Reader
,这个叫字符输入流,因为抽象,所以不能用它来实例化对象,我们来查看JDK API文档:
可以看到,它继承自Object包,直接子类也不多。
- 在字节流里面,
InputStream
类是爸爸,但我们可以看到,实际写代码时一般都是儿子FileInputStream
在干活。
同样字符流的爸爸Reader
类,我们可以试着找一找有没有个儿子叫FileReader
类,这样我们就可以照葫芦画瓢,这一小节也就不用学了,但是我们会发现——没有!
我们找其中一个InputStreamReader
类,试试看:
哎!你会发现它就只有一个直接子类
FileReader
类,不着急,我们再看下这个孙子FileReader
类:
2019-03-09_222741.png
这可不是我截图不全,已经是全部了。会发现,孙子地位不行,能力也没有,全靠继承老爹
InputStreamReader
类(虽然它爹只生了一个儿子)和爷爷、太爷爷的各种方法。
我们平时要用的字符流,就是在用这个孙子FileReader
类。
那为什么本来是儿子的地位沦落到孙子的地位,还这么惨?
这里涉及我们上面所说的装饰者模式。(我们先不着急说明什么是装饰者模式,暂且不表)
8.32 在写出的时候,如下代码:
FileWriter fw = new FileWriter("aaa.txt");
fw.write("大家好,我是渣渣辉。");
//97输出后,在aaa.txt文件中会变成a,
//因为字符流先处理字符问题,再转成字节流输出,所以它同样经历了字节流的处理
fw.write(97);
fw.close();
如果不写fw.close();
,执行会发现,什么也没有存入。
哎不对呀!还学没到缓冲字符流的地步,怎么不执行fw.close();
就不行了,FileWriter
类 明显不按套路出牌。
我们查看FileWriter
类源代码:
2019-03-09_225626.png
这里就不贴代码了,里面同样看不出什么来,继续看它的继承父类OutputStreamWriter
的源代码:
同样看不出什么来,不急,继续看它的继承父类
Writer
的源代码:2019-03-09_225857.png
哎!这里有了,虽然不是缓冲流,但是它自带了一个小缓冲
writeBuffer
,而且大小是1024,注意了,这里的1024不是1024个字节,而是1024个字符 = 2048个字节。所以如果用字符流的
FileWriter
类和FileReader
类而不去关闭它们,就会丢失数据。
8.4 类比一下,同样字符流也可以像字节流一样,可以不必一个字符一个字符地读和写,自定义小数组:
public static void demo() throws FileNotFoundException, IOException {
FileReader fr = new FileReader("xxx.txt");
FileWriter fw = new FileWriter("yyy.txt");
char[] arr = new char[1024];
int len;
while((len = fr.read(arr)) != -1) { //将文件上的数据读取到字 ···符数组中
fw.write(arr,0,len); //将字符数组中的数据写到文件上
}
fr.close();
fw.close();
}
你看,字符流就这些东西,完毕。
9.0 缓冲字符流
9.1 缓冲字符流
同样,目的还是为了提高效率,才会有缓冲字符流,如果你用字符流并不需要用到这样的功效,那么大可不必使用缓冲字符流,用字符流就可以了。
-
BufferedReader
的read()方法
读取字符时会一次读取若干字符到缓冲区, 然后逐个返回给程序, 降低读取文件的次数, 提高效率。 -
BufferedWriter
的write()方法
写出字符时会先写到缓冲区, 缓冲区写满时才会写到文件, 降低写文件的次数, 提高效率。
9.2 用法同样很简单,反正都是3步走:
BufferedReader br = new BufferedReader(new FileReader("aaa.txt")); //创建字符输入流对象,关联aaa.txt
BufferedWriter bw = new BufferedWriter(new FileWriter("bbb.txt")); //创建字符输出流对象,关联bbb.txt
int ch;
//read一次,会先将缓冲区读满,从缓冲去中一个一个的返给临时变量ch
while((ch = br.read()) != -1) {
//write一次,是将数据装到字符数组,装满后再一起写出去
bw.write(ch);
}
//关流
br.close();
bw.close();
其原理和字节流一模一样。
9.3 细节来了。字符流中有一些方法,比较有意思,可以加以了解。
public static void demo() throws FileNotFoundException, IOException {
BufferedReader br = new BufferedReader(new FileReader("zzz.txt"));
String line;
while((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
}
- 关于这段代码,我们可以去查看文档,在我们的缓冲字符输入流
BufferedReader
中有一个readLine()
方法,作用是读取一个文本行。我们可以去查看这个方法的源码,你就会发现自己居然也可以为java语言贡献一个方法出来。
2019-03-10_003324.png
注意了,我们的缓冲字符输入流BufferedReader
中的readLine()
方法,读取后,相应的换行、回车符(\r
、\n
)就丢了,如果你常试直接写出的话,会发现所有的内容都会在同一自然段全部写完。
聪明的朋友已经猜到了,没错我们的缓冲字符输出流BufferedWriter
中除了有一个 write()
方法,查看文档还发现出现一个专门换行的方法newLine()
:
BufferedReader br = new BufferedReader(new FileReader("zzz.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("aaa.txt"));
String line;
while((line = br.readLine()) != null) {
bw.write(line);
bw.newLine(); //写出回车换行符
//bw.write("\r\n");
}
br.close();
bw.close();
当你需要这样去读取文件时,上面就是可以copy的模板了。
细节又来了。我们的注释语句里面有一句//bw.write("\r\n");
,我们可以看到,newLine()
这个方法我们不可以直接通过注释中那样编写代码也能实现吗?为什么java要这么累赘一下。
我们不要忘了,在windows操作环境下,他们的确是一样的,但不同的是newLine()
方法可以兼容所有JVM运行的平台。
9.4 我们的缓冲字符输入流BufferedReader
,还有一个独生子。
-
LineNumberReader
跟踪行号的缓冲字符输入流。继承自BufferedReader
父类。意思就是说,它和BufferedReader
具有相同的功能, 一模一样,只不过是多了个可以统计行号的技能。 -
BufferedWriter
注意!缓冲字符输出流BufferedWriter
并没有亲儿子,其他缓冲流也没有,都是单身狗。
public static void main(String[] args) throws IOException {
LineNumberReader lnr = new LineNumberReader(new FileReader("zzz.txt"));
String line;
lnr.setLineNumber(100);
while((line = lnr.readLine()) != null) {
System.out.println(lnr.getLineNumber() + ":" + line);
}
lnr.close();
}
如上,你可以常试这样去用。我们先把lnr.setLineNumber(100);
注释掉。
可以看到输出会从第0行开始,加冒号:
,然后加具体哪一行的内容,我们可以查看LineNumberReader
类的源代码:
可以看到里面有一个变量
lineNumber
初始值是0,并提供了get( )方法
和set( )方法
。所以,当我们把
lnr.setLineNumber(100);
注释去掉时,会发现输出语句的记录行数会从第100行开始,就这么个功能,需要就用。而且这样功能的类只有它有,其他缓冲流都没有。
这一部分其实就这么写内容,完毕,更加深入的运用,上面也给出一个另一篇文章的链接,里面会深入讲解IO流的使用。粘贴下来:
【链接暂空】
10.0 装饰者模式
当然,我到现在依然分不明白23种设计模式,都不一定全能默写出来,更别说分类了。
装饰者模式通俗理解为
:小情人Mary过完轮到Sarah过生日,还是不要叫她自己挑了生日礼物,不然这个月伙食费肯定玩完,拿出我去年在步行街照的照片,在背面写上“最好的的礼物,就是爱你的老刚”,再到街上礼品店买了个像框(卖礼品的MM也很漂亮哦),再找隔壁搞美术设计的Mike设计了一个漂亮的盒子装起来……,我们都是装裱匠,最终都在装饰我这个人。
开个玩笑。我们这里当然是讲解装饰者模式,主要是基于IO流来说的,毕竟它基本上就是基于装饰者模式弄出来的。举个例子:
interface Coder {
public void code();
}
class Student implements Coder {
@Override
public void code() {
System.out.println("javase");
System.out.println("javaweb");
}
}
class JuanLanMenStudent implements Coder {
//1,获取被装饰类的引用
private Student s; //获取学生引用
//2,在构造方法中传入被装饰类的对象
public JuanLanMenStudent (Student s) {
this.s = s;
}
//3,对原有的功能进行升级
@Override
public void code() {
s.code();
System.out.println("ssh");
System.out.println("数据库");
System.out.println("大数据");
System.out.println("...");
}
}
有一种编码能力,刚出道的大学生,可能就学了点javase
和javaweb
,发现毕业后工作不好找,工资不咋地。于是决定拜山学艺,就拜入卷帘门当学生。于是他在卷帘门还学了ssh
、数据库
、大数据
、高速路发传单等等等等
,这样对原来的大学生进行装饰,他就是卷帘门出品的大学生那毕业后就了不得了。
所以,重点又来了!——装饰设计模式的好处是:
* 耦合性不强,被装饰的类的变化与装饰类的变化无关,子类的改变不会引起父类的改变。
11.0 下面讲的东西怕看多了搞混,所以单拿出来写最后,注意:这里面的功能都是字符流里面的特例了,和字节流没有什么共同特性(字节流里面没有类似的功能、方法)
11.1 使用指定的码表读写字符
-
FileReader
是使用默认码表读取文件, 如果需要使用指定码表读取, 那么可以使用InputStreamReader(字节流,编码表)
-
FileWriter
是使用默认码表写出文件, 如果需要使用指定码表写出, 那么可以使用OutputStreamWriter(字节流,编码表)
InputStreamReader isr = new InputStreamReader(new FileInputStream("utf-8.txt"), "uTf-8"); //指定码表读字符
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("gbk.txt"), "gbk"); //指定码表写字符
int c;
while((c = isr.read()) != -1) {
osw.write(c);
}
isr.close();
osw.close();
**细节:
- 1.0 ** 这里的
"UTF-8"
、"GBK"
里面的英文字母大小写随意,还可以大小写乱搭,只要你字母顺序别打错就行了。 - 2.0 这里发现没,终于用了
InputStreamReader
和OutputStreamWriter
类,很多人有疑问,比如InputStreamReader
类前半段是字节流的InputStream
,后半段是字符流的Reader
,那它到低算字节流还是字符流?
其实我们查看源代码或者查看Java API文档,就知道InputStreamReader
类继承自Reader
类,当然是根正苗红的字符流了。 - 3.0 上面我们当然还可以优化,毕竟缓冲流的存在不就是为了这个么:
BufferedReader br =//更高效的读
new BufferedReader(new InputStreamReader(new FileInputStream("utf-8.txt"), "uTf-8"));
BufferedWriter bw =//更高效的写
new BufferedWriter(new OutputStreamWriter(new FileOutputStream("gbk.txt"), "gbk"));
int c;
while((c = br.read()) != -1) {
bw.write(c);
}
br.close();
bw.close();
END