最近看到一篇文章,初步介绍java.io.InputStream,写的非常通俗易懂,在这里我完全粘贴下来。
来源于 https://mp.weixin.qq.com/s/hDJs6iG_YPww7yeiPxmZLw
在学习java.io.*包的时候,Input-Stream那一群类很让人反感,子类繁多就不用说,使用起来非常奇怪。我们想以缓存的方式从文件中读取字节流。总要先创建一个FileInput-Stream,然后把这个File-InputStream放入Buffered-InputStream构造函数中去创建BufferedInputStre-am。完成这些工作后才能开始读取文件。
(以缓存方式从文件中读取数据输入流的JAVA代码)
为什么我们不能直接创建以缓存方式从文件中读取数据的输入流类呢?今天我带着这样的问题走进InputStream的家,希望他能够给我答案。
我:老人家,我想问问,为什么我们想以带缓存的方式从文件中读取字节流需要创建FileInputStream和BufferedInput-Stream两个类,这么麻烦的实现方式您是怎么想到的?这种设计是不是太缺心眼了?
InputStream:年轻人,这要从很久很久以前说起。那时候Java刚刚被创造出来,我也幸运的被创造出来。那时候我还没有任何子孙,孤家寡人一个,无所事事。一天,有一个叫小霍的年轻人找到了我。他说他要让我飞黄腾达,子孙满堂。
我:这么神?那您讲讲您和小霍之间的故事吧。
时光倒流回20年前,小霍初见InputStream。
小霍:InputStream你好,我是JAVA帝国计划生育委员会的工作人员,今天我带着任务而来,组织决定让您儿孙满堂。
InputStream:此话当真?
小霍:我小霍是谁啊,必须比啊。而了实现这个目标,我已经从计划生育委员会给你争取了好几个生育名额。
InputStream:我知道从那争取到名额很不容易,那你说说,组织准备让我生几个?
小霍:你是IO的输入类,负责读取数据(字节流)。数据就是你的包裹,你一般从哪些渠道获得包裹?
InputStream:文件,字节数组,StringBuffer,其它线程,对了还有已经被序列化的对象。
小霍:那你先根据数据来源的渠道生5个孩子,老大叫FileInputStream处理文件,老二叫ByteArrayInputStream处理字节数组,老三叫StringBufferInputStream处理StringBuffer,老四叫PipedInput-Stream处理线程间的输入流,老五叫ObjectInputStream处理被序列化的对象。
InputStream:万一有一个包裹里面有多个或者多种数据输入流呢。
小霍:那就再生一个SequenceInputStream处理一个包裹里有多种数据来源的业务。还有其它问题吗?没问题我就回单位了。帝国刚建立,我们计划生育委员会掌管着全国的生育名额,我还忙着呢。你抓紧时间生孩子,有问题再找我。
InputStream:好咧,我这就关灯造人。
交流完毕后,小霍走了,我也抓紧时间把我6个孩子生了出来,为国家做贡献。In-putStream的6个孩子齐心协力,处理了JAVA早期很多的输入业务。但是他们也面临了新的问题。没过多久。年轻的计生委员小霍再次找到了InputStream。
小霍:你那6孩子都是能人啊,但是现在客户抱怨他们的工作还不够到位。
InputStream:我那6个孩子个个工作勤勤恳恳,怎么还不到位?
小霍:客户嘛,都比较挑剔。他们抱怨你们读取数据太慢了,尤其是你的老大FileInputStream每次读数据都慢死了。好多客户等待都超过了几十秒了,还没把数据等回来。
InputStream:几十秒很慢吗?
小霍:我们计算机都是以纳秒计时的,所谓世间一秒钟,机器已千年。那些客户头发都等白了。
InputStream: 读数据慢能怪我吗?这不是硬盘慢造成的吗?
小霍:是,是硬盘造成的,我们想一个办法让用户减少访问硬盘的次数。比如建一个buffer怎么样?用户需要的数据先让他们在buffer里面找,能找到就直接从buffer里返回,实在找不到再去硬盘里找。Buffer在内存里,内存可比硬盘快10万倍呢(内存在随机访问的速度上是硬盘的10万倍以上)。
InputStream:这办法好。客户抱怨的其他问题呢?
小霍:客户想要的数据类型都是int, long, String这样的JAVA基本类型,而你提供给他们的都是byte类型,还需要客户自己进行类型转换。客户觉得麻烦。还有一个问题,stream里面读出来的数据就不能重新放回stream,客户想要一个功能,能把读出来的数据再推回stream里面。
InputStream:看来我得再生3个孩子: 拥有缓存的BufferedInputStream,把byte转换成JAVA基本类型的DataInput-Stream和回写数据到stream的Push-backInput-Stream。
小霍:老伙计,你糊涂了,不止3个。你想想,你已经有6个孩子。他们掌握着6种数据来源。如果每一个数据来源都分别实现这3个新功能。你至少得生18个孩子吧。还有更大的问题,万一客户既想有数据回写,又想要输出int,long,String这样的数据,还有要缓存。或者有的客户只需要这3个新功能的其中2个呢?那么一个数据源需要7个新类,6兄弟一共是42个。
InputStream:看来我们得想象另外的办法。这样会造成类爆炸。再说让我一下生那么多孩子,我也煎熬啊。
小霍:你玩过俄罗斯套娃没?一个实心的娃娃被各种各样娃娃外壳套着。一个实心娃娃先套一个学生的外壳,那么他就是学生了,如果我再在外面套一个运动员的壳,那么他就成了有运动员身份的学生。我们模仿这种形式,比如最里面的实心娃娃是处理文件读取的FileInputStream,外面套一个BufferedInputStream的壳,那么这个套娃就是带buffer的FileInput-Stream。如果再套一个DataInputStre-am,那么套娃就成了能输出int这样java 基本类型并且带buffer的FileInputStre-am。搭配由客户去决定,我们只需要提供套娃壳(新的3个功能类)和最里面的实心娃InputStream(InputStream的6个孩子)。客户在搭配套娃的时候必须有一个实心娃娃,否则就没有数据来源。
InputStream:这很巧妙,那如何实现这样一种设计呢?
小霍:有2个关键点:
1,既然套娃中一定有实心娃娃,那么套娃的壳的类必须包含一个实心娃。比如BufferedInputStream里面要包含一个InputStream,我们通过BufferedInput-Stream的构造函数把这个实心娃娃Input-Stream放进去,当然DataInput-Stream和PushbackInputStream也一样。
2,BufferedInputStream+实心娃娃InputStream组成的新套娃又可以当作DataInputStream的实心娃娃,那么我们让这些套娃的外壳BufferedInputStre-am,DataInputStream,BufferedInputStream都继承自InputStream类,这样才能实现新组成的套娃又可以被另外的套娃壳嵌套。这3个套娃壳有着共同的特点都是装饰实心娃娃,我们再在他们上层在抽象一个FilterInputStream,让FilterInput-Stream继承自InputStream,让Filter-InputStream里面包含一个实心娃娃In-putStream。以后所有的装饰类都从FilterInputStream继承。
InputStream:这样我也省事了,只需要再多生一个FilterInputStream,剩下的BufferedInputStream,DataInputStream,PushbackInputStream这样的装饰类都让FilterInputStream去生了。
小霍:对,加上FilterInputStream,你就有7个孩子了,跟葫芦娃一样。前面6个哥哥都是和数据源有关,7弟就是用来装饰6个哥哥。把数据源的InputStream类和装饰的InputStream整合在了一起。
InputStream:而且对于BufferedInput-Stream,DataInputStream,PushbackInputStream来说,我还是爷爷,想着他们叫我爷爷的样子,我心里就美滋滋的。
小霍:美的你,Java中无论多少次继承都是子类父类关系,没有爷爷这么一说。我把家谱给你。你儿子太多,我就画Byte-ArrayInputStream,FileInputStream 和FilterInputStream的简化版。
(装饰者模式类图)
小霍:这样的设计既避免了类爆炸,又可以让用户自己去搭配核心类和装饰类。而且也满足了一个重要的设计原则:开闭原则。这是指导思想。所谓开闭原则就是要对扩展开放,对修改关闭。我们的目标是允许类很容易的进行扩展,在不修改代码的情况下就可以搭配新的行为。至于缺点嘛,在实例化的时候用户不仅仅实例化核心类,还要把核心类包进装饰者中。对于初次接触IO类库的人,无法轻易理解。
InputStream:这是一个好的设计模式,只是需要一些学习成本。以后要有人不理解这设计模式,我就把我和你之间的故事给他讲一遍。
小霍:甚好。
时光回到现在.
InputStream:故事讲完了,这下你明白了吗?
我:原来是这样啊,要是按照我起初的想法,有一个专门的类来处理我的InputFile-Stream+BufferedInputStream,那Input-Stream你早就因为类太多引起爆炸了。小霍是个厉害的人物啊。
InputStream:是啊,年轻人就的多学习。小霍确实是个了不起的人物。
结束语:有人问过关于文章里提及的人物真实存在吗?其实大多数都是我杜撰的。而本文中的小霍确有其人。亚瑟.范.霍夫(Arthurvan Hoff)早期杰出的Java工程师,大多数的Java.io类都出自他手。后来也担任过Flipboard,Dell公司CTO.谢谢他把这么精彩的设计带到人间。文中提及的所有类InputFileStream,Buffered-InputStream等都可以在java.io.*中找到,有兴趣的可以去读读源码,jdk的源码就是最规范的java规范,最详细的文档。
下面我们单独看看SequenceInputStream,用来合并流。
public class SequenceInputStream extends InputStream {
Enumeration<? extends InputStream> e;
InputStream in;
/**
* Initializes a newly created <code>SequenceInputStream</code>
* by remembering the argument, which must
* be an <code>Enumeration</code> that produces
* objects whose run-time type is <code>InputStream</code>.
* The input streams that are produced by
* the enumeration will be read, in order,
* to provide the bytes to be read from this
* <code>SequenceInputStream</code>. After
* each input stream from the enumeration
* is exhausted, it is closed by calling its
* <code>close</code> method.
*
* @param e an enumeration of input streams.
* @see java.util.Enumeration
*/
public SequenceInputStream(Enumeration<? extends InputStream> e) {
this.e = e;
try {
nextStream();
} catch (IOException ex) {
// This should never happen
throw new Error("panic");
}
}
/**
* Initializes a newly
* created <code>SequenceInputStream</code>
* by remembering the two arguments, which
* will be read in order, first <code>s1</code>
* and then <code>s2</code>, to provide the
* bytes to be read from this <code>SequenceInputStream</code>.
*
* @param s1 the first input stream to read.
* @param s2 the second input stream to read.
*/
public SequenceInputStream(InputStream s1, InputStream s2) {
Vector<InputStream> v = new Vector<>(2);
v.addElement(s1);
v.addElement(s2);
e = v.elements();
try {
nextStream();
} catch (IOException ex) {
// This should never happen
throw new Error("panic");
}
}
...
}
主要的构造方法有两个,我们分别来距离来举例看一下。
例一:
public class SequenceInputStreamTest {
public static void main(String[] args) throws Exception {
Vector<InputStream> v = new Vector<InputStream>();
v.add(new BufferedInputStream( new FileInputStream("D:/eclipse_4.4/mywork/myselfWork/src/1.txt")));
v.add(new FileInputStream("D:/eclipse_4.4/mywork/myselfWork/src/2.txt"));
Enumeration<InputStream> en = v.elements();
SequenceInputStream sis = new SequenceInputStream(en);
FileOutputStream fos = new FileOutputStream("D:/eclipse_4.4/mywork/myselfWork/src/3.txt");
byte[] buf = new byte[1024];
int len = 0;
while((len=sis.read(buf))!=-1)
{
fos.write(buf,0,len);
}
fos.close();
sis.close();
}
}
例二:
public class SequenceInputStreamTest {
public static void main(String[] args) throws Exception {
BufferedInputStream bf = new BufferedInputStream( new FileInputStream("D:/eclipse_4.4/mywork/myselfWork/src/1.txt"));
FileInputStream file = new FileInputStream("D:/eclipse_4.4/mywork/myselfWork/src/2.txt");
SequenceInputStream sis = new SequenceInputStream(bf,file);
FileOutputStream fos = new FileOutputStream("D:/eclipse_4.4/mywork/myselfWork/src/3.txt");
byte[] buf = new byte[1024];
int len = 0;
while((len=sis.read(buf))!=-1)
{
fos.write(buf,0,len);
}
fos.close();
file.close();
bf.close();
}
}
这两个方法,都将1.txt和2.txtx写入了3.tet。
再看看 PushBackInputStream,可以向输入流中添加功能,对数据处理后允许使用unread回推数据。
public class PushBackInputStream {
public static void main(String[] args) {
byte[] b = {1,2,3};
//PushBackInputStream可以向输入流中添加功能,允许使用unread回推数据。
PushbackInputStream pbi = new PushbackInputStream (new ByteArrayInputStream(b));
int result;
try {
while ((result = pbi.read()) != -1) {
System.out.print("取出:" +result + ";");
//读出字节,对字节进行处理,然后回推到流中
result = result+1;
pbi.unread(result);
pbi.read();
System.out.println("回推后取出:"+result + "。");
}
} catch (IOException e) {
e.printStackTrace();
}
try {
pbi.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
结果: