查看原文
其他

对Java Inputstream的一次采访

2017-05-25 育树霖疯 码农翻身

前言: 本文是来自于“育树霖疯”微信公众号, 作者把Java IO的装饰者模式以很形象的方式讲了出来, 很有意思。

在学习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规范,最详细的文档。


你看到的只是冰山一角, 更多精彩文章,请移步《码农翻身文章精华


有心得想和大家分享? 欢迎投稿 ! 我的联系方式:微信:liuxinlehan  QQ: 3340792577


码农翻身

用故事给技术加点料

微信号:coderising


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存