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沒有一個輸出方法可以傳入除字節類型的數據。