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<Integer> list = new ArrayList<>();
private static Hashtable<String, Integer> 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<Integer> 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<Integer> 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情况同时运行时,输出如下:
B,D情况同时运行时,输出如下:
注意Hashtable使用Enumeration遍历时,是从后往前遍历。参考Java 集合系列18之 Iterator和Enumeration比较,Java 集合系列04之 fail-fast总结,Java提高篇(三四)—–fail-fast机制
字符流
同样以装饰者设计模式角度来看看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没有一个输出方法可以传入除字节类型的数据。