IO流按照流向分类是指输入流和输出流。其中,输入输出是相对于内存而言的。
将外设中的数据读取到内存中是输入,读。
将内存中的数据写入到外设中是输出,写。
IO流也可以按照所操作的数据分为两种:字节流和字符流。
字符流和字节流的区别:
在大多数情况下,字节是数据最小的基本单位,用于处理字节数据的流对象就是字节流。在最一开始是没有字符流,只有字节流。因为我们面对计算机通常都是要操作文字的,美国为了让计算机识别他们的文字,就规定了一个字节的数字对应一个英文字母或者符号,这张编码表就是ASCII码表,它用一个字节来存储,8位一共能表示2^8=256个符号编码。这对英语来说已经足够用了。后来其他的国家的语言德语、法语、西班牙语之类的也分别定义了属于自己语言的特殊编码标准,产生了很多张码表。但到了中日韩三国的语言文字上面,256个符号编码就太少了,收到的每个字节就不能简单的解码成一个字母了,而是需要好几个字节组合在一起解码成一个汉字,通常一个汉字由1个、2个或4个字节组成。为了标准化,国际标准组织把所有国家的文字都放在一张码表中,就产生了Unicode码。Unicode码使全世界每个不同语言的不同字符都统一编码。在一开始,每个字符占用2个字节,一共2^16=65536个字符空间,从第四版开始加入“扩展字符集”开始使用4个字节编码。
我们在操作文字的时候,用字节流读取了硬盘中的文字字节数据,然后不直接进行操作,而是去查阅编码表,这样将字节流解码成对应的文字再进行操作。为了方便应用,就将字节流和编码表一起封装成了对象,就是字符流。
IO包中有很多类,但他们的基础功能都是读和写,根据这个特性可以抽取出4个顶层基类
字节流:
1.InputStream 2.OutputStream
字符流:
1.Reader 2.Writer
1.FileWriter类
如果我们想向硬盘中存储文本文件,就需要用到操作文件的字符流的类FileWriter类,因为文件必须要有文件名和存储路径,所以这些信息都要在创建对象的时候指定好,作为FileWriter对象的参数传递进去,并且如果指定的存储路径不存在的时候,就会抛出异常。
当创建了FileWriter对象后调用writer()方法写入数据,是把写入的内容存储到临时存储缓冲区中,也可以理解为将数据保存在了流中,如果想要让内容从流中存储到硬盘里,就需要调用flush()方法进行刷新,将缓存的内容立即写入目标。在刷新后还可以继续写入。
当所有内容写入完成后就要关闭流,因为写入流占用着Windows资源,当调用close()方法时其实也调用了flush()方法,就是说在关闭流之前系统会自动刷新一次。如果关闭了流,继续调用write()或flush()方法,就会抛出异常。
FileWriter fw = new FileWriter("E://abc.txt");
fw.write("abcde");
fw.flush();
fw.write("fg");
fw.close();
//fw.write("higk");这句会报错 java.io.IOException: Stream closed
输出结果:
//E盘目录下会多出一个abc.txt的文本文件。
abcdefg
换行
换行采用的是和System类中的getProperties()方法
final String LINE_SEPARATOR = System.getProperty("line.separator");
FileWriter fw = new FileWriter("E://abc.txt");
fw.write("abcde"+LINE_SEPARATOR+"fg");
fw.close();
输出结果:
abcde
fg
续写
如果在FileWriter的构造函数中加入true,就可以实现对文件的续写
public FileWriter(String fileName, boolean append):append - 一个 boolean 值,如果为 true,则将数据写入文件末尾处,而不是写入文件开始处。
FileWriter fw = new FileWriter("E://abc.txt",true);
fw.write("abcdefg");
fw.close();
输出结果:
//执行一次
abcdefg
//执行两次
abcdefgabcdefg
2.IO异常处理
只要进行读写操作,一般都要处理IO异常
FileWriter fw = new FileWriter("E://abc.txt");
fw.write("abcd");
fw.close();
上面这三句话都有可能会出现异常情况
第一句可能指定的路径不存在
第二句可能在写入的过程中出错,比如在连续写入的时候遇到了硬盘坏道
第三局可能在关闭的过程中Windows报错
所以这三句话都有需要处理的异常,就需要try…catch。其中,close语句是一定要执行的,就要放在finally语句中,代码变成
try {
FileWriter fw = new FileWriter("E://abc.txt");
fw.write("abcd");
} catch (IOException e) {
System.out.println(e.toString());
}finally{
fw.close();
}
这样fw就变成了try代码块的局部变量,在finally代码块中是不识别的。所以就要在try…catch语句外面创建FileWriter的引用变量,然后在try..catch语句里面进行对象初始化,然后再对close语句单独进行异常处理。并且在处理close语句的时候,有可能会发生空指针异常,因为在某些情况下,FileWriter对象是没有被创建出来的,比如路径错误,这时候关闭动作是没有意义的,所以处理close语句的异常要加一个判断。
FileWriter fw = null;
try {
fw = new FileWriter("E://abc.txt");
fw.write("abcd");
} catch (IOException e) {
System.out.println(e.toString());
}finally{
if(fw!=null)
try {
fw.close();
} catch (IOException e) {
throw new RuntimeException("关闭失败");
}
}
变量在外面,
new在里面,
finally close在里面,
判断为空。
3.FileReader类
和Writer类类似,Reader类也有用来读取字符文件的便捷类,就是FileReader类。在创建FileReader类的时候就应该明确被读取的文件,所以将指定文件名作为构造函数参数进行传递。
读取单个字符
在Reader类中存在方法public int read():读取单个字符。返回字符的整数,如果已达流的末尾,则返回-1。
在输入流执行完毕后,也要关闭流,释放Windows资源。
FileReader fr = new FileReader("E:\\abc.txt");
int ch = 0;
while ((ch=fr.read())!=-1) {
System.out.println((char)ch);
}
fr.close();
输出结果:
a
b
c
d
e
读取字符数组
除了读取单个字符,Reader类还提供了方法用于按照数组读取字符。
public int read(char[] cbuf):将字符读入数组。返回的是读取的字符数,如果已到达流的末尾,则返回 -1。
在使用数组读取字符数据的时候,返回的是读到的字符数。数组是有固定长度的,这个长度由我们自己定义,当数组装满后就不再装了,下一次读取的时候会覆盖上一次存在数组中的数据。如果最后一次读取数组中没有存满,后面没有保存的角标位会依然保存上一次读到的数据。如果再读取因为已经达到流的末尾,就返回-1。
比如一个文本内容是abcde,第一次读完数组是abc返回3,第二次读完数组是dec返回2,第三次读完数组是dec返回-1。
char[] chs = new char[3];
int len = 0;
while ((len = fr.read(chs))!=-1) {
System.out.println(new String(chs,0,len));
}
输出结果:
abc
de
一般情况下,数组的长度我们定义为1024。
4.缓冲区
缓冲区概念的提出
现在我们有一个需求就是把E盘下的abc.txt文件复制到F盘中
如果读写都用数组来实现,代码如下:
FileReader fr = null;
FileWriter fw = null;
try{
//创建输入流和输出流
fr = new FileReader("E:\\abc.txt");
fw = new FileWriter("F:\\abc.txt");
char[] cbuf = new char[1024];//创建一个临时容器,用于缓存读取到的字符
int len = 0;//记录读取到的字符数
while((len=fr.read(cbuf))!=-1){
fw.write(cbuf, 0, len);
}
}catch(Exception e){
throw new RuntimeException("读写失败");
}finally{
if(fr!=null)
try {
fr.close();
} catch (IOException e1) {
e1.printStackTrace();
}
if(fw!=null)
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
在这里,数组其实就是起到了缓存的功能。本来数据存储到硬盘中,如果想要复制,就是从源中获取一个字符,然后写入到目的。然后再从源中获取一个字符,再写入到目的。硬盘的盘片是不断旋转的,不管是获取还是写入都是需要寻找的,每个字符都重复同样的操作很没有效率。所以先创建一个数组缓冲区,从源获取到字符后先存入到缓冲区中,当缓冲区存满后再一次性向目的中写入,这样做就提高了效率。数组在这里就是一个缓冲区。
BufferedWriter类
在刚才的例子中,数组缓冲区是我们自己new出来的,其实,java对这种提高效率的方法已经将他们封装成了对象,我们直接用就好了。
BufferedWriter:将文本写入字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。
缓冲区本身其实是封装了数组的对象,用数组来缓存流所操作的对象。因为缓冲区是为了提高效率而存在的,所以它必须要有所操作的对象,也就是流对象。
FileWriter fw = new FileWriter("E:\\IO.txt");
//为了提高效率,创建缓冲区。因为这里是写,所以创建了一个字符输出流的缓冲区对象,并与流对象相关联,然后操作的就都是缓冲区的方法了。
BufferedWriter bufw = new BufferedWriter(fw);
//使用缓冲区的方法将数据写入到缓冲区中
bufw.write("abcdefg");
bufw.newLine();//缓冲区给我们提供行分隔符的方法,就不用我们再自己写行分隔符了。
bufw.write("higk");
//将缓冲区中的数据刷新到目的地中
bufw.flush();
//关闭缓冲区,因为缓冲区也就是一个数组,并没有调用Windows底层资源,关闭缓冲区实际上就是关闭流。
bufw.close();
输出结果:
abcdefg
higk
BufferedReader类
BufferedReader:从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。
public String readLine():读取一个文本行。如果已到达流末尾,则返回 null 。
FileReader fr = new FileReader("E:\\IO.txt");
//创建一个字符输入流的缓冲区对象
BufferedReader bufr = new BufferedReader(fr);
String line = null;
while ((line= bufr.readLine())!=null) {
System.out.println(line);
}
bufr.close();
输出结果:
abcdefg
higk
sdfs
cvvbawsfgvfadbsbsbsbsdf
readLine方法原理
我们查看API文档时发现,BufferedReader类中重写了read()方法读取单个字符和读取数组中某一部分,但是读取数组的方法却是使用Reader类中继承来的。
因为如果我们在硬盘上存有一段二进制的字符数据,如果我们想要获取这些字符数据,一种方法是逐个获取字符,然后逐个输出字符,但是这样很没有效率。所以我们在内存中创建一个数组,就是创建一个缓冲区对象,然后调用缓冲区对象的bufr.read(buf)方法,把获取到的字符先存放在数组中,这里的read方法其实就是继承自Reader类的读数组方法。当数组存满后就不再存了,然后再调用缓冲区的read()方法把数组中的内容输出到IO设备中,因为Reader类的read()方法操作的是硬盘的数据,而BufferedReader类中的read()方法操作的是缓冲区中的数据,所以BufferedReader类中的read()方法需要覆盖Reader类中的方法。到这里就已经可以操作从缓冲区中读取字符了,已经实现了高效率。
但是根据文本内容的行的特点,我们可以按照行进行读取,于是就有了readLine()方法。readLine()方法先调用缓冲区的read()方法从缓冲区中读取单个字符,然后判断是否是行结束标记,如果不是就把读到的字符存到临时容器中,如果是行结束标记,就不向临时容器中存储。最后这个临时容器中存储的就是一行的内容(不包含任何行终止符)。然后将这些有效内容变成字符串返回,这就是readLine方法的底层原理。
//使用缓冲区复制文件
FileReader fr = new FileReader("E:\\IO.txt");
FileWriter fw = new FileWriter("F:\\IO.txt");
BufferedReader bufr = new BufferedReader(fr);
BufferedWriter bufw = new BufferedWriter(fw);
String str = null;
while((str = bufr.readLine())!=null){
bufw.write(str);
bufw.newLine();
bufw.flush();
}
bufr.close();
bufw.close();
装饰设计模式
装饰模式
对新房进行装修并没有改变房屋的本质,但它可以让房子变得更漂亮、更温馨、更实用。 在软件设计中,对已有对象(新房)的功能进行扩展(装修)。把通用功能封装在装饰器中,用到的地方进行调用。装饰模式是一种用于替代继承的技术,使用对象之间的关联关系取代类之间的继承关系。引入装饰类,扩充新功能。
其实装饰设计模式能够实现的功能使用继承都可以实现,但是使用继承的话,只要想要具备高效的类都要有一个子类来继承它然后实现高效,这样随着类越来越多,继承体系会变得越来越臃肿。
但是如果使用装饰设计模式,只要在类中有一个具备高效功能的装饰类,其他的类如果想要实现高效功能,只要把对象作为参数传递到装饰类就可以了,这样做比较灵活,不需要产生子父类关系。
但是需要注意一点,装饰类和被装饰类都必须所属同一个接口或者父类。
LineNumberReader类
在装饰类BufferedReader类下面还有一个子类,这个子类也是装饰类,它可以实现行号的设置和获取。
通过两个方法setLineNumber(int) 和 getLineNumber()可以分别设置行编号和获取行编号。
FileReader fr = new FileReader("E:\\IO.txt");
LineNumberReader lnr = new LineNumberReader(fr);
String line = null;
while ((line = lnr.readLine())!=null) {
System.out.println(lnr.getLineNumber()+":"+line);
}
输出结果:
1:abcdefg
2:higk
3:sdfs
4:cvvbawsfgvfadbsbsbsbsdf