IO/NIO学习总结


【1.什么是InputStream?InputStream类结构?】

首先学习基类java.io.InputStream(读取一系列字节的对象),以及在它基础上派生出来的子类,类结构图如上图所示。InputStream是一个抽象类,是所有数据形式为字节的输入流的父类,为基于字节的数据输入定义了基本操作方法。实际上,InputStream的子类大部分都没有增加任何其他的接口函数(在上面的类结构图中就可以发现),因此在看InputStream子类的时候,我们主要学习其构造函数。

PushbackInputStream 向另一个输入流添加“推回 (push back)”或“取消读取 (unread)”一个字节的功能。这在以下情况下非常有用,即代码片段可以很方便地读取由特定字节值分隔的不定数量的数据字节;在读取终止字节后,该代码片段可以“取消读取”该字节,这样,输入流上的下一个读取操作将会重新读取被推回的字节。例如,表示构成标识符的字符的字节可能由表示操作符字符的字节终止;其作业只是读取标识符的方法可以进行读取,直到该操作看到此操作符,然后将该操作符推回以进行重读。

1.2.1 PushbackInputStream

“给你第二次机会”——小议PushbackInputStream, Java I/O系统是一个典型的Decorator模式的实现,它以InputStream/OutputStream为基本核心,通过继承关系,不断为该核心添加新的功能,如文件流、缓冲、加解密等。对I/O系统设计模式感兴趣的话,可以参考developerWorks上的一篇文章:从Java类库看设计模式。Java I/O默认是不缓冲流的,所谓“缓冲”就是先把从流中得到的一块字节序列暂存在一个被称为buffer的内部字节数组里,然后你可以一下子取到这一整块的字节数据,没有缓冲的流只能一个字节一个字节读,效率孰高孰低一目了然。有两个特殊的输入流实现了缓冲功能,一个是我们常用的BufferedInputStream,像读文件我们常用

BufferedInputStream in = new BufferedInputStream(new FileInputStream("datafile")); 

while ((b = in.read()) != -1) 

{ 

 ... 

} 

in.close();


在通常状态下,“流”意味着“一次性”,就是说你进行了一次操作后它的状态就变了,譬如读,无论是文件还是socket,你读的过程中一个潜在的“读指针”一样的东东就在移动,你无法在读以后再重新定位(当然RandomAccessFile是另一种情况),如果你以前奇怪为什么数据库操作中ResultSet里get某个字段以后就不能再第二次get它了,这里或许是个解释。但好在PushbackInputStream给了我们第二次读的机会。

我们先来区别一下“监听”和“截获”的概念,“监听”就是把得到的消息copy一份,原始消息并不作任何改变地传递到目的地;而“截获”则是先把消息“扣押”下来,不让其自动转给目标,而是先进行一些处理以后在转发给目标(如果是网络安全专业的背景知识,大概知道“监听”是对“机密性”的攻击,而“截获”不仅是对“机密性”还是对“完整性”的攻击)。有的朋友大概对hook这个名词有些了解,它是一种Windows的一种消息处理机制,似乎就是一种消息截获手段,但我对Windows编程一窍不通。

此外,如果你熟悉Servlet的话,也能找到像Filter这样的处理机制,在对每个HTTP请求/应答进行转发之前,先在里头耍一点花招,确定哪些予以转发,哪些屏蔽掉,这也算是“截获”吧。通过上面的介绍,我们不妨把PushbackInputStream看成是对输入流的一种“截获”手段,其中最重要的方法是unread:

public void unread(int b) throws IOException 

 public void unread(byte[] b) throws IOException 

 public void unread(byte[] b, int off, int len) throws IOException



