在上一篇,咱们知道了 Java IO 的根本运用,从本篇开始,咱们一起来探求规划形式是怎么在 Java IO 中运用的。首要,咱们先要学习的是装修器形式。

我相信,友好的评论交流会让互相快速进步!文章难免有疏漏之处,非常欢迎咱们在评论区中批评指正。

什么是装修器形式

装修器形式经过组合代替承继的办法在不改变原始类的情况下增加增强功用,首要解决承继联系过于杂乱的问题(Java IO 就归于这种杂乱情况)。

刚上来,咱们先知道装修器是干啥的,解决啥问题就行,详细的是怎么做的,咱们边剖析边说。

Java IO 庞大的类库

Java IO 的类库非常庞大,有 40 多个类,担任 IO 数据的读取和写入。咱们能够从以下视点将其划分为四类,详细如下:

(笼统基类) 字节省 字符流
输入流 InputStream Reader
输出流 OutputStream Writer

针对不同的读取和写入场景,Java IO 又在四个父类根底上,扩展了许多子类。详细如下(只列举了一些常用的类):

Java面试必知必会 —— 全面解读 Java IO (装饰器模式篇)

Java IO 流的嵌套用法

还记得咱们在 Java IO 根底篇中流的运用事例吗?若要运用缓存字节输入流,咱们需要在 BufferedInputStream 的结构函数中传递一个 FileInputStream 目标来运用(这便是,运用 BufferedInputStream 增强 FileInputStream 的功用)。详细如下:

try (BufferedInputStream bis =
     	new BufferedInputStream(new FileInputStream("test.txt"))) {
    byte[] b = new byte[128];
    while (bis.read(b) != -1) {
        // ...
    }
}

或许,你可能想为什么 Java IO 不规划一个承继 FileInputStream 并且支撑缓存的 BufferedFileInputStream 类呢?

假如是这样的话,咱们岂不是能够直接创立一个 BufferedFileInputStream 类目标,支撑缓存并且能够翻开文件读取数据,这样多省劲简略啊。

try (InputStream in = new BufferedFileInputStream("test.txt")) {
   byte[] b = new byte[128];
   while (bis.read(b) != -1) {
        // ...
    }
}

咱们的这种思路便是根据承继的规划计划了。

根据承继的规划计划

假如说 InputStream 只要一个子类 FileInputStream 的话,那么咱们在 InputStream 根底上,再规划一个孙子类 BufferedFileInputStream,也算是能够,究竟承继结构比较简略,能够承受。

可是,事实上,咱们在上面的常用类图中也看到了,承继 InputStream 的子类非常多,那么咱们就需要给每一个 InputStream 子类都派生一个支撑缓存读取的子类,这数量太庞大了!

并且,支撑缓存仅仅拓宽功用之一,咱们还要对其他功用进行增强,比如 DataInputStream 类,它支撑依照一切 Java 根本数据类型来读取数据。

try (DataInputStream dis = new DataInputStream(new FileInputStream("test.txt"))) {
    int data = dis.readInt();
}

假如咱们持续依照承继的办法来完结的话,那咱们就需要派生出 DataFileInputStreamDataPipedInputStream 等类。

假如咱们还需要既支撑缓存、又支撑依照根本数据类型读取的类,那就要再持续派生出 BufferedDataFileInputStreamBufferedDataPipedInputStream 等超多的类。

现在仅仅附加了两个增强功用,假如要增加更多增强功用,那就会导致类数量爆破,类的承继结构将变得无比杂乱,代码既欠好拓宽,也欠好保护。

那有没有什么办法能够解决这个问题呢?当然有,咱们能够运用组合(composition)和托付(delegation)到达承继行为的效果。这种计划契合规划准则:多用组合,少用承继

根据承继的规划计划,一切的子类都会承继到相同的行为。而运用组合和托付,咱们能够动态地组合目标,能够写新的代码增加新的功用,而无需修正现有代码,引进 bug 的机会将大幅削减。这也契合另一个规划准则:开闭准则,类应该对扩展敞开,对修正关闭。

根据装修器形式的规划计划

装修器形式的标准类图

因为运用承继完结的结构过于杂乱,Java IO 采用了根据装修器形式的规划计划。咱们先来看看装修器形式的标准类图是什么姿态的。

Java面试必知必会 —— 全面解读 Java IO (装饰器模式篇)

从类图视点剖析 Java IO 是怎么运用装修者形式的

首要咱们先从类图的视点来看看 Java IO 是怎么运用装修者形式的。

Java面试必知必会 —— 全面解读 Java IO (装饰器模式篇)

源码视点剖析 Java IO 是怎么运用装修者形式的

咱们再从源码的视点去检查 Java IO 是怎么运用装修者形式的。

