一、概覽
“流”(stream)有方向:流進(input stream)和流出(output stream)。
“流”有流動的最小單位:①有基於一個字節(single-byte)流動的InputStream和OutputStream家族;②也有基於兩個字節流動(two-byte)的Reader和Writer家族。
為什么會有兩大家族呢?
1、基於single-byte流動的有兩個最基本的抽象類(abstract classes):InputStream和OutputStream。稍后我們會看到以這兩個抽象類作為父類,衍生了一個龐大的IO家族。
2、由於基於single-byte的流不方便處理那些用Unicode編碼方式存儲的字符characters信息。從而java的IO系統中又出現了另外的一個基於Reader和Writer抽象類,用於處理characters信息的家族。
二、讀寫bytes
抽象類InputStream中有一個抽象讀方法:
abstract int read();
每次調用這個方法就會從流中讀取一個byte並返回讀取到的byte值;如果遇到輸入流的末尾,則返回-1。
這個抽象類還重載了其它的read方法,但都是在底層調用了上面這個讀取單字節的抽象的read()方法。該抽象類還有如下方法:
①、abstract int read();
②、int read(byte[] b),最大讀取b.length個字節數據;
③、int read(byte[] b, int off, int len),最大讀取len個字節數據到b字節數組中,從off位置開始存放;
④、long skip(long n),在輸入流中跳過n個字節,返回實際跳過的字節數。當遇到末尾的時候實際跳過的數據可能小於n;
⑤、int available(),返回在不阻塞的情況下流中的可以讀取的字節數;
⑥、void close(),關閉流;
⑦、void mark(int readlimit),在輸入流的當前位置打一個標記(注:不是所有的流都支持這一特性);
⑧、void reset(),返回到最后一個標記處。隨后調用read方法會從最后一個標記處重新讀取字節數據。如果當前沒有標記,則不會有任何變化;
⑨、boolean markSupported(),判斷當前流是否支持標記操作;
對應的,抽象類OutputStream中也有一個抽象的寫方法:
abstract void write(int b);
OutputStream類有如下方法:
①、abstract void write(int b);
②、void write(byte[] b),將b中存放的所有數據都寫入到流中;
③、void write(byte[], int off, int len),將b字節數組中從off位置開始的len個字節數據寫入到流中;
④、void close(),關閉和flush輸出流;
⑤、void flush,對輸出流做flush操作,也就是說,將所有輸出流中緩存的數據都寫入到實際的目的地;
上面抽象的read()和write()方法都會阻塞,直到byte讀寫成功為止。這就意味着,如果在讀寫過程中,如果當前流不可用,那么當前線程就會被阻塞。為解決阻塞的問題InputStream類提供了一個avaliable()方法,可以檢測當前可讀的字節數。所以,下面這段代碼永遠不會被阻塞:
int bytesAvailable = in.available(); if(bytesAvailable > 0){ byte[] data = new byte[bytesAvailable]; in.read(data); }
當我們讀寫完畢以后,應該要調用close()函數來關閉流。這樣做,一方面可以釋放掉流所持有的系統資源。另外一方面,關閉一個輸出流也會將暫存在流中的數據flush到目標文件中去:輸出流會持有一個buffer,在其buffer沒有滿的時候是不會實際將數據傳遞出去的。特別的,如果你沒有關閉一個輸出流,那么很有可能會導致最后那些存放在buffer中的數據沒有被實際的傳遞出去。當然,我們也可以通過調用flush()方法手動的將buffer中的數據flush出去。
三、結合stream filters
先來看一下第一個家族:
什么叫Combining Stream Filter呢?我們逐一的解釋。
我們從第一個層面上看(直接繼承自InputStream或OutputStream的這些類),FileInputStream能夠讓你得到一個附着在磁盤文件上的輸入流,FileOutputStream能夠得到一個對磁盤文件的輸出流。比如用下面的方式:
FileInputStream fin = new FileInputStream("employee.dat");
FileOutputStream fout = new FileOutputStream("employee.dat");
和InputStream、OutputStream抽象類一樣,FileInputStream和FileOutputStream也只提供基於byte的讀寫方法。
但是,我們如果能夠得到一個DateInputStream,那么我們就可以從流中讀取numeric types了,比如我們可以從流中讀取一個double類型的數據:
DataInputStream din = ... double s = din.readDouble();
現在我們可以YY一下,要是能夠直接向file中讀寫numeric types該多好!!!你當然可以做得到,就像下面這樣:
FileInputStream fin = new FileInputStream("employee.dat"); DataInputStream din = new DataInputStream(fin); Double s = din.readDouble();
看,你做到了。只要將兩個層面上的流結合起來,就可以了。java使用了一種很好的機制將對底層和對上層的操作分開,這樣既方便了流向底層寫byte,也方便了我們使用我們習慣的numeric types類型。
再介紹一對很重要的流,它對提高讀寫效率有很大的幫助:BufferedInputStream和BufferedOutputStream,他們分別為輸入和輸出流提供了一個緩沖區。比如在上面的流中添加一個緩沖區,讓它更快一些:
FileInputStream fin = new FileInputStream("employee.dat"); BufferedInputStream bin = new BufferedInputStream(fin); DataInputStream din = new DataInputStream(bin); Double s = din.readDouble();
有了上面的分層介紹以后,你當然會很明白為什么要將BufferedInputStream放在中間層,而不是很殺馬特的將其放在最外層了。你可知道,BufferedInputStream和BufferedOutputStream只提供對byte的讀寫方法。還有以下兩個例子:
//1、可以利用pin.unread(b)來跳躍,利用din.readLong()等讀取numeric types PushbackStream pin = null; DataInputStream din = new DataInputStream( pin = new PushbackStream(new FileInputStream("employee.dat"))); //2、對zip的操作 ZipInputStream zin = new ZipInputStream(new FileInputStream("employee.dat")); DataInputStream din = new DataInputStream(zin);
理解到這里,我們可以放心的相信一件事情了:關閉流的時候,只需要關閉最外層的流即可。因為,它自己會一層一層的往里面調用close()方法。
四、讀寫character
字符相對來說比java基本類型的數據難處理。我們知道,字符有很多種編碼方式。比如,ASCII編碼占用1個字節長度,每個Unicode占用2個字節長度。為了方便處理文本形式的流,JDK單獨開辟了另外一個專門的IO家族——Reader和Writer。類似於前面的InputStream和OutputStream,這兩個類分別有一個抽象的讀/寫方法:
abstract int read(); //返回一個0~65535之間的整數,遇到流末尾則返回-1。 abstract void write(int c);
五、讀寫文本(text)
當你向保存一個數據的時候,你有兩種選擇保存數據的方式:二進制和文本格式。比如說,整數1234用二進制保存的時候,它是這樣的 00 00 04 D2(in hex);如果采用文本格式,則它會被保存為字符串“1234”的形式。
盡管,對二進制數據的讀寫很快速而且高效,但是二進制不方便於人的閱讀。當我們保存一個一個文本字符串的時候,我么需要考慮到字符的編碼方式。如果用UTF-16的編碼方式,則“1234”將會保存為 00 31 00 32 00 33 00 34(in hex);而采用ISO8859-1編碼,則會保存為 31 32 33 34(in hex)。舉個例子:
InputStreamReader in = new InputStreamReader(System.in);
這個InputStreamReader會將從控制台讀取到的數據用系統默認的編碼方式進行編碼。當然,也可以用InputStreamReader(new FileInputStream("kernel.dat"),"ISO8859_5")的方式明確指定哪種編碼方式。
因為,我們有很多地方需要將一個file綁定到reader或者是writer上面;所以,JDK給我們提供了一對方便的讀寫類FileReader和FileWriter。比如說下面兩種定義是等價的:
//方便的定義方式 FileWriter out = new FileWriter("output.txt"); //等價的定義方式 FileWriter out = new FileWriter(new FileOutputStream("output.txt"));
1、怎樣寫Text
對於文本的輸出,有一個方便的類PrintWriter。因為,這個類提供了文本格式的寫字符串和寫數字的方法,其print方法有很多種重載方式。同時,我們還可以很方便的將PrintWriter和FileWriter聯系起來,下面的兩種方式是等價的:
//定義PrintWriter的便捷方式 PrintWriter out = new PrintWriter("out.txt"); //等價的定義方式 PrintWriter out = new PrintWriter(new FileWriter("out.txt")); //聯想到FileWriter我們還可以得出一種等價方式 PrintWriter out = new PrintWriter(new FileWriter(new FileOutputStream("out.txt")));
還需要注意的一點就是,PrintWriter自帶了一個緩沖器,默認情況下只有在緩沖區填滿的時候才會將數據flush到目的地。PrintWriter的構造器有兩種:
//默認情況下 autoFlush是關閉的,緩沖區慢才會將數據傳遞出去 PrintWriter out = new PrintWriter(Writer out); //可以指定autoFlush為true。這樣,無論何時調用print函數,都會立刻flush緩沖區 PrintWriter out = new PrintWriter(Writer out, boolean autoFlush);
注意,PrintWriter有如下的構造函數:
PrintWriter(Writer out) PrintWriter(Writer out, boolean autoFlush) PrintWriter(String fileName) PrintWriter(File file) //這個很強大,可以直接對輸出流做打印 PrintWriter(OutputStream out) PrintWriter(OutputStream out, boolean autoFlush) //還有一個很有意思的printf函數。這個對調整格式很方便 void printf(String format, Object... args)
2、怎樣讀Text
如我們所知道的,對二進制數據的讀寫很方便的可以使用DataInputStream和DataOutputStream對。上面也說了,寫Text有一個很好用的PrintWriter。那么,讀Text呢?還會有想二進制這么方便嗎?比如說,我想讀取一個Double類型的數據: r.readDouble()。答案:不好意思,沒有!!
就目前來講,有兩種方式:①、Scanner類可用,也提供了不少方法;②、BufferedReader in = new BufferedReader(new FileReader("employee.txt"));可用,用它來讀取一行,然后自行分解去吧。但是,BufferedReader么有讀取numeric這么方便的方法。
其實,也可想而知,文本嘛,就沒有所謂的Double啊,Integer啊什么的區別了,所有的都是“文本”了,只是它長得像數字罷了。
最后,來看一下Writer和Reader家族: