Java笔记 - IO流 - 字符流

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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值