InputStream(笼统组件)

下面是简化后的 InputStream 源码,它是一个笼统类,作为一个笼统组件。咱们详细看 read() 办法。

public abstract class InputStream {
	// ...
    public abstract int read() throws IOException;
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }
    public int read(byte b[], int off, int len) throws IOException {
        // 详细的完结逻辑
    }
    //...
}

FileInputStream (详细组件)

FileInputStream 承继自 InputStream,有公有的结构办法能够直接运用,也能够被装修者包起来运用。 功用函数的完结逻辑与 InputStream 的完结逻辑不同,是新的行为。

public class FileInputStream extends InputStream {
    public FileInputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null);
    }
    public FileInputStream(File file) throws FileNotFoundException {
        // 代码略...
    }
    public FileInputStream(FileDescriptor fdObj) {
        // 代码略...
    }
    public int read() throws IOException {
        // 新行为,没有调用笼统组件的 read() 办法
    }
    public int read(byte b[]) throws IOException {
        // 新行为,没有调用笼统组件的 read(byte b[]) 办法
    }
    public int read(byte b[], int off, int len) throws IOException {
        // 新行为,没有调用笼统组件的 read(byte b[], int off, int len) 办法
    }
}

FilterInputStream(笼统的装修者)

下面是 FilterInputStream 源码,它承继了 InputStream,作为一个装修者,它保存了笼统组件的引证。结构函数声明为 protected,标明用户不能直接结构该类的目标,只能结构该类的子类目标。

FilterInputStream 没有对 InputStreamread() 进行增强,可是仍是将其从头完结了一遍,简略地包裹了对 InputStream 目标的函数调用,托付给传递进来的 InputStream 目标来完结。

请有必要检查代码中的要害注释!

public class FilterInputStream extends InputStream {
    protected volatile InputStream in; // 保存笼统组件组件的引证
    // 结构函数声明为 protected
    // 标明用户不能直接结构该类的目标,只能结构该类的子类
    protected FilterInputStream(InputStream in) {
        this.in = in;
    }
    // 直接调用了笼统组件的 read() 办法
    public int read() throws IOException {
        return in.read(); // 托付给传递进来的 InputStream 目标来完结
    }
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }
    // 直接调用了笼统组件的 read(byte b[], int off, int len) 办法
    public int read(byte b[], int off, int len) throws IOException {
        return in.read(b, off, len); 
    }
}

BufferedInputStream(详细的装修者)

BufferedInputStream 承继了 FilterInputStream,作为一个详细的装修者,它增强了 read() 的功用,增加了缓存功用。请有必要检查代码中的要害注释!

public class BufferedInputStream extends FilterInputStream {
    public BufferedInputStream(InputStream in) {
        this(in, DEFAULT_BUFFER_SIZE);
    }
    public BufferedInputStream(InputStream in, int size) {
        super(in); // 记载装修者所包着的笼统组件
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
    // 在旧办法的根底上完结了缓存功用
    public synchronized int read() throws IOException {
        if (pos >= count) {
            fill();
            if (pos >= count)
                return -1;
        }
        return getBufIfOpen()[pos++] & 0xff;
    }
    // 在旧办法的根底上完结了缓存功用
    public synchronized int read(byte b[], int off, int len)
        throws IOException
    {
        // ... 详细逻辑省掉
    }
}

PushbackInputStream(详细的装修者)

BufferedInputStream 相同,承继了 FilterInputStream,它增加了一种在读取输入流时将数据“推回”流中的功用,然后能够从头读取该数据。请有必要检查代码中的要害注释!

public class PushbackInputStream extends FilterInputStream {
	public PushbackInputStream(InputStream in) {
        this(in, 1);
    }
    public PushbackInputStream(InputStream in, int size) {
        super(in); // 记载装修者所包着的笼统组件
        if (size <= 0) {
            throw new IllegalArgumentException("size <= 0");
        }
        this.buf = new byte[size];
        this.pos = size;
    }
    // 功用增强
    public int read() throws IOException {
        ensureOpen();
        if (pos < buf.length) {
            return buf[pos++] & 0xff;
        }
        return super.read(); // 旧的办法
    }
    // 功用增强
    public int read(byte[] b, int off, int len) throws IOException {
        // 省掉了部分代码,这些代码用于增强...
        if (len > 0) {
            len = super.read(b, off, len); // 旧的办法
            if (len == -1) {
                return avail == 0 ? -1 : avail;
            }
            return avail + len;
        }
        return avail;
    }
}

从上面的代码能够知道,为了避免代码重复,Java IO 笼统出来一个装修者父类 FilterInputStreamInputStream 的一切详细的装修器类(BufferedInputStreamDataInputStreamPushbackInputStream)都承继自这个装修器父类。详细的装修器类只需要完结它需要增强的办法就能够了,其他办法都承继装修器父类的默许完结。

装修器形式的代码结构

咱们将上述的内容整理出来一个代码结构,详细如下所示:

// 笼统类也能够替换成接口
// 笼统组件
public abstract class Component {
    void f();
}
// 详细组件
public class ConcreteComponent {
    public ConcreteComponent() {}
    public void f() {
        // 新的完结逻辑
    }
}
// 笼统装修器(详细装修器的父类)
public class Decorator extends Component {
    protected Component c; // 组合
    // 无法结构自己的目标,只能结构自己的子类目标
    protected Decorator(Component c) {
        this.c = c;
    }
    public void f() {
        c.f(); // 托付
    }
}
// 详细装修器
public class ConcreteDecoratorA extends Decorator {
    public ConcreteDecoratorA(Component c) {
        super(c); // 经过结构器组合笼统组件
    }
    public void f() {
        // 功用增强代码
        super.f(); // 托付
        // 功用增强代码
    }
}

疑问解答时刻

为什么装修器形式仍是用到承继了呢,不是说要运用组合取代承继吗?

在之前的根据承继的规划计划中,咱们谈到运用承继的计划会导致类数量爆破,类的承继结构将变得无比杂乱,代码既欠好拓宽,也欠好保护。

在装修器形式中,运用承继的首要目的是让装修器和笼统组件是相同的类型,也便是要有一起的超类,也便是运用承继到达「类型匹配」,而不是运用承继取得「行为」。

当咱们将装修器与组件组合时,便是在参加新的行为。这种新的行为并不是承继自超类,而是由组合目标得来的(在代码结构中已给出了清晰注释)。

别的,假如是根据承继的规划计划,那么一切的类的行为只能在编译时静态决定,也便是说,行为不是来自于超类,便是子类掩盖后的版本。假如需要新的行为,有必要修正现有的代码,这不契合敞开关闭准则。

而在装修器形式中,咱们运用组合,能够把装修器混合运用,并且,能够在任何时候,完结新的装修器增加新的行为。

为什么 Component 规划成一个笼统类,而不是一个接口呢?

通常装修器形式是采用笼统类,在 Java 中也能够运用接口。文中给出的代码结构是从源码中提取而来的。

总结一下

装修器形式首要用于解决承继联系杂乱的问题,经过组合和托付来代替承继。

装修器形式的首要作用便是给组件增加增强功用,能够在组件功用代码的前面、后面增加自己的功用代码,甚至能够将组件的功用代码完全替换掉。

装修器和详细的组件都承继相同的笼统类或接口(组件),所以能够运用无数个装修器包装一个详细的组件。

装修器形式也是有问题的,它会导致规划中出现许多小类,假如过度运用,会让程序变得很杂乱。

练习题

现在咱们已经知道了装修器形式,也看过 Java IO 的类图和源码,那么接下来咱们来编写一个自己的输入装修器吧。

需求

编写一个装修器,把输入流内的一切大写字符转成小写。比如,”HELLO WORLD!”,装修器会把它转换成 “hello world!”

代码完结

首要,咱们得扩展 FilterInputStream,这是一切 InputStream 的笼统装修器。

咱们有必要完结两个 read() 办法,一个针对字节,一个针对字节数组,把每个大写字符的字节转成小写。

public class LowerCaseInputStream extends FilterInputStream {
    public LowerCaseInputStream(InputStream in) {
        super(in); // 保存 FilterInputStream 的引证
    }
    // 处理字节
    public int read() throws IOException {
        int c = super.read();
        return (c == -1 ? c : Character.toLowerCase((char) c));
    }
    // 处理字节数组
    public int read(byte[] b, int off, int len) throws IOException {
        int result = super.read(b, off, len);
        for (int i = off; i < off + result; i++) {
            b[i] = (byte) Character.toLowerCase((char) b[i]);
        }
        return result;
    }
}

测验一下

public class InputTest {
    public static void main(String[] args) {
        int c;
        try (InputStream in =
                     new LowerCaseInputStream(
                             new BufferedInputStream(
                                     new FileInputStream("test.txt")))) {
            while ((c = in.read()) != -1) {
                System.out.print((char) c);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

test.txt 文件中保存着 “HELLO WORLD!”

运转成果如下:

hello world!

好啦,以上便是本篇文章的全部内容了。咱们讲解了什么是装修器形式,装修器形式的标准类图、代码结构,知道了 Java IO 是怎么运用装修器形式的。

希望以上内容对你有协助,一起加油!

参考资料

  1. Head First 规划形式
  2. 规划形式之美
  3. Patterns in Java APIs