Stream概述
Stream是一个数据流,可以从它读取数据或写入数据。它是连接数据源或数据目的地,例如文件,网络连接。
Stream中没有和数组一样,读、写数据时利用索引访问的概念。也没有和数组或RandomAccessFile一样的,向前或向后移动。Stream是一个连续的数据流。
某些流,如PushbackInputStream可以把数据放回流中重新读取,但是这只能是有限个数的数据,而且无法随意遍历数据。数据只能被顺序访问。
流的特性:
先进先出,最先写入输出流的数据最先被输入流读取到。
顺序存取,可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。
只读或只写,每个流只能是输入流或输出流的一种,不能同时具备两个功能,在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流。
I/O流分类
有无数据源或目的地
根据流对象构造时是否需要数据源或数据目的地,可以分为两类:节点流和处理流。
节点流需要有数据源或数据目的地,处理流需要一个另一个流作为参数。java.io包结构采用装饰者模式设计流。
数据源一般有,File、ByteArray、String、char、pipes。
pipes
pipes,中文意思为通道。他的能力是为2个在同一个JVM中运行的线程提供通信。所以它可以是数据源也可以是数据目的地。不可以使用pipe在两个不同进程中的线程间提供通信。
Java中的pipe和Linux/Unix中的概念不同。后者可以用于运行在两块不同空间地址的进程间通信。
使用时一个pipedInputStream应该和一个PipedOutputStream相连。写入pipe输出流的数据是在另一个线程中通过与其相连的pipe输入流读取到的。然后调用PipedOutputStream.write()输出到当前线程。
public class PipeExample {
public static void main(String[] args) throws IOException {
final PipedOutputStream output = new PipedOutputStream();
final PipedInputStream input = new PipedInputStream(output);
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
output.write("Hello world, pipe!".getBytes());
} catch (IOException e) {
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
int data = input.read();
while(data != -1){
System.out.print((char) data);
data = input.read();
}
} catch (IOException e) {
}
}
});
thread1.start();
thread2.start();
}
}
可以看到输出流作为参数传递给输入流的构造器,使它们相连。当然可以用PipedOutputStream/PipedInputStream.connect()方法去连接另一个pipe流。
注意,pipe流的read()或者write()都是阻塞方法,必须在同一个进程中的不同线程中使用。如果在同一个线程中使用会造成线程死锁。而且一般线程通信,传递的都是一个完整对象,很少有用raw byte。翻译自Java IO: Pipes
流向
依据流的方向,可以分为输入流河输出流。
输入流只进行读操作,输出流写进行写操作。
处理数据类型
按照处理数据类型,可以分为:字符流和字节流。
由于字符集的原因,导致同一个字符根据不同的字符集有不同的编码标示。于是有了适合处理字符的便捷流。其本质就是字符和字节根据字符集之间的相互转换。两者区别:
读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。
如果只是处理文本字符,优先使用字符流。其它数据类型(字符数据也可以)使用字节流。
字节流
java.io包中的类结构是采用装饰者模式设计的,所以分别介绍字节流输入,输出流的节点流和处理流。
输入流
所有字节输入流的父类是一个抽象类,InputStream。java.io包中,它的直接子类如下:
Stream Name
Dec
ByteArrayInputStream
属于节点流,连接byte数组作为数据源
PipedInputStream
属于节点流,连接线程间共享的通道(pipe)
StringBufferInputStream
属于节点流,连接字符串(已废弃)
FileInputStream
属于节点流,连接本地文件(通过FileSystem连接JVM可访问的file system中的文件)
ObjectInputStream
属于处理流,反序列化先前通过ObjectOutputStream写入的数据
FilterInputStream
属于处理流,覆写了InputStream中的方法来实现数据的转换或提供额外的方法。它的子类扩展了更多的功能也属于处理流
SequenceInputStream
属于处理流,用于其他输入流的逻辑连接。它依照输入流集合顺序开始读取,直到读到最后一个流中数据源尾为止
输出流
所有字节流输出流的负累是一个抽象类,OutputStream。java.io包中,他的直接子类如下:
Stream name
Dec
ByteArrayOutputStream
属于节点流,连接byte数组作为数据输出对象
PipedOutputStream
属于节点流,连接线程间共享的通道(pipe)
FileOutputStream
属于节点流,连接本地文件(通过FileSystem连接JVM可访问的file system中的文件)
FilterOutputStream
属于处理流,覆写了OutputStream中的方法来实现数据的转换或提供额外的方法。它的子类扩展了更多的功能也属于处理流
ObjectOutputStream
属于处理流,向包含的OutputStream输出基本数据类型和对象实例。可写入的对象实例必须是后期可通过ObjectInputStream反序列化的,即它必须是可序列化的
PrintStream
属于处理流,给其他输出流添加功能,使用系统默认的字符集编码各种类型的数据(基本数据类型,字符串,引用类型),转换成字节输出
流详解
PushbackInputStream
一个装饰流,他的主要作用就是回退字节(字节数组)或者称作字节(字节数组)未读,下一个读取操作继续读取该字节(字节数组)。它适用于一个片段代码读取一串以特定字节值结束,数量不确定的字节数组;当读取到特定的字节并调用unread()后以便于下一个读取操作读取到回退的字节。
PushbackInputStream有两个字段:
buf,用于缓存回退字节的字节数组。
pos,缓存字节数组中元素个数。
构造PushbackInputStream时,不指定buf大小,默认值为1。注意unread()操作并不会跳过回退的一个或多个字节,下一个读取操作一定会从缓存数组取出回退的项目。
SequenceInputStream
一个装饰流,用于逻辑连接多个输入流,并且按照集合顺序开始读取操作。它的构造函数有:
SequenceInputStream(Enumeration extends InputStream> e)
SequenceInputStream(InputStream s1, InputStream s2)
Enumeration封装了有关遍历集合的方法,同样还可以遍历集合的接口是Iterator。
注意遍历并不是指单纯的获取,它的行为类似for循环。所以这两个接口和集合类中的获取元素方法并不重叠。
它们的区别在于:
Enumeration只能获取集合中的数据,不能修改集合结构。而Iterator除了遍历集合,还可以删除集合中的数据,修改集合结构。
Iterator支持fail-fast错误检测机制,而Enumeration不支持。
Enumeration只能为Vector,Hashtable类型集合提供遍历,且由它们生成对象;而Iterator可以为HashMap,ArrayList等集合提供遍历。
相同点:它们的方法都是线程安全,支持同步。
可以看出Enumeration的命名和它本身提供的功能有关,只能够枚举集合元素,不能修改集合结构。这和Enum类似。
fail-fast错误检测机制
fail-fast错误检查机制指,同一时间有多个线程,使用除了Iterator自身的方法对集合的结构修改,会快速失败。并且抛出ConcurrentModificationException异常。
关于fail-fast,这里需要注意两点:
Iterator自身方法支持线程安全,所以不会触发fail-fast
必须是同一时间有多个线程对集合作出结构上的修改。
实践测试fail-fast
public class TestFailFast {
private static List list = new ArrayList<>();
private static Hashtable table = new Hashtable<>();
public static void main(String[] args) {
for(int i = 0; i < 10; i++) {
list.add(i);
table.put(String.valueOf(i), i);
}
System.out.println("测试fail-fast发生时机");
//测试fail-fast,以及不同时操作不触发fail-fast的情况
new Thread01().start();
new Thread02().start();
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("测试Enumeration不支持fail-fast");
//测试Enumeration不支持fail-fast
new Thread03().start();
new Thread04().start();
}
private static class Thread01 extends Thread {
public void run() {
//A.测试线程1,2不同时操作集合list,
//会不会抛出ConcurrentModificationException
//给线程1睡眠10毫秒,让线程2先执行
// try {
// Thread.sleep(10);
// }catch(InterruptedException e) {
// e.printStackTrace();
// }
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
int i = iterator.next();
System.out.println("Thread 1 iterator in: " + i);
//B.如果希望抛出ConcurrentModificationException,
//就把当前线程睡眠10毫秒。并且注释A代码片段执行
try {
Thread.sleep(10);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
private static class Thread02 extends Thread {
public void run() {
int i = 0;
while(i < list.size()) {
System.out.println("Thread 2 run in: " + i);
if(i == list.size() / 2) {
list.remove(i);
}
i++;
}
}
}
private static class Thread03 extends Thread {
public void run() {
Enumeration e = table.elements();
//c.测试同时有另一个线程改变了table结构,会不会在后续的遍历中看到改变的结果
//线程3睡眠10毫秒,等待线程4修改集合后再遍历。
// try {
// Thread.sleep(10);
// }catch(InterruptedException ex) {
// ex.printStackTrace();
// }
while(e.hasMoreElements()) {
int i = e.nextElement();
System.out.println("Thread 3 iterator in:" + i);
//d.测试同时有两个线程操作table,
//会不会抛出ConcurrentModificationException。
//给当前线程3睡眠10毫秒,让线程4操作集合。并且注释c代码片段执行。
try {
Thread.sleep(10);
}catch(InterruptedException ex) {
ex.printStackTrace();
}
}
}
}
private static class Thread04 extends Thread {
public void run() {
int count = table.size();
int i = 0;
while(i < count) {
System.out.println("Thread 4 run in: " + i);
if(i == count / 2) {
table.put("random", 23);
}
i++;
}
}
}
}
A,C情况同时运行时,输出如下:
测试Iterator支持fail-fast以及Enumeration不支持fail-fast.png
B,D情况同时运行时,输出如下:
测试多线程不同时操作集合不会触发fail-fast,以及Enumeration遍历时集合被修改可以遍历修改部分.png
字符流
同样以装饰者设计模式角度来看看java.io包下的字符流
Reader
Reader是字符流输入流的父类,它是一个抽象类。
Reader Name
Dec
BufferedReader
属于处理流,缓存字符,提高读取字符,数组以及行的效率
CharArrayReader
属于节点流,连接char数组数据源
FilterReader
属于处理流,过滤式字符读取流的抽象父类,子类扩展该类
InputStreamReader
属于处理流,连接字节流和字符流的桥梁,将字节转换为字符
PipedReader
属于节点流,连接线程间共享的通道(pipe)数据源
StringReader
属于节点流,连接String对象数据源
Writer
Writer是字符输出流的父类,它是一个抽象类
Writer Name
Dec
BufferedWriter
属于处理流,缓冲字符,提高输出字符,数组,字符串的效率
CharArrayWriter
属于节点流,连接char数组作为数据写入对象
FilterWriter
属于处理流,字符过滤式输出流的抽象父类,子类扩展该类功能
OutputStreamWriter
属于处理流,连接字节流与字符流的桥梁,将字符编码转换为字节
PipedWriter
属于节点流,连接线程件共享的通道(pipe)作为数据输出对象
PrintWriter
属于处理流,向文本输出流打印对象的格式化形式(包括基本数据类型,字符串和对象)
StringWriter
属于节点流,一个String缓冲输出流,生成String对象
流详解
PushbackReader&PushbackInputStream
两个都具有回退功能,前者针对字符,后者针对字节。PushbackReader内部维护了一个char数组缓存回退字符;PushbackInputStream内部维护了一个byte数组缓存回退字节。两个又一个共性,在没有指定缓存区大小时,默认只能回退一个字符或字节。
BufferedReader&inputStreamReader
BufferedReader是给其它Reader对象添加字符缓冲区,而InputStreamReader内部有一个字节数组缓冲区,用于每次进行底层读取(native关键词的读方法,磁盘I/O交互操作)时,尽可能读取更多字节而不是满足必须的数量。
官方文档中建议为了提高效率,应该使用BufferedReader+InputStreamReader组合。既然InputStreamReader有了缓冲区,干嘛还需要BufferedReader?
这是因为两者缓存的并不是同一样东西,提高的效率也不是同一个对象。
BufferedReader的作用是每一次的读操作尽量从底层的字节流或字符流(这里的底层字节、字符流是指BufferedReader包含的其他流对象)读取更多的字符放入缓冲区,从而避免多次字节转换字符,并为其分配内存。如果底层是字节流,还会减少与磁盘文件的I/O交互。
InputStreamReader属于处理流,必须由InputStream对象作为构造参数来实例化对象。它内部的缓存区主要作用是减少与磁盘的底层I/O交互。每一次读取时尽可能多的读取字节,放入字节数组缓存区。
InputStreamReader和BufferedReader组合的意义是,每调用BufferedReader对象的读方法,尽可能多的从InputStreamReader中获取解码后的字符,放入缓存区。而此时InputStreamReader对象与底层磁盘文件交互时尽可能多的读取字节放入缓存区,减少I/O交互。
实践效率差
public class TestEfficiency {
public static void main(String[] args) {
File file = new File("../file/TestFile0.txt");
long startTime = 0;
long endTime = 0;
InputStreamReader in = null;
BufferedReader reader = null;
FileInputStream underlyIn = null;
try {
try {
underlyIn = new FileInputStream(file);
in = new InputStreamReader(underlyIn, "GBK");
int c = -1;
startTime = System.nanoTime();
while((c = in.read()) != -1) {
}
endTime = System.nanoTime() - startTime;
System.out.println(
"using InputStreamReader input characters from text spend time:"
+ endTime);
reader = new BufferedReader(in);
startTime = System.nanoTime();
while((c = reader.read()) != -1) {
}
endTime = System.nanoTime() - startTime;
System.out.println(
"using BufferedReader input characters from text spend time:"
+ endTime);
}finally {
reader.close();
}
}catch(IOException e) {
e.printStackTrace();
}
}
}
时间输出比较:
using InputStreamReader input characters from text spend time:22936559
using BufferedReader input characters from text spend time:33166
从上面的分析可以看出,从JVM外部文件读写都是采用字节流中的native方法,实现I/O交互。而在内存中操作字符优化(如为字符、字节分配内存)是依靠BufferedReader/BufferedWriter。
LineNumberReader
一个缓冲字符输入流,监视行数。从0开始,没读取到一个行结束数据加一。虽然类中有setLineNumber()可以改变行数的数值,实际上无法达到随机访问文件的效果,仍旧是顺序读取。设置的行数只是改变了getLineNumber()的返回值(也就是类内部记录行数的变量值被修改)。
FileReader
InputStreamReader是连接字符和字节的桥梁,而一般读取文件使用字符流是它的子类FileReader。内部实现了FileInputStream和Reader之间的转换,提供了读取文本的快捷方式。
PrintWriter&PrintStream
打印对象格式化形式
通过文档得知,两个类都可以对引用类型对象进行格式化打印。那么怎么打印呢?
PrintStream.java&PrintWriter.java
public void print(Object obj) {
write(String.valueOf(obj));
}
String.java
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
分析:首先使用String.valueOf(),调用Object.toString()得到String对象。然后使用系统默认的字符集转换成字节,使用write(int)方法输出。
原理
两个类都有涉及到OutputStreamWriter,它是字符和字节之间的桥梁。
PrintStream
类内部有以下几个成员变量:
BufferedWriter textOut
OutputStreamWriter charOut
在构造PrintStream时,传入OutputStream(因为PrintStream是处理流),并且执行父类构造函数。然后使用当前对象(实际类型为PrintStream)构造charOut对象,最后构造textOut对象。
由于是字节流(继承自OutputStream),有两个公共的write()重载方法,输出字节。其内部都是调用父类中的成员变量out(这个out就是PrintStream构造函数传入的OutputStream参数)相应的write()。
这样做的目的是为了冲刷BufferedWriter的缓冲区。而out变量不一定有冲刷缓冲区的方法。
所有的print()重载方法内部都是调用了PrintStream的write(byte[])和write(String)私有方法。而println()是调用了相应的print()后再跟上一个依赖系统的换行符。
以write(String)为例
private void write(String s) {
try {
synchronized (this) {
ensureOpen();
textOut.write(s);
textOut.flushBuffer();
charOut.flushBuffer();
if (autoFlush && (s.indexOf('\n') >= 0))
out.flush();
}
}
catch (InterruptedIOException x) {
Thread.currentThread().interrupt();
}
catch (IOException x) {
trouble = true;
}
}
流程分析:
字符串s传给textOut.write()。
BufferedWriter内部也有一个out变量,就是charOut。传递给charOut.write()。
OutputStreamWriter内部有一个se变量,实际类型为SteamEncoding。而构造se对象时,需要传入OutputStream,OutputStreamWriter本身,以及字符集名称。
在StreamEncoding内部,通过字符集编码字符串s,并且转传成字节序列,调用传入的OutputStream对象的write()。而这个OutputStream对象实际类型就是PrintStream,也就是它自身实现的Write()公共方法。
PrintWriter
PrintWriter类内部有一个成员变量out,其表现类型为Writer。该变量实际类型有两种情况,一是BuferedWriter,另一种是构造PrintWriter对象时传入的Writer对象。
类的print()重载方法都是调用相应的write()方法。而write()方法内部都是调用out.write()方法。
println()内部就是调用print()然后加上以来系统的换行符。其余方法内部实现可以参考Java I/O PrintWriter
总结
从上述分析来看,两者在print()重载方法方面没有什么区别。但两者根本区别是PrintStream是字节流,有自己处理字节的方法。而PrintWriter是字符流,没有处理字节的方法。
另一方面就是自动冲刷缓冲区机制的区别:
PrintStream自动冲刷情况,write(byte[]),println()的重载方法,print()的重载方法,以及write(byte)输出换行符字节或者字节值为10的调用时。
PrintWriter自动冲刷情况,printf(),println()的重载方法,format()调用时。
猜测PrintStream的存在是为了让字节流使用字节意外的数据进行I/O操作。一般字节流,如FileOutputStream没有一个输出方法可以传入除字节类型的数据。