IO流与装饰器设计模式的理解

最近学习了IO流,可能理解的还不是特别好望大家见谅

下面我来叙述一下我对IO流的理解:

IO:对文件进行操作,是in和out,就是输入和输出

本质上是处理设备之间的数据传输,比如,读/写文件,网络通讯等

流:是一个抽象的概念,可以看作是交互的数据像水流一样在数据流通的管道里运输

假设要我们对一个文件进行读写操作时,我们会与数据架起一个通道,数据在通道里流过去~

有点丑,大家理解意思就好哈哈哈

而对于Java来说,IO流实际上是内存和持久化设备的交互,这里的持久化设备更多的应该是硬盘,后面我也就对硬盘来说了。

读:将数据从硬盘读到内存中

写:将数据从内存写到硬盘中

我们可以这样理解,对于内存来说,数据增加了就是读,数据减少了就是写,读是进,写是出

而对于流来说有几个特点:

一:先进先出

二:有顺序存储:按顺序读取和写入数据,不能随机访问数据

三:只能读或者写:对于读写操作一个流只能干一件事,如果需要读和写两个操作,我们就要创建两个流来操作

IO流的分类:

按数据:字节流和字符流

字节流:处理一切数据,二进制,图片,视频,文本等等,处理一切的数据

字符流:只能处理文本文件:.txt文件,不能处理文本文件以外的数据

对于文本来说,一个英文字母是1位字节,中文可能是2字节和3字节,而字符流可以直接区分每个字符,来进行读取。而字节流如果获取的字节数不对,可能出现乱码的清空,比如说:

好这个字,是3字节,如果字节流每次读取1字节,就会造成乱码

字符流是包装流

按流的方向:输出流和输入流

输入流:将数据读入到内存中

输出流:将数据写入到硬盘中

按功能:节点流和包装流

节点流:直接操作数据读写

包装流:基于节点流对其进行功能的拓展和强化

(别处找到的,大家将就看一下)

下面我们来实现一下IO流,如果对文件进行操作:

首先是FileInputStream、FileOutputStream(字节流)

import java.io.*;

public class FileInputStreamText {
    public static void main(String[] args) {
        File file;
        try {
            file = new File("C:\\Users\\yizhi\\Desktop\\a.txt");
            String content = read(file);
            System.out.println(content);
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static String read(File file) throws IOException {
        //创建读入流
        InputStream in = new FileInputStream(file);
        //创建缓冲区数组,一字读取3字节
        byte[] bytes = new byte[3];
        //字符串拼接
        StringBuilder string = new StringBuilder();
        int length;
        //read方法返回读取的数量,length用来接收read返回值,判断是否读入完毕
        while ((length = in.read(bytes)) != -1){
            //字符串拼接,将byte数组转为字符串
            string.append(new String(bytes,0,length));
        }
        in.close();
        return string.toString();
    }
}

首先创建一个File对象,里面存储着文件的绝对路径

(File对象大家可以去看看大佬们的讲解,其实就是文件的删除建立等,不是很难,这里我就不详细说了,大家只需要知道这是一个文件对象就好)

下面我们调用read方法,也就是IO流的数据读取

首先需要创建节点流对象,基于上面的图我们可以看到,对于节点流来说

有两个父类抽象类,一个是InputStream(读),另一个是OutputStream(写)

所以这里是多态的体现,父类型指向子类型FileInputStream,实际指向的是文件的读,这个流

将本次的文件传入到流中

下面创建一个数组,充当缓冲区,下面再说一下什么是缓冲区,现在可以看成创建了一个byte数组长度为3

然后构建一个字符串对象,用于字符串的拼接

下面使用while循环进行数据的遍历读取,节点流的read方法返回读取的数量,length用来接收read返回值,判断是否读入完毕,我们可以这样理解,如图:

假设我们本次读取的文本内容时,abcde,当调用read方法时,指针会指向第一个字节a,并将其放入到数组中,然后指针会移动一位指向b,当再次调用read方法时,将b放入数组中,指针再次移动一位。

 当数组填满时,会将整个数组返回,然后重复刚刚的过程直到文件中没有数据可读

我们看下结果:

可以看到确实是这样的,每次都会返回一个遍历数据的数组

最后一定要记得,将流关闭

下面我们看一下输出,将数据写到内存的流

public class FileOutputSteamText{
    public static void main(String[] args) {
        File file;
        try {
            file = new File("C:\\Users\\yizhi\\Desktop\\a.txt");
            write(file);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void write(File file) throws IOException {
        //创建写入流 FileOutputStream
        OutputStream out = new FileOutputStream(file);
        String data = "大家好";
        out.write(data.getBytes());
        out.close();
    }
}

和刚刚读的操作是一样的,只是创建的流不一样了,这次我们创建的是输出节点流,一样的将文件路径传入,本次我们要写入的内容是,大家好,下面调用write方法,将该字符串首先进行byte数组的转换,因为我们要知道,数据本质是二进制,将字符串变成byte数组的过程其实是编码的过程,最后关闭流即可:

我这里就不去讲字符流的相关操作了,过程是一模一样的,只是创建的流不一样。

下面我按照我的理解说一下什么是缓冲操作:

普通流每次读写一个字节,而缓冲流在内存中设置一个缓存区,缓冲区先存储足够的待操作数据后,再与内存或磁盘进行交互。这样,在总数据量不变的情况下,通过提高每次交互的数据量,减少了交互次数。

我们要知道,我们每次读写是和硬盘在交互,交互次数越多,效率越低,所以就有了缓存的概念

举个例子:假如我们现在有一堆砖要从前院运到后院,我们需要一趟搬一块,需要50多个来回,可是我们不会这么做,我们一般是准备一个推车,将砖全部放到推车上,最后一起推到后院即可,只需要一个来回,这个效率是很高的。

而在上面的使用中,数组其实就是充当了缓存区,小推车这个作用

下面我们使用一下BufferedInputStream:

我们看一下源码:

可以看到BufferedInputStream本质上是创建了一个大数组,将数据读取到这个大数组中,而我们自己新建的数组,不再于硬盘交互,而是于BufferedInputStream创建的大数组交互,从这里读取数据

而BufferedInputStream的使用于节点流对象没有什么差别,差别只有是包装类:

import java.io.*;

public class BufferedStreamTest {
    public static void main(String[] args) {
        File file;
        try {
            file = new File("C:\\Users\\yizhi\\Desktop\\a.txt");
            String content = read(file);
            System.out.println(content);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private static String read(File file) throws IOException {
        //创建缓冲流,缓冲流为包装流,需要在里面创建一个节点流使用
        InputStream in = new BufferedInputStream(new FileInputStream(file));
        byte[] bytes = new byte[3];
        StringBuilder string = new StringBuilder();
        int length;
        while ((length = in.read(bytes)) != -1){
            string.append(new String(bytes,0,length));
        }
        return string.toString();
    }
}

我们可以看到使用时没有任何改变的,只有在创建流的时候有点不同,我们需要创建BufferedInputStream流中先创建一个节点对象,这就是装饰器的特点。

我觉得常用的目前常用的流可能就是这几个,比如标准读,标准输出,压缩流,转换流等等其实用法是大差不差的,就是根据情况转换,我目前还没遇见这几个流的使用,大家如果详细了解其他流的使用场景可以去看看别的大佬写的。

下面我说一下我对装饰器的理解

什么是装饰器

也称为包装模式 是指在不改变原有对象的基础之上,将功能附加到对象上

我们来简单举个例子:

还记得多态,接口,抽象类和继承的使用吗,顺便复习一下

比如我们现在有一个宠物猫和一个宠物狗,我们将它们两个抽象成动物,猫和狗都有名字和行为,接口来规定行为,结构如下:

再来墨迹一下,猫狗是真实存在的对象,我们将猫狗共同的特征和行为抽象出来,提取出一个抽象类为动物,并且都实现一个行为,为吃东西。

这个例子很简单,我就不再去着重讲了,下面我们思考一个问题,如果我们现在有一个对狗有一个要求,我需要狗在吃饭之前先刷牙,请问我们有几种方式实现:(我目前掌握的,不是全部的)

第一种:重写狗这个子类的eat方法,在原有的代码上增加功能

第二种:在狗的子类下再创建一个类继承狗,并重写父类狗的eat方法

在解除包装类之前,我可能会选择第二种,因为这样不违背OCP原则,并没有对原有代码进行更改,而是新增了一个类,可是现在我又有一个需求,我需要让猫也刷牙,再吃饭,我们是不是需要再增加一个类,去重写猫的eat方法

我们现在是两个宠物,可以这样做,那么如果我们有100只种类的宠物呢,哈哈哈假设你是动物园的管理员,我们需要重写100个宠物的eat方法,会死人的哈哈哈,简称类爆炸

那么我们这时候需要一个功能,能不能只写一个模块,将对象放进去,对该方法进行统一的重写,是不是就完成了目标,解决了类爆炸的问题,这时候,需要引入一种设计模式:装饰器模式

下面我们回到这个例子,我们想让动物在吃饭之前刷牙,我们只需要将每个动物对象丢入到包装器中。

下面我们来实现一下:

Animal动物类:

public abstract class Animal implements Action{

    public String name;

    public Animal(String name) {
        this.name = name;
    }
    
    abstract public void eat();
}

很简单的抽象父类,实现了Action接口,让子类去实现吃的行为

Action接口:

public interface Action {
    void eat();
}

不多说了,定义了,吃,这个行为

Cat子类:

public class Cat extends Animal{

    public Cat(String name) {
        super(name);
    }

    @Override
    public void eat() {
        System.out.println(super.name + "在吃鱼");
    }
}

继承父类,并实现吃的行为

Dog子类:

public class Dog extends Animal{

    public Dog(String name) {
        super(name);
    }

    @Override
    public void eat() {
        System.out.println(super.name + "在吃骨头");
    }
}

没什么区别和猫类,就是变成了吃骨头

测试类:

public class DecoratorText {
    public static void main(String[] args) {
        Animal cat = new Cat("小猫");
        Animal dog = new Dog("小狗");
        cat.eat();
        dog.eat();
    }
}

没什么问题

我们下面来实现如何让它们吃饭之前先刷牙

我们先来了解一下装饰器的结构
装饰(Decorator)模式中的角色:

抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
具体构件(Concrete Component)角色 :实现抽象构件,通过装饰角色为其添加一些职责。
抽象装饰(Decorator)角色 : 继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

我来解释一下我的理解:

抽象构件:就是被扩展的行为或者对象,在这个例子中是action这个接口中,eat这个动作,我们想扩展eat这个东西,在吃之前,刷牙。

具体构件:就是一个具体的对象,谁在吃,在这个例子中是,猫和狗两个具体的对象

抽象装饰:其实本质上也是一个抽象类,继承接口中的行为,指定一个对象去实现具体的内容,本质上还是对,猫和狗的eat进行重写,只不过交给具体装饰去实现

具体装饰:就是对eat功能的扩展实现,在这里对eat方法进行重写

如果有点懵,看下代码:

抽象构件:

public interface Action {
    void eat();
}

就是接口中的方法,对其进行扩展 

具体构件:

public class Dog extends Animal{

    public Dog(String name) {
        super(name);
    }

    @Override
    public void eat() {
        System.out.println(super.name + "在吃骨头");
    }
}

我们需要对该对象的功能进行扩展 

抽象装饰器:

public abstract class Decorator implements Action{

    public Action action;

    public Decorator(Action action) {
        this.action = action;
    }

    @Override
    abstract public void eat();
}

我们可以看装饰器类去重写吃这个行为,首先需要在装饰器内创建其实现对象的属性,也就是Action这一接口,并在构造函数中去让其指向传入的对象

Animal dog = new Dog(“小狗”)

dog指向Dog类,这时多态的体现

Action action = dog  ---->   Action action = new Dog(“小狗”)

看到这里我们应该明白,最终action指向的其实是Dog类

下面是对eat内容的具体实现

下面是 具体装饰:

public class ConcreteDecoratorBrush extends Decorator{

    public ConcreteDecoratorBrush(Action action) {
        super(action);

    }
    Animal animal = (Animal) action;

    public void before(){
        System.out.println(animal.name + "先刷牙");
    }
    public void after(){
        System.out.println("吃饱了");
    }

    @Override
    public void eat() {
        before();
        action.eat();
        after();
    }
}

在具体装饰里我们对eat进行了功能扩展,首先执行父类的构造函数,下面对action进行向下转型,如果前面理解了,我们是知道,action指向的是Dog类,那一定是可以转型到Dog的父类Animal的,获取对象的名字,通过父类指向子类获取对象的名称。

然后重写eat方法对其进行扩展,先刷牙,然后吃饭dog.eat,最后吃饱了。

让我们看一下测试程序:

public class DecoratorText {
    public static void main(String[] args) {

        Animal dog = new Dog("小狗");

        dog.eat();
        System.out.println("===============");

        Decorator decorator1 = new ConcreteDecoratorBrush(dog);
        decorator1.eat();
    }
}

我们先使用了多态进行了普通的eat执行,然后我们使用了装饰器,将dog对象传入进去,发现功能进行了扩展,如果是猫呢?我们再试一下

public class DecoratorText {
    public static void main(String[] args) {
        Animal cat = new Cat("小猫");
        cat.eat();

        System.out.println("===============");
        Decorator decorator1 = new ConcreteDecoratorBrush(cat);
        decorator1.eat();
    }
}

这次我们传入的是猫这个对象,发现同样进行了扩展,所以这里也证明了我们可以对多个对象进行同一种功能的扩展,我们完全没有对原有的代码进行任何的修改,也没有添加过多的子类,就实现了功能的增加,希望大家能理解这种方式,其实最终的本质,还是多态和继承

下面我再来写一个装饰器,让动物们先喝水再吃饭:

public class ConcreteDecoratorBrush extends Decorator{

    public ConcreteDecoratorBrush(Action action) {
        super(action);

    }
    Animal animal = (Animal) action;

    public void before(){
        System.out.println(animal.name + "先刷牙");
    }
    public void after(){
        System.out.println("吃饱了");
    }

    @Override
    public void eat() {
        before();
        action.eat();
        after();
    }
}

测试程序:

public class DecoratorText {
    public static void main(String[] args) {
        Animal cat = new Cat("小猫");
        cat.eat();

        System.out.println("===============");
        Decorator decorator = new ConcreteDecoratorDrink(cat);
        decorator.eat();
    }
}

又增加一种行为,本质还是对吃这个动作,进行了功能扩展

好了希望大家能理解装饰器的设计模式,还有IO流的使用,IO流我并没有去详细讲述每种流应该什么时候使用,大家去看看别的大佬写的,很详细,我这里只是讲述我的理解,如果有错误的地方,希望大家的指出!谢谢大家!

  • 32
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值