美文网首页
Java 基础(九)字符流

Java 基础(九)字符流

作者: Anonymous___ | 来源:发表于2017-10-23 18:16 被阅读0次

    终于结束了集合的学习,今天我们就开始学习 I/O的操作了。 I/O 系列的内容分为 I/O概述、字符流、字节流。今天要学的是 I/O和字符流的操作

    由于概述篇幅较短,所以就把概述压缩到这里来了。

    I/O 概述

    I/O:即 Input Output,输入输出的意思。

    • IO 流用来处理设备之间的数据传输。
    • JAVA 对数据的操作都是通过流的方式
    • JAVA 用于操作流的对象都在 IO 包里面
    • 流的操作分两种:字符流、字节流
    • 流的流向分两种:输入流、输出流

    对数据的操作,其实就是对 File 文件。我偷了一张祖师爷传下来的图来描述 IO 流类结构关系。

    从图中可以看出,都是从这以下四个类中派生出来的子类,子类的类型也好区分,后缀都是抽象基类名。

    • 字节流抽象基类
      • InputStream
      • OutputStream
    • 字符流抽象基类
      • Reader
      • Writer

    IOException

    IO 异常大致分为三种,一是 IO 异常、二是找不到文件异常、三是没有对象异常。

    因此,我们在异常处理的时候,比较严峻的写法应该这样

    FileWriter fileWriter = null;
    try {
        fileWriter = new FileWriter("demo.txt");
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } finally {
        try {
            if (fileWriter != null) {
                fileWriter.close();
                }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    

    字符流-Reader/Writer

    Reader 和 Writer的定义

    Reader: 用于读取字符流的抽象类。子类必须实现的方法只有 read(char[], int, int) 和 close()。但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。

    Writer: 写入字符流的抽象类。子类必须实现的方法仅有 write(char[], int, int)、flush() 和 close()。但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。

    字符流的读写FileReader 和 FileWriter。

    先看看基本运用吧~

    try {
            // 创建读取流和写入流
            FileReader fileReader = new FileReader("raw.txt");
            FileWriter fileWriter = new FileWriter("target.txt");
            // 读取单个字符,自动往下读
            int ch;
            while ((ch = fileReader.read()) != -1) {
                fileWriter.write(ch);
            }
                fileReader.close();
                fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    

    每次读取一个字符再执行写入操作,效率比较慢,我们可以尝试一次读取更多的数据再一次性写入。

    try {
            // 创建读取流和写入流
            FileReader fileReader = new FileReader("raw.txt");
            FileWriter fileWriter = new FileWriter("target.txt");
            // 读取1024个字符,自动往下读
            char[] buf = new char[1024];
            while (fileReader.read(buf) != -1) {
                fileWriter.write(buf);
            }
            fileReader.close();
            fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    

    以上两种方法实现了文件的读写操作,将 raw.txt 读取写入 targer.txt 文件里面。

    同时,这步操作也可以当成是文件的拷贝

    需要注意的是:

    • 读取文件时,如果路径对应的文件不存在会报 IOException
    • 写入文件时,如果路径对应的文件不存在会自动在该目录下创建文件

    如果要实现追加写入文件的操作,比如将 raw1.txt 和 raw2.txt 共同写入 target.txt 里面,只需要在创建 FileWriter 的时候使用两个参数的构造方法即可,如:FileWriter fileWriter = new FileWriter("target.txt",true);

    至于源码读写的实现,我简单说一下吧。其实 FileReader/FileWriter 的构造方法里面都有 new FileInputStream/FileOutputStream 的对象,然后继承的是InputStreamReader/OutputStreamWriter.这说明啥,我想大家心里应该有数了吧,其实还是调用了字节流的读取,然后使用StreamEncoder/StreamDecoder进行编码解码操作。

    具体流程是这样的,这里每次读/写太长了,我只说读取操作了,反正都是相对的。

    1. 首先创建 FileReader 对象,FileReader对象构造方法里面创建了FileInputStream。然后再使用 FileInputStream 作为参数,创建 StreamDecoder对象。
    2. 调用FileReader.read()读取一个字符,实际上就是调用了 StreamDecoder 同时读取两个字节,再使用这两个字节组成一个字符返回。

    字符流的缓冲区

    字符流的缓冲区,提高了对数据的读写效率,他有两个子类

    • BufferedWriter
    • BufferedReader

    缓冲区要结合流才可以使用
    在流的基础上对流的功能进行了增强

    源码就不带着大家一起读了,我给大家分析一下 BufferedWriter 的思想。以下内容划重点,期末考试要考!

    要想理解 BufferReader,就要先理解它的思想。BufferReader 的作用是为其它Reader 提供缓冲功能。创建BufferReader 时,我们会通过它的构造函数指定某个Reader 为参数。BufferReader 会将该Reader 中的数据分批读取,每次读取一部分到缓冲中;操作完缓冲中的这部分数据之后,再从Reader 中读取下一部分的数据。
    为什么需要缓冲呢?原因很简单,效率问题!缓冲中的数据实际上是保存在内存中,而原始数据可能是保存在硬盘中;而我们知道,从内存中读取数据的速度比从硬盘读取数据的速度至少快10倍以上。
    那干嘛不干脆一次性将全部数据都读取到缓冲中呢?第一,读取全部的数据所需要的时间可能会很长。第二,内存价格很贵,容量不想硬盘那么大。

    通过字符流缓冲区来复制文件操作

    还用上面那个案例

    BufferedReader bufferedReader;
    BufferedWriter bufferedWriter;
    try {
        // 创建读取流和写入流
        bufferedReader = new BufferedReader(new FileReader("raw.txt"));
        bufferedWriter = new BufferedWriter(new FileWriter("target.txt", true));
        // 读取一行字符串
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            bufferedWriter.write(line);
        }
        bufferedReader.close();
        bufferedWriter.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    

    没什么特别的,很简单

    仿写一个 readLine

    上文中,出现了一个 readLine 方法,可以一次读取一行字符串。
    其实这个一次读取一行字符串还是蛮有用的,比如说读取一些Key-value 形式的配置文件。

    我们来看看 JDK 中关于 readLine 的描述

    读取一个文本行。通过下列字符之一即可认为某行已终止:换行 ('\n')、回车 ('\r') 或回车后直接跟着换行。
    返回:包含该行内容的字符串,不包含任何行终止符,如果已到达流末尾,则返回 null
    抛出:IOException - 如果发生 I/O 错误

    所以,我们自己要封装一个 readLine,只需要判断读取到的字符是否为'\n'、'\r',再一次性返回就行了。

    class MyBufferReaderLine {
    
        private Reader fr;
    
        public MyBufferReaderLine(Reader fr) {
            this.fr = fr;
        }
    
        // 一次读取一行的方法
        public String readLine() throws IOException {
    
            // 定义临时容器
            StringBuilder sb = new StringBuilder();
            int ch = 0;
            while ((ch = fr.read()) != -1) {
    
                if (ch == '\r' || ch == '\n') {
                    return sb.toString();
                } else {
                    sb.append((char) ch);
                }
            }
            if(sb.length() != 0){
                return sb.toString();
            }
            return null;
        }
    
        public void close() throws IOException {
            fr.close();
        }
    }
    

    代码实现很简单,就是参考 BufferedReader 写的一个包装类。

    LineNumberReader

    你们先感受一下这个类的用法

    FileReader fr;
    try {
        fr = new FileReader("test.txt");
        LineNumberReader lnr = new LineNumberReader(fr);
        String line;
        while ((line = lnr.readLine()) != null) {
            System.out.println(lnr.getLineNumber() + ":" + line);
        }
        lnr.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    

    这货和我们刚刚手写的MyBufferReaderLine 基本没啥区别,继承自BufferedReader ,然后多了一个lineNumber 属性,lineNumber用来记录当前行数。
    实现没有什么意义,我们在MyBufferReaderLine 上添加一个字段lineNumber,每次 readLine 成功之后 lineNumber++ 即可。

    但是,为什么要讲他呢,因为他和 BufferedReader 一样,也是一个包装类啊。

    包装类就是装饰设计模式啊~

    好了,你们都知道了,就是提一下装饰模式。

    装饰模式

    当想要对已有的对象进行功能增强时,可以定义一个类,将已有对象传入,并且提供加强功能,那么自定义的该类就称为装饰类。
    装饰模式又名包装(Wrapper)模式。装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。

    可能有的同学会问,那么为什么不用继承呢?我写一个新的类,继承、再添加扩展方法或者重写方法也可以实现呀。

    假如咖啡厅卖咖啡,运营可一段时间,发现客户对咖啡的甜度有不同的需求,有如下三种需求加少量糖、一般糖、多糖。代码实现可以给 Coffee 添加一个糖量的属性,但是一开始设计 Coffee 这个类的时候没有加这个属性,根据开发守则,我们是不应该去修改原 Coffee 类,此时可以选择新增三个子类,LowSurgeCoffee、MidSurgeCoffee、HightSurgeCoffee,或者使用装饰模式,添加3个不同糖量的 SurgeDecorator。此时,使用装饰模式和继承没什么区别。但是运行了一段时间之后,需求又加了,咖啡需要新增口味卡布奇诺和摩卡。此时再组合之前的三种糖量,一共需要9个咖啡类。但是如果使用装饰模式,只需要新增摩卡和卡布奇诺装饰器就行了。一共6个装饰类。之后再扩展新的口味需要的子类是乘算,但是如果是装饰类,就只是加算。

    以上这个例子没有代码实现,因为我懒。。。。。。

    针对的问题:

    动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。不改变接口的前提下,增强所考虑的类的性能。

    何时使用
    • 需要扩展一个类的功能,或给一个类增加附加责任。
    • 需要动态的给一个对象增加功能,这些功能可以再动态地撤销。
    • 需要增加一些基本功能的排列组合而产生的非常大量的功能,从而使继承变得不现实。
    优缺点
    • 装饰者模式比继承要灵活,避免了继承体系的臃肿,而且降低了类与类之间的关系
    • 装饰类因为增强已有对象,具备功能和已有的想相同,只不过提供了更强的功能,所以装饰类和被装饰类通常属于一个体系中的

    相关文章

      网友评论

          本文标题:Java 基础(九)字符流

          本文链接:https://www.haomeiwen.com/subject/yigauxtx.html