JAVA框架理解-装饰者与传统IO

概述

对于应用程序开发程序员来说,深层次接触IO的机会并不多,可能偶尔需要自己持久化文件的时候会用到一点,通常是去网上抄一段代码,调用一些常见的Api,比如RandomAccessFile,FileInputStream等,对于安卓程序员来说,甚至用一些安卓sdk封装的持久化数据的Api,这是大部分程序猿的开发日常,在调用了这些Api后,背后发生了些什么事情常常被忽略掉,由于大部分是读写一些小文件,磁盘消耗和读写速度如何也不加以考虑了。由于有太多的业务需要去实现,这样做是有其合理性的,但是作为一个合格的开发者,IO包是非常重要的,需要进行系统性的学习,这样不至于乱用Api,写出一些低效甚至错误的代码。顺便说一下,JDK里面的源码注释写的非常好,甚至有些还有简单的使用案例代码。

方法论

JAVA IO包下有很多的类,为了更佳宏观上进行把控同时对关键问题进行深入分析,本文打算分以下几个部分来写:

  1. 装饰者模式相关知识
  2. IO基本概念和操作
  3. IO包如何利用装饰者模式设计管理相关类
  4. 核心接口和类的用法和注意点
  5. 常见的IO相关的问答
    通过以上几个知识点的梳理,可以对JAVA的IO包有了比较系统的认识,对于以后的开发将会大有裨益,。

装饰者模式

装饰者模式的作用

首先要用一个模式的时候,关心的是它能解决什么问题,那么装饰者模式,能干什么呢?我自己做个比喻吧,考虑以下这么个场景,我们要实现一个根据信息生成简历的系统,一般情况下,假设我们定义了一个最顶层的接口,比如:

1
2
3
public interface Resume{
void generateResume();
}

接下来,我们要给这个简历系统增加各种各样的功能,比如从txt读取生成,比如从xml读取生成,比如从图片直接读取生成等等,除了从各种文件读取生成的功能,还想要增加改变背景颜色,改变自定义logo等一堆乱七八糟的功能,这些功能可以根据需要进行组装。这个时候,很多开发者就会说啦,这有什么问题,需要用到什么功能的时候,继承自Resume接口写就可以了,如果需要在原有的功能上增加功能,那么在定义基础功能的这个实现类的基础上进行继承,问题就这么解决了,最后的代码组织上大概如下:


可见,整个代码的组织方式看上去会是中心辐射状的,有很多类仅仅是在继承的先后顺序上不同,但最终功能是一致的,这样就会导致一个问题,类的规模会变得不可控制,冗余功能代码很多,灵活性也不高。
上面的这个例子和IO功能的开发需求是类似的,IO框架首先需要有向各种源写入和读取的基本功能,然后可以附加一些便捷的操作,比如多个流的叠加、增加缓存功能、增加基本数据类型的读写的快速Api等,其复杂程度和以上的例子是很类似的,JAVA大牛们肯定想的比我们多,所以,它们使用了装饰者模式来组织代码,最后,有效的控制了规模。

什么是装饰者模式

装饰者模式是在不改变原来类文件和不使用继承的情况下,动态的扩展一个对象的功能,其实现原理是通过创建一个包装对象,来包裹真实的对象。
这都在说啥呢?难道是在讲代理?什么原有对象,真实对象啥的。好吧,其实上面这句话是非常精炼的,是对装饰者模式精华的概括,如果理解了原理,会发现字字到位。顺便说一下,代理模式和装饰者模式的确挺像的,装饰者模式的设计里面的公共函数的实现其实就是用的代理模式,但是两者想要解决的问题是不一样的,代理模式是为其它对象提供一种代理以控制这个对象的访问,重点在于共同实现的方法上,代理调用方法可以在原型的基础上进行一些变化和扩展。
好了,直接上图,看着图说话,简单容易很多


Component接口定义一个对象接口,可以给这些对象动态的添加职责,ConcreteComponent是定义了一个具体的对象,也可以给这个对象添加一些职责。Decorator是装饰者抽象类,继承自Component,从外类来扩展Component,但对于Component来说,是无需知道Decorator的存在的,至于ConcreteDecorator就是具体的对象,起到给Component添加职责的功能。那么我们可以根据装饰者模式来重构前面简历系统的组织方式如下:


可见,经过这样一重构,在类的规模上得到了有效的控制,同时使用的时候,只需要根据需要进行组装就有了相应的控能。
在这里讲解装饰者模式代码就不贴了,免得占用很大的篇幅,可以参考我的另外一篇博客(链接地址->http://gordon-rawe.github.io)。

IO基本概念

流是一个比较抽象的概念,大概可以用流水来进行一些比喻。IO操作在本质上是单个字节的移动,而流是字节移动的载体和方式,其不停由源向目标处移动数据,我们所要做的就是根据流的方向从流中读取数据或者向流中写入数据。一个流,必有源和目标,它们可以是计算机内存的某些区域,也可以是磁盘文件,甚至可以是Internet上的某个URL。流的方向是重要的,根据流的方向,流可分为两类:输入流和输出流。我们从输入流读取数据,向输出流写入数据。
IO可以分为字节流和字符流,它们的不同在于字符流由字节流包装而来,在IO读入之后经过JVM处理,把字节流转换成字符流。而字符流的字符采用哪种字符编码实现由JVM默认编码决定。

字节与字符

字节是通过网络传输信息(或在硬盘或内存中存储信息)的单位。字节是计算机信息技术用于计量存储容量和传输容量的一种计量单位,1个字节等于8位二进制,它是一个8位的二进制数,是一个很具体的存储空间。
字符是人们使用的记号,在抽象意义上的一个符号,为此,在计算机专业领域有多种编码标准,如ANSI、UNICODE等。按照ANSI编码标准,标点符号、数字、大小写字母都占一个字节,汉字占2个字节。按照UNICODE标准所有字符都占2个字节。

字节流与字符流

搞清楚了字节和字符的概念,理解字节流和字符流就没有问题了。字节流是以字节为导向的流(Stream),所有的IO操作都是针对于一个个字节信息的,同样字符流是将原始的字节流,处理成字符,赋予了其意义的一种IO流。
在Java的IO框架里,InputStream和OutputStream及其子类成员都是用来处理字节流的,而Reader和Writer及其子类成员是用来处理字符流的,当然字节流可用来构建字符流(InputStreamReader,InputStreamWriter),字符流在写入文件时候,本质上也是字节流。

IO包如何使用装饰者模式组织管理类

JAVA IO框架下有很多的类,这些类有各自的功能,为了不至于类的规模不可收拾,而且可以彼此组装功能,IO包采用了装饰者模式来组织管理这些类(当然不是所有的类都与装饰者模式相关哈,这一点要注意)。
用图来展示架构可能更佳宏观,为此,根据JDK1.8的源码和Oracle的文档,画了5张UML概括IO包下核心类的组织关系和核心Api:


顶层设计上,主要是InputStream和OutputStream两个用于字节流IO的接口、Reader和Writer两个用于字符流IO的接口以及可以随机读写功能的RandomAccessFile类。本小节只打算从概览一下整个包的组织架构,不展开讨论每个类的实现原理和细节。


按照装饰者模式设计划分,InputStream和OutputStream对应于Component,ByteArrayInputStream、ObjectInputStream、FileInputStream、SequenceInputStream和PipedInputStream等是InputStream模块的ConcreteComponent,ByteArrayOutputStream、ObjectOutpuStream、FileOutputStream等是OutpuStream模块的ConcreteComponent,FilterInputStream和FilterOutputStream分别是两个模块的Decorator,它们的子类BufferedOutputStream、DataOutputStream、PrintStream、BufferedInputStream、DataInputStream和PushBackInputStream是相应的ConcreteDecorator。


同样,按照装饰者模式设计划分,Reader和Writer是顶层抽象Component,InputStreamReader、CharArrayReader、StringReader、PipedReader和BufferedReader是字符流Reader的ConcreteComponent,BufferedWriter、CharArrayWriter、OutputStreamWriter、PipedWriter、StringWriter和PrintWriter是字符流Writer的ConcreteComponent,FilterReader和FilterWriter是Decorator,PushBackReader是ConcreteDecorator。
可以发现JAVA IO的确是用了装饰者模式,但是不是所有的类都参与其中,核心功能比如从什么源来构建流,属于核心功能,那么将其设计为ConcreteComponent,而一些辅助,这里写辅助并不是说不重要啊,比如Buffer缓冲功能就很重要,被设计为ConcreteDecorator。所以一般的使用的思路就首先确定从什么源选最初的构造,比如从文件读取,那么就是FileInputStream,然后想要添加缓冲功能增加效率,减少IO次数,就使用BufferedInputStream包装一下,再然后我想直接读取原始类型数据,那么再用DataInputStream来包装一下,这样,我们强大的功能就这么被组装起来了,由于篇幅,不展开说如何去组装,感兴趣的同学可以自己DIY。

核心接口和类的用法和注意点

为了更好的理解JAVA IO,那么IO基础原理必须得了解,顶层的接口里,每个类的注释都写的非常好,顶层函数的定义,体现了构建整个IO功能最基础的功能,InputStream、OutputStream、Reader和Writer定义的函数不多,我们可以一个个来看。

InputStream
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* This abstract class is the superclass of all classes representing
* an input stream of bytes.
*/
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 {
// some codes are ignored...
int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {}
return i;
}
public long skip(long n) throws IOException {
long remaining = n;
//some codes are ignored...
return n - remaining;
}
public int available() throws IOException { return 0; }
public void close() throws IOException {}
public synchronized void mark(int readlimit) {}
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
public boolean markSupported() {
return false;
}

从官方的注释可以明白其定义目的,该类是所有具有字节流处理功能的类的抽象父类。
read()方法尝试读取字节流的下一个字节,返回读取到的值byte用0到255范围的int类型返回,如果读取到最后没有更多的字节的时候,调用该方法会返回-1。调用该方法会尝试读取一个字节数据,如果这个字节正在传输而处于暂时不可获取状态,该方法会阻塞,知道有可获取的数据、读取到文件末尾(一般用EOF标志)或者发生异常时,该方法就会停止阻塞。
read(byte[] bytes)本质上是调用了read(byte[] bytes,int off,int len)方法,在InputStream顶层的接口就实现了该方法,它是通过循环调用read()来实现的,如果片面的看InputStream的实现,容易产生一个误会,既然read(byte[] bytes)是循环调用read(),那么一次多个字节缓冲并不起任何作用,为何我们还说使用缓冲可以节省磁盘写入和读取的次数呢?这个问题我们后面来讲。
skip(long n)方法用于跳过并丢弃掉字节流的n个byte,其返回值可能会由于一些原因,比如n超过了总的字节数或者超过了剩余的未读字节数的情况下,会返回一个比n小的数,如果输入了一个负数,默认实现是返回0,并且不会丢弃任何字节,但是子类在这个方面可以有自己的处理办法和意义。
avaialble()方法用于返回当前字节流下一次读取前不用阻塞就能读取或跳过的字节数的大约值,这里写下一次读取前不能阻塞就能读取,是针对一些类似网络IO的情况,本地文件的读写使用available()一般可以返回总的字节数,但是一些网络IO,比如,Socket通讯时,对方明明发来了1000个字节,但是自己的程序调用available()方法却只得到900,或者100,甚至是0,感觉有点莫名其妙,怎么也找不到原因。其实,这是因为网络通讯往往是间断性的,一串字节往往分几批进行发送。本地程序调用available()方法有时得到0,这可能是对方还没有响应,也可能是对方已经响应了,但是数据还没有送达本地。对方发送了1000个字节给你,也许分成3批到达,这你就要调用3次available()方法才能将数据总数全部得到,好了,简单来说就是available有些时候不太靠谱,我也曾经才过坑,从网上拉过一些半成品文件。
close()方法用于关闭一个字节流,并且释放和这个字节流相关的系统资源,当一个流关闭了以后,其它方法涉及与系统io操作都不能进行了,不然就会得到IO异常。
后面三个方法通常是配套使用的,InputStream默认是不支持mark的,子类需要支持mark必须重写这三个方法。 mark用于在此输入流中标记当前的位置。对reset 方法的后续调用会在最后标记的位置重新定位此流,以便后续读取重新读取相同的字节,readlimit参数告知此输入流在标记位置失效之前允许读取许多字节。reset方法就是简单的讲下一个要读取的pos复位位上次标记的时候的值,BufferedInputStream就实现了mark和reset机制,可以进行参考,一般使用这些功能进行数据窥探和重复使用,细节不做展开,(link -> http://zhangbo-peipei-163-com.iteye.com/blog/2022460)可参考该链接博客。

OutputStream
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* This abstract class is the superclass of all classes representing
* an output stream of bytes. An output stream accepts output bytes
* and sends them to some sink.
*/
public abstract void write(int b) throws IOException;
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
public void write(byte b[], int off, int len) throws IOException {
// some codes are ignored...
for (int i = 0 ; i < len ; i++) {
write(b[off + i]);
}
}
public void flush() throws IOException {}
public void close() throws IOException {}

该类是所有具有字节输出功能类的父类,主要有上面五个方法,OK,其实是四个。
write(int b)用于写入一个字节的数据,这里使用的int,但是只有最后的八位是有效的,其它的24位都被忽略掉了。
write(byte[] bytes,int off,int len)方法用于写入多个byte的数据,在OutputStream中的默认实现也是循环调用write来实现的,那么和上面一样,同样会有一种调用该方法并没有减少磁盘压力的作用。
flush()方法用于将数据强制刷新到要写入的目的地。
close()方法用于释放系统资源,已经关闭了以后,其它IO操作时会发生IO异常。

Reader和Writer

Reader、Writer和InputStream、OutputStream接口差不多,Reader多了ready和read(CharBuffer buffer),前者很好理解,后者是和NIO中的Buffer进行对接,然后read系列方法里InputStream是int,而Reader是char,同时还有同步方面lock的踪迹,做了线程安全方面的一些处理。Writer增加write(String s,int off,int len),可以方便的将字符串写入目标,还增加了append系列的函数,有点像动态扩容数组,方便追加内容,同样,Writer也做了同步方面的处理。

核心类

限于篇幅,不可能围绕每个类详细的展开,不然就成了jdk文档了,那么,本文打算列举一些常用的代码片段,以此来对IO包的常用类有直观的认识。

InputStream族

ByteArrayInputStream把内存中的一个缓冲区作为InputStream使用。
StringBufferInputStream把一个String对象作为InputStream,但是官方不推荐使用StringBufferInputStream方法,此类不能将字符正确的转换为字节。
FileInputStream把一个文件作为InputStream,实现对文件的读取操作,getChannel()方法返回一个FileChannel对象,这个主要用于JNIO中的通道的。
PipedInputStream实现了管道的概念,主要在线程中使用,管道输入流是指一个通讯管道的接收端。一个线程通过管道输出流发送数据,而另一个线程通过管道输入流读取数据,这样可实现两个线程间的通讯。其connection(PipedOutputStream)方法用来连接PipedOutputStream对象。
SequenceInputStream把多个InputStream合并为两个InputStream,其允许应用程序把几个输入流连续地合并起来,并且使它们像单个输入流一样出现,每个输入流依次被读取,直到到达该流的末尾。
ObjectInputStream用于操作Object的stream,ObjectInputStream(InputStream)用一个InputStream对象来实例化一个ObjectInputStream对象,其中InputStream就是对象的输入流。readObject(Object)方法将一个对象写入到stream中,但是这个object必须实现序列化接口,其他的还有像readInt,readFloat等这样基本类型的方法,因为基本类型对应的对象也都是Object.
FilterInputStream是一个装饰者类,直接使用它没有任何意义,但是它的子类可以改变或增强原有的一些类的功能。
BufferedInputStream使用缓冲区的stream,增强了原有的传入的InputStream的功能,使用缓冲区有一些好处,可以通过支持mark和reset操作,jdk里BufferedInputStream的markSupport是返回true的,所以,想要用这种带缓冲的字节流,BufferedInputStream很适合,构造的时候可以选择缓冲区大小,默认是8k,除了mark和set功能,其最重要的还是预读缓存策略,即如果调用read(),那么第一次会读一批数据缓存起来,以后的每次read()从缓存中去取,这样就不会频繁的触发操作系统IO。
DataInputStream是一种将数字格式化的InputStream,可以从流中直接读取原始类型,读取的时候最好和写入的类型顺序一致,不然可能会读取失败。
PushBackInputStream挺有意思的,可以有unread操作,即先读一部分,然后还可以返回去一些字节,该流可以窥探一些数据,然后做操作,相比其它的只能向前读取,其提供了很多灵活性。

OutputStream族

ByteArrayOutputStream把信息存入内存中的一个缓冲区中,实现了一个以字节数组形式写入数据的输出流,当数据写入缓冲区时,发生扩容,toByteArray()和toString()方法用于查看数据,writeTo(OutputStream out)用out.write(buf, 0, count)调用输出流的写方法将该字节数组输出流的全部内容写入指定的输出流参数。
FileOutputStream向File或FileDescriptor输出数据,可以指定是否以追加的方式进行写入,默认是不追加的,getChannel()方法返回一个FileChannel对象,这个主要用于NIO中的通道操作。
PipedOutputStream是一个通讯管道的发送端,一个线程通过管道输出流发送数据,而另一个线程通过管道输入流读取数据,这样可实现两个线程间的通讯,其connection(PipedInputStream)用于连接一个PipedInputStream。
ObjectOutputStream是对象输出流,于ObjectInputStream对应使用,能够将一个对象写入到OutputStream中,其writeObject(Object)方法将一个对象Object写入到OutputStream中,writeInt(),writeFloat()等类似的基本类型方法,因为基本类型对应的对象类型都是Object的子类,所以可以这样操作,ObjectInputStream/ObjectOutputStream主要用与将一个对象Object写入到本地或者是在网络中进行传输的,所以这些对象需要进行序列化操作。
FilterOutputStream是架构装饰者模式中的Decorator,无实际意义,主要是为了组织其子类而创建的类。
BufferedOutputStream带有缓冲区的stream,它的特点在于不会频繁的和调用操作系统进行IO,其采取了更佳聪明的策略,即先缓冲起来,等到到达一定的数量了,一次写入,那么这样就减少了磁盘IO的次数,遇到EOF或者调用flush,都会在没有满的情况下触发磁盘IO。
DataOutputStream是一种具有格式化的OutputStream,用于增强一个OutputStream让其具有写出各种基础数据类型的功能,典型的函数如writeInt,writeFloat,writeDouble等。

Reader族

CharArrayReader在功能上与ByteArrayInputStream对应此类实现一个可用作字符输入流的字符缓冲区。
StringReader功能上与StringBufferInputStream对应,从一个字符串读取生成字符流。
FileReader是InputStreamReader的子类,本质上是封装了一个FileInputStream的InputStreamReader。
PipedReader功能上与PipedInputStream对应。
InputStreamReader是InputStream和Reader之前转化的桥梁,是从字节流到字符流的桥梁,它读入字节,并根据指定的编码方式,将之转换为字符流。InputStreamReader的read()方法之一的每次调用,可能促使从基本字节输入流中读取一个或多个字节。
为了达到更高效率,考虑用BufferedReader封装InputStreamReader,使用的编码方式可能由名称指定,或平台可接受的缺省编码方式。

Writer族

CharArrayWriter功能上与ByteArrayOutputStream对应,可以向另一个Writer写字符流。
FileWriter本质上是一个包装了FileOutputStream的OutputStreamWriter。
OutputStreamWriter将OutputStream转化成Writer,将多个字符写入到一个输出流,根据指定的字符编码将多个字符转换为字节,每个OutputStreamWriter合并它自己的CharToByteConverter,因而是从字符流到字节流的桥梁,PrintReader和PrintStream对应。

核心类的使用方法

这个小结一直在犹豫要不要写,网上随便google一大堆,没有什么意义,最后还是决定加到文章里,不是为了凑篇幅,个人写博客写的是情怀,又不是像读研时候的论文,而是为了日后查阅方便,对于这些操作已经很轻车熟路的童鞋们,就直接跳过这个区域吧!
ByteArrayOutputStream实例Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ByteArrayOutputStreamDemo {
public static void main(String args[])throws IOException {
ByteArrayOutputStream bOutput = new ByteArrayOutputStream(12);
while( bOutput.size()!= 10 ) {
//从字符串生成字符数组
bOutput.write("hello".getBytes());
}
byte b [] = bOutput.toByteArray();
System.out.println("Print the content");
for(int x = 0 ; x < b.length; x++) {
//打印这些字符
System.out.print((char)b[x] + " ");
}
System.out.println(" ");
int c;
ByteArrayInputStream bInput = new ByteArrayInputStream(b);
System.out.println("Converting characters to Upper case " );
for(int y = 0 ; y < 1; y++) {
while(( c = bInput.read())!= -1) {
System.out.println(Character.toUpperCase((char)c));
}
bInput.reset();
}
}
}

StringBufferInputStream官方已经标记为Deprecated了,因为其不能正确的进行编码转换,既然如此,那就不看了,推荐使用StringReader,那我们来看看StringReader的实例Demo吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class StringReaderDemo {
public static void main(String[] args) {
String str = "Hello World! \nThis is StringReader Program.";
StringReader sr = new StringReader(str);
int i=0;
try {
while((i=sr.read())!=-1){
System.out.print((char)i);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

最常见的应该是FileInputStream和FileOutputStream了,一个典型的不带缓冲的读写例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class FileInputStreamDemo {
public final static String SRC = "src.txt";
public final static String DEST = "dest.txt";
public static void main(String[] args) {
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
int tmpValue;
try {
inputStream = new FileInputStream(SRC);
outputStream = new FileOutputStream(DEST);
while ((tmpValue = inputStream.read()) != -1) {
outputStream.write(tmpValue);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
outputStream.flush();
inputStream.close();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

提高一点效率,来个带缓冲的读写的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class BufferedFileInputStreamDemo {
public final static String SRC = "src.txt";
public final static String DEST = "dest.txt";
public static void main(String[] args) {
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
byte[] tmpValue = new byte[1024];
int tmpCount;
try {
inputStream = new FileInputStream(SRC);
outputStream = new FileOutputStream(DEST);
while ((tmpCount = inputStream.read(tmpValue)) != -1) {
outputStream.write(tmpValue, 0, tmpCount);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
outputStream.flush();
inputStream.close();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

都讲到这里了,那就得提一下BufferedInputStream和BufferedOutputStream啦,同样的代码,使用BufferedInputStream和BufferedOutputStream包装一下,真实的磁盘IO会是两样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class BufferedInputStreamDemo {
public final static String SRC = "src.txt";
public final static String DEST = "dest.txt";
public static void main(String[] args) {
InputStream inputStream = null;
OutputStream outputStream = null;
int tmpValue;
try {
inputStream = new BufferedInputStream(new FileInputStream(SRC));
outputStream = new BufferedOutputStream(new FileOutputStream(DEST));
while ((tmpValue = inputStream.read()) != -1) {
outputStream.write(tmpValue);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
assert outputStream != null;
outputStream.flush();
inputStream.close();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

BufferedInputStream还支持mark和reset操作,假设一个文本src.txt里面有 gordon is handsome! 这几个字符,当都到h的时候我们进行标记,当到!的时候reset一下,重新读取,我们可以写出如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class BufferedInputStreamDemo {
public final static String SRC = "src.txt";
public static void main(String[] args) {
InputStream inputStream = null;
boolean marked = false;
int tmpValue;
try {
inputStream = new BufferedInputStream(new FileInputStream(SRC));
while ((tmpValue = inputStream.read()) != -1) {
char tmpChar = (char) tmpValue;
if (tmpChar == 'h' && !marked) {
inputStream.mark(inputStream.available());
marked = true;
}
if (tmpChar == '!' && marked) {
inputStream.reset();
marked = false;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

在本地文件读取的时候,available还是可靠的,从上面的demo我们可以看到mark和reset的用法,代码简单,就不赘述了,除了BufferedInputStream外,上面的ByteArrayInputStream也支持该功能。

PipedInputStream和PipedOutputStream也挺有意思的,附上一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class PipedInputOutputDemo {
final static PipedOutputStream pipedOut = new PipedOutputStream();
final static PipedInputStream pipedIn = new PipedInputStream();
class PipedOutputThread implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
pipedOut.write(("Message " + i + "\n").getBytes());
Thread.sleep(500);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class PipedInputThread implements Runnable {
@Override
public void run() {
try {
int i = 0;
while ((i = pipedIn.read()) != -1) {
System.out.print((char) i);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
try {
pipedOut.connect(pipedIn);
} catch (IOException e) {
e.printStackTrace();
}
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new PipedInputOutputDemo().new PipedOutputThread());
service.execute(new PipedInputOutputDemo().new PipedInputThread());
}
}

代码依然简单如斯,不做解释。

到了DataInputStream了,上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class DataInputStreamDemo {
public static void main(String[] args) throws IOException {
InputStream is = null;
DataInputStream dis = null;
FileOutputStream fos = null;
DataOutputStream dos = null;
double[] dbuf = {65.56, 66.89, 67.98, 68.82, 69.55, 70.37};
try {
fos = new FileOutputStream("test.txt");
dos = new DataOutputStream(fos);
for (double d : dbuf) {
dos.writeDouble(d);
}
dos.flush();
is = new FileInputStream("test.txt");
dis = new DataInputStream(is);
while (dis.available() > 0) {
double c = dis.readDouble();
System.out.print(c + " ");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null)
is.close();
if (dos != null)
is.close();
if (dis != null)
dis.close();
if (fos != null)
fos.close();
}
}

再来个有趣的,SequenceInputStream用于多个文件流合并,上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SequenceInputStreamDemo {
public static void main(String[] args) throws Exception {
FileInputStream fis1 = new FileInputStream("testfile1.txt");
FileInputStream fis2 = new FileInputStream("testfile2.txt");
FileInputStream fis3 = new FileInputStream("testfile3.txt");
Vector<InputStream> inputStreams = new Vector<InputStream>();
inputStreams.add(fis1);
inputStreams.add(fis2);
inputStreams.add(fis3);
Enumeration<InputStream> enu = inputStreams.elements();
SequenceInputStream sis = new SequenceInputStream(enu);
int oneByte;
while ((oneByte = sis.read()) != -1) {
System.out.write(oneByte);
}
System.out.flush();
}
}

太多了,来两个Reader的吧,在byte的时候不知道字节换行什么的,但是转换为字符后,就知道在各种编码方式下,什么时候是换行了,于是就有了readLine操作了,当然代码上这很简单了,上个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BufferedReaderDemo {
public static void main(String[] args) throws Exception {
String thisLine = null;
try {
// open input stream test.txt for reading purpose.
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(new File("src.txt"))));
while ((thisLine = br.readLine()) != null) {
System.out.println(thisLine);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

再写一个ObjectOutputInputStream和ObjectInputStream的例子,由于类还是挺多了,到这儿就差不多了吧,看完该吃午饭了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class ObjectWriteReadDemo{
public static class User implements Serializable {
private static final long serialVersionUID = 8309080721495266420L;
private String name;
private int age;
private boolean male;
public User(String name, int age, boolean male) {
this.name = name;
this.age = age;
this.male = male;
}
@Override
public String toString() {
return "name -> " + name + " age -> " + age + " male -> " + (male ? "yes" : "no");
}
public static User createUser(String name, int age, boolean male) {
return new User(name, age, male);
}
}
public static void main(String[] args) throws Exception {
FileOutputStream fileOutputStream = null;
ObjectOutputStream objectOutputStream = null;
FileInputStream inputStream = null;
ObjectInputStream objectInputStream = null;
try {
fileOutputStream = new FileOutputStream("src.txt");
objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(User.createUser("gordon rawe", 25, true));
} catch (IOException e) {
e.printStackTrace();
} finally {
assert objectOutputStream != null;
objectOutputStream.close();
objectOutputStream.flush();
fileOutputStream.close();
}
try {
inputStream = new FileInputStream("src.txt");
objectInputStream = new ObjectInputStream(inputStream);
User user = (User) objectInputStream.readObject();
System.out.println(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
assert objectInputStream != null;
objectInputStream.close();
inputStream.close();
}
}
}

运行代码,得到结果是:

1
name -> gordon rawe age -> 25 male -> yes

如果我们将name字段变为transient的,那么的到的结果是:

1
name -> null rawe age -> 25 male -> yes

原因大家都懂,不做过多的解释啦,这个章节我总觉得是多余了,好了,希望大家不要喷我!

常见的关于IO的问答

使用缓冲流如何提高效率?

很多人在看了InputStream接口的实现后会感到困惑,为什么听别人说使用缓冲流或者自己提供缓冲方法可以有效提高读写效率,而接口里面的read(byte[] buf,int off,int len)是循环的调用read()方法啊,的确接口是这么实现的,但是,JAVA的多态将细节隐藏了,如果查看FileInputStream的read(byte[] buf,int off, int len)是调用了一个native方法readBytes(byte[] buf,int off,int len),该方法可以一次拷贝多个字节,由于每次读写都是用户空间数据和内核空间数据的交互,这些交互需要从两个空间进行切换,减小交互次数能够提高程序的效率,这也是程序鼓励使用缓冲的原因。

Buffered系列的字符流和字节流怎么就提高效率了?

这个问题在上面其实已经提及过了,这些类在内部内置了一个缓冲区,用来缓冲数据,在读取数据的时候,一次性预读取一个缓冲区的数据,然后以后的零散的读取数据会从盖缓冲区根据下标去取,这样就不用频繁的调用读取单个字节的read()方法,触发一次磁盘IO;同样,在输出的时候,调用零散的write()方法,会等到一个缓冲区满了以后才将数据正式写入到磁盘,这样就有效减少了磁盘的IO次数,好了,原因就是这样的,突然感觉JAVA官方考虑问题都很完善很具体啊,源码有很多值得学习的地方。

那么全能选手RandomAccessFile效果如何呢?

这里有一篇讲的不错的别人的博客,我就不抄袭了,花1K内存实现高效I/O的RandomAccessFile类(https://www.ibm.com/developerworks/cn/java/l-javaio/)

总结

最后总结一些通用性的原则。
一、按数据来源(去向)分类:
1 、是文件: FileInputStream, FileOutputStream, ( 字节流 )FileReader, FileWriter( 字符 )
2 、是 byte[] : ByteArrayInputStream, ByteArrayOutputStream( 字节流 )
3 、是 Char[]: CharArrayReader, CharArrayWriter( 字符流 )
4 、是 String: StringBufferInputStream, StringBufferOuputStream ( 字节流 )StringReader, StringWriter( 字符流 )
5 、网络数据流: InputStream, OutputStream,( 字节流 ) Reader, Writer( 字符流 )

二、按是否格式化输出分:
1 、要格式化输出: PrintStream, PrintWriter

三、按是否要缓冲分:
1 、要缓冲: BufferedInputStream, BufferedOutputStream,( 字节流 ) BufferedReader, BufferedWriter( 字符流 )

四、按数据格式分:
1 、二进制格式(只要不能确定是纯文本的) : InputStream, OutputStream 及其所有带 Stream 结束的子类
2 、纯文本格式(含纯英文与汉字或其他编码方式); Reader, Writer 及其所有带 Reader, Writer 的子类

五、按输入输出分:
1 、输入: Reader, InputStream 类型的子类
2 、输出: Writer, OutputStream 类型的子类

六、特殊需要:
1 、从 Stream 到 Reader,Writer 的转换类: InputStreamReader, OutputStreamWriter
2 、对象输入输出: ObjectInputStream, ObjectOutputStream
3 、进程间通信: PipeInputStream, PipeOutputStream, PipeReader, PipeWriter
4 、合并输入: SequenceInputStream
5 、更特殊的需要: PushbackInputStream, PushbackReader, LineNumberInputStream, LineNumberReader

参考资料

通过mark和reset方法重复利用InputStream:
http://zhangbo-peipei-163-com.iteye.com/blog/2022460

Java中的IO流系统详解:
http://blog.csdn.net/jiangwei0910410003/article/details/22376895

Java IO最详解
http://blog.csdn.net/luckylcs/article/details/50416933

细说java io相关:
http://www.cnblogs.com/zhuYears/archive/2013/04/10/2993292.html

很有意思的老外的pdf:
http://people.cs.aau.dk/~torp/Teaching/E03/OOP/handouts/io.pdf

一个老外的各种上课资料:
http://people.cs.aau.dk/~torp/Teaching/E03/OOP/handouts/io.pdf

oracle官网
http://docs.oracle.com/javase/tutorial/essential/io/streams.html

花1K内存实现高效I/O的RandomAccessFile类
https://www.ibm.com/developerworks/cn/java/l-javaio/