原文链接:https://blog.csdn.net/zl1zl2zl3/article/details/72763024
在学习java.io.*包的时候,InputStream那一群类很让人反感,子类繁多就不用说,使用起来非常奇怪。我们想以缓存的方式从文件中读取字节流。总要先创建一个FileInputStream,然后把它放入BufferedInputStream构造函数中去创建BufferedInputStream。完成这些工作后才能开始读取文件。
为什么我们不能直接以缓存方式(BufferedInputStream)从文件中读取数据呢?今天我带着这样的问题走进InputStream的家,对老人家进行一次采访, 希望他能解决我心头的疑惑。
我:老人家您好,我用InputStrem用了很久了, 一直以来都有一个问题困扰着我, 想请教您一下,为什么我们想以带缓存的方式从文件中读取字节流需要创建FileInputStream和BufferedInputStream两个类,这太麻烦了,你看看人家Python, Ruby, 都是打开文件后直接就可以读了,多方便啊!
InputStream:年轻人,不要有情绪 ! 存在的就是合理的, 这其实是一个很长的故事, 要从很久很久以前说起。那时候Java刚刚诞生,我也幸运地被创造出来。那时候帝国实施严格的计划生育,我还没有任何子孙,很是寂寞。一天,有一个叫小霍的年轻人找到了我。他说他要让我飞黄腾达,子孙满堂。
我:这么神?那您讲讲您和小霍之间的故事吧。
时光倒流回20年前,小霍初见年轻的InputStream。
小霍:InputStream先生你好,我是JAVA帝国计划生育委员会的工作人员,组织上听说你孤苦伶仃的,很是于心不忍,又给你争取了好几个生育名额, 让你像Java集合框架那样儿孙满堂。
InputStream:真的吗? 我知道争取生育名额很不容易,那你说说,组织准备让我生几个?
小霍:你是IO的输入类,负责读取数据(字节流)。数据就是你的包裹,你一般从哪些渠道获得包裹?
InputStream:文件,字节数组,StringBuffer,其它线程,对了还有已经被序列化的对象。
小霍: 那你先根据数据来源的渠道生5个孩子,老大叫FileInputStream,处理文件,老二叫ByteArrayInputStream,处理字节数组,老三叫StringBufferInputStream,处理StringBuffer,老四叫PipedInputStream,处理线程间的输入流,老五叫ObjectInputStream,处理被序列化的对象。
InputStream:万一有一个包裹里面有多个或者多种数据输入流呢。
小霍:那就再生一个SequenceInputStream,处理一个包裹里有多种数据来源的业务。还有其它问题吗?没问题我就回单位了。帝国刚建立,我们计划生育委员会掌管着全国的生育名额,我还忙着呢。你抓紧时间生孩子,有问题再找我。
InputStream:好咧,我这就关灯造人。
交流完毕后,小霍走了,我也抓紧时间把我6个孩子生了出来,为国家做贡献。InputStream的6个孩子齐心协力,处理了JAVA早期很多的输入业务。但是他们也面临了新的问题。没过多久。年轻的计生委员小霍再次找到了InputStream。
小霍:你那6个孩子都是能人啊,但是现在客户抱怨他们的工作还不够到位。
InputStream:我那6个孩子各司其职,工作勤勤恳恳,怎么还有人抱怨?
小霍:客户嘛,都比较挑剔。他们抱怨你们读取数据太慢了,尤其是你的老大FileInputStream每次读数据都慢死了。好多客户等待都超过了几秒了,还没把数据等回来。
InputStream:几秒很慢吗?
小霍:我们计算机都是以纳秒计时的,所谓世间一秒钟,机器已千年。那些客户头发都等白了。
InputStream: 读数据慢能怪我吗?这不是硬盘慢造成的吗?
小霍:是,是硬盘造成的,我们想一个办法让用户减少访问硬盘的次数。比如建一个buffer怎么样?用户需要的数据先让他们在buffer里面找,能找到就直接从buffer里返回,实在找不到再去硬盘里找。Buffer在内存里,内存可比硬盘快10万倍呢(内存在随机访问的速度上是硬盘的10万倍以上)。
InputStream:这办法好。客户抱怨的其他问题呢?
小霍:客户想要的数据类型都是int, long, String这样的JAVA基本类型,而你提供给他们的都是byte类型,还需要客户自己进行类型转换。客户觉得麻烦。还有一个问题,Stream里面读出来的数据就不能重新放回去,客户想要一个功能,能把读出来的数据再推回Stream里面。
InputStream:看来我得再生3个孩子: 拥有缓存的BufferedInputStream,把byte转换成JAVA基本类型的DataInputStream和回写数据到stream的PushbackInputStream。
小霍:老伙计,你糊涂了,不止3个。就拿FileInputStream 来说吧, 加上这三个功能就需要三个子类
Buffered + FileInputStream
Data+ FileInputStream
PushBack + FileInputStream
还有更大的问题,万一某个特殊的客户既想有数据回写,又想要输出int,long,String这样的数据,还有要缓存。 或者说他们只要三个功能中的两个,这样组合起来, 又需要4个子类
Buffered + Data + FileInputStream
Buffered + PushBack + FileInputStream
Data + PushBack + FileInputStream
Buffered + Data + PushBack + FileInputStream
换句话说, 仅仅是FileInputStream, 就需要7个子类, 你还有其他5个孩子呢! 总共需要6 * 7 = 42个子类, 我估计客户看到这么多子类,眼都花了。
InputStream:看来我们得想另外的办法。要不然我的家族就要“类爆炸”了,再说让我一下生那么多孩子,我也煎熬啊。
小霍:你玩过俄罗斯套娃没?一个实心的娃娃被各种各样娃娃外壳套着。一个实心娃娃先套一个学生的外壳,那么他就是学生了,如果我再在外面套一个运动员的壳,那么他就成了有运动员身份的学生。我们模仿这种形式,比如最里面的实心娃娃是处理文件读取的FileInputStream,外面套一个BufferedInputStream的壳,那么这个套娃就是带buffer的FileInputStream。如果再套一个DataInputStream,那么套娃就成了能输出int这样java 基本类型并且带buffer的FileInputStream。搭配由客户去决定,我们只需要提供套娃壳(新的3个功能类)和最里面的实心娃InputStream(InputStream的6个孩子)。
InputStream:这很巧妙,那如何实现这样一种设计呢?
小霍:这有2个关键点:
1. 既然套娃中一定有实心娃娃,那么套娃的壳的类必须包含一个实心娃。比如BufferedInputStream里面要包含一个InputStream,我们把实心娃娃通过BufferedInputStream的构造函数放进去,当然DataInputStream和PushbackInputStream也一样。
2. BufferedInputStream+实心娃娃InputStream组成的新套娃又可以当作DataInputStream的实心娃娃,那么我们让这些套娃的外壳BufferedInputStream,DataInputStream,BufferedInputStream都继承自InputStream类,这样才能实现新组成的套娃又可以被另外的套娃壳嵌套。这3个套娃壳有着共同的特点都是装饰实心娃娃,我们再在他们上层在抽象一个FilterInputStream,让FilterInputStream继承自InputStream,让FilterInputStream里面包含一个实心娃娃InputStream。以后所有的装饰类都从FilterInputStream继承。
InputStream:这样我也省事了,只需要再多生一个FilterInputStream,剩下的BufferedInputStream,DataInputStream,PushbackInputStream这样的装饰类都让FilterInputStream去生了。
小霍:对,加上FilterInputStream,你就有7个孩子了,跟葫芦娃一样。前面6个哥哥都是和数据源有关,7弟就是用来装饰6个哥哥。把数据源的InputStream类和装饰的InputStream整合在了一起。
InputStream:而且对于BufferedInputStream,DataInputStream,PushbackInputStream来说,我还是爷爷,想着他们叫我爷爷的样子,我心里就美滋滋的。
小霍:美得你,Java中无论多少次继承都是子类父类关系,没有爷爷这么一说。我把家谱给你。你儿子太多,我就画ByteArrayInputStream,FileInputStream 和FilterInputStream的简化版。
小霍:这样的设计既避免了类爆炸,又可以让用户自己去搭配核心类和装饰类。而且也满足了一个重要的设计原则:开闭原则。这是指导思想。所谓开闭原则就是要对扩展开放,对修改关闭。我们的目标是允许类很容易地进行扩展,在不修改代码的情况下就可以搭配新的行为。至于缺点嘛,在实例化的时候用户不仅仅实例化核心类,还要把核心类包进装饰者中。对于初次接触IO类库的人,无法轻易理解。
InputStream:这是一个好的设计模式,只是需要一些学习成本。以后要有人不理解这设计模式,我就把我和你之间的故事给他讲一遍。
小霍:如此甚好。
回忆结束, 时光回到现在.
InputStream:故事讲完了,这下你明白了吗?
我:原来是这样啊,为了防止类爆炸而采用了装饰者模式, 要是按照我起初的想法,有一个专门的类来处理我的InputFileStream+BufferedInputStream,那InputStream您早就因为类太多引起爆炸了。小霍是个厉害的人物啊。
InputStream:是啊,年轻人就得多学习,小霍确实是个了不起的人物。
结束语:有人问过关于文章里提及的人物真实存在吗?其实大多数都是我杜撰的。 而本文中的小霍确有其人。亚瑟.范.霍夫(Arthurvan Hoff)早期杰出的Java工程师,大多数的Java.io类都出自他手。后来也担任过Flipboard,Dell公司CTO.谢谢他把这么精彩的设计带到人间。文中提及的所有类InputFileStream,BufferedInputStream等都可以在java.io.*中找到,有兴趣的可以去读读源码,jdk的源码就是最规范的java规范,最详细的文档。