我们可以想象一下,PushbackInputStream内置一个缓冲区(事实上,你可以从它的源代码里找到这个protected的字节数组),当低层流进来时先流进这个buffer,在你把流“物归原主”之前还有机会对它耍花招,然后再用unread方法“反悔”一下,把缓冲区里已经读过的内容(一般是没有被改动的,当然你也可以改动它,那就失去“归赵”的意义了,因为已经不是“完璧”了)再插入到流的头部,下次读的时候是流剩余的部分再加上从缓冲区“归还”的部分。上面三个unread方法分别代表从缓冲区“归还”一个字节、一个字节数组以及一个字节数组中指定的部分。
PushbackInputStream是对二进制流的处理,字符流下相对应的就是PushbackReader。

有什么用?

【重要】学过编译的话就容易理解了,比如从左向右扫描字符流“for(int i=0;i<10;i++)”,扫描到“for”是不是就可以说是个关键字了呢?不行,说不定后面是“for1”,那就是个变量而不是关键字了,知道看到“(”才恍然大悟,哦,我可以安全地说“看到for关键字”了,但“(”还得归还给输入流,因为需要后面继续扫描。在上下文相关语言里,就更需要这种补偿机制。又如,在解析HTML文档的时候,我需要根据它的“meta”标签的“charset”属性来决定使用哪种字符集进行解析,但HTML可不是“charset”而是“<html>”开头的哦!所以需要通过PushbackInputStream缓冲前面一段内容,等取到字符集名称后在把读到的流全部归还,再用指定的字符集进行解析。

【2.Java I/O中是如何采用Decorator(装饰)模式的呢?】

答:下面为大家详细说明,看到FilterInputStream类(也就前面说的过滤流,后面你会发现更多的过滤流),你是否发现了?对,就是 FilterInputStream类,她就相当于Decorator(装饰)模式中的Decorator类,而且的 BufferedInputStream、DataInputStream、PushbackInputStream则相当于是 ConcreateDecorator,如下图为Decorator模式结构图所示:

那么Java I/O中到底是如何使用的了?Decorator(装饰)模式的主要意图是:动态地给一个对象添加一些额外的职责,这句话很抽象,我们结合Java I/O举个具体的例子:比如说我们读取文件,首先打开文件获取到File,然后我们再创建一个FileInputStream,然后读取文件。读取文件是一个很费时的操作,尤其是需要多次的读写文件。

Decorator模式的设计动机:有时希望给某个对象增加而不是整个类增加一些功能,例如,给一个图像界面工具箱允许你对人员一个用户界面的组件添加一些特性,比如说边框,或者窗口滚动。使用继承机制是添加功能的一种有效途径,从其他类继承过来的边框特性可以被多个子类的实例所实现。但是这种方法不够灵活,因为边框的选择是静态的,用户不能控制对组件加边框的方式和时机。

一种较为灵活的方式是将组件嵌入另外一个对象中,由这个对象添加边框,我们称这个嵌入的对象为装饰。

在Java中采用OOP:BufferedInputStream实现了对数据读取的缓冲机制,通过FileInputStream来读取数据,BufferedInputStream将已经读取的数据存储到缓冲区,BufferedInputStream相当于对 FileInputStream进行了“装饰”。

Decorator示例代码如下:

File file = new File(“c:\\moandroid.txt”,true); 

InputStream is = new BufferedInputStream(new FileInputStream(file)); 

long length = file.length(); 

if(length>Integer.MAX_VALUE){ 

System.out.println(“source file is too large”); return ; 

} 

byte[] bytes = new byte[(int)length]; 

int offset = 0,numRead = 0; 

while( offset<bytes.length && (numRead = is.read(bytes,offset,bytes.length-offset))>= 0) 

offset += numRead; 

if(offset<bytes.length) 


throw new IOException(“Could not completely read file”+file.getName()); 

is.close(); 


DataInputStream类的功能则更加强大,其在InputStream类的基础上增加了很多读取函数的接口,举个例子如下: 


DataInputStream:增加了读取JAVA基本数据类型的功能。 


BufferedInputStream:增加了缓冲的功能。 


InputStream is = null; 

try{ 

File file = new File(“c:\\moandroid.txt”,true); 

is = new DataInputStream(new FileInputStream(file)); 

int intData = is.readInt(); 

boolean boolData = is.readBoolean(); 

}catch(FileNotFoundException e) { e.printStackTree();} 

catch(IOException e){ e.printStackTree(); } 

finally{ if(is!=null){ try{is.close();} catch(IOException e){} }}



PS:DataInputStream读取的顺序必须和实际数据存储的顺序一致,否则会出现IOException。

1.3 OutputStream及其Family:

【1.什么是OutputStream?OutputStream类结构图?】

和InputStream类似,OutputStream(写入一系列字节的对象)是所有字节形式输出流的类。 java.io.InputStream/OutputStream的类结构中可以找到相互对应的类,这里额外需要说明的是PrintStream 类。

实际上标准输出流:System.out的类型就是java.io.PrintStream。PrintStream作为FilterOutputStream的子类,其作用也是将某个输出流再次封装,并且提供了一些新的输出特性。其他标准输出:System.in的类型是InputStream,其默认是由JRE指向系统的标准输入流,在控制台下默认是键盘的输入,使用in.read()方法,将返回用户键盘输入的值;System.err的类型也是 java.io.PrintStream。

下面举个具体的例子来说明,如何实现重定向标准输入/输出:

PrintStream output = new PrintStream(new FileOutputStream(“c:/out.log”)); 

System.setOut(output); 


PrintStream errOutput = new PrintStream(new FileOutputStream(“c:/err.log”)); 

System.setErr(errOutput); 


System.out.println(“Output redirect test”); 

System.err.println(“Error redirect test”);



原来在控制台输出的文字都将被写入out.log或err.log文件中。

1.4 Reader及其Family?

【7.什么是Reader?Reader类结构图?】

InputSteream和OutpurStream是针对基于字节(byte)输入输出设计的,应用中常需要读写的是基于字符(char ,Unicode 2个字节)的,java.io.Reader和java.io.Writer就是所有读写字符数据流的父类。Reader提供的方法和InputStream提供的几乎是一样的,不同之处在于Reader的操作多数是char类型的。

对上图(从下往上看)总结说明如下:

· Reader类中定义了成员变量lock,顾名思义,lock的用途是解决实现对流操作的同步问题。Reader的子类可以使用这个成员变量来实现流操作的同步操作。

· FilterReader也是采用了Decorator(装饰)模式,与我们在前面学习InputStream、OutputStream相比较,如下图所示:

前面的 BufferedInputStream、BufferedOutputStream都是采用了Decorator(装饰)模式,而这里的 BufferedReader是直接从Reader继承下来了,而不是从F ilterReader继承的,不符合Decorator(装饰)模式据说这个JAVA SDK中的一个BUG,但是好像一直也没有修改,估计对使用没有大的影响吧。此外BufferedReader还提供了一个String readLine()函数,方便我们从文件中读取一行数据,这个函数也是比较有用的。

· InputStreamReader就像是一个桥:把字节流(byte)转换为字符流(char),它读取字节流,并使用指定的字符集(charset) 将这些字节流转换为字符。常见的字符集有:GBK、ISO-8859-1、UTF-8,Java的字符流本身采用的就是Unicode,关于字符集的详细说明请看文档。例子如下:

BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 

String strLine; 

while(strLine=in.readline()!=null){ 

System.out.println(strLine); 

} 

in.close();


具体的含义就是:将标准输入流转换为字符流,并缓存到BufferedReader的缓冲区中,并将缓冲区中的数据显示在标准输出设备上。

总结说明

Reader子类中较常用的还是:BufferedInputStream、InputStreamReader,BufferedInputStream为我们提供了数据缓存机制,对于读取大块的数据比较方便,尤其是在读取文件的时候;InputStreamReader作为流的转换,转换后的字符流是什么编码,与虚拟机默认的字符集有关,关于字符集部分大家还需要去深入学习。