java IO流學習分享

被擱淺的魚發表於2016-04-06

從剛接觸Java程式設計開始,JavaIO流在我的心中就始終如潘多拉魔盒那般,神祕而強大。但自己卻始終提不起勇氣去一探這個盒子當中的奧祕。直到最近一個專案要用到非同步IO和圖片讀寫,並要求給新員工培訓(領導施壓之下,典型的說著不走,打著才走的型別!),萬般無奈之下,才終於鼓起了勇氣去探索了這個神祕魔盒中蘊藏的寶藏。在這裡,本人就將探索寶藏的血淚史和大家做一個分享,希望對大家理解Java IO流能有所幫助! 首先,在瞭解具體Java IO 之前,一直困惑我的一個問題是:什麼是流?它是幹嘛用的?為了搞清這個問題,我翻閱了大量的圖書,在這裡就簡單的說下自己從書裡得到的對流的感悟(特別宣告,真沒有賣弄的意思,很長一段時間,我真不知道流是幹什麼的!歡迎大家拍磚)!

流是Java抽象出來的一個概念,具體的說就是對 [輸入/輸出] 裝置的抽象(大家知道在計算機中,資料一般會存在於三個地方:記憶體、網路、磁碟,三者都所屬於不同的硬體裝置:記憶體條、網路卡、磁碟)。對於不同的硬體裝置,作業系統提供的系統讀取寫入呼叫API是不同的,但 [輸入/輸出] 這個操作基本是相同的(好比不同關聯式資料庫中連結地址字首名稱是不相同的,可是運算元據庫的sql語句都是相同的一樣)。於是Java程式中,對於資料的 [輸入/輸出] 操作都是以“流”這種抽象的方式進行(這也是遵循了物件導向的程式設計思想)。裝置可以是檔案,網路,記憶體等。簡而言之,就是把 [輸入/輸出] 不同裝置資料的操作步驟抽象成了“流”這個概念。

好了,費了大把的力氣搞懂了“流”這個抽象概念,接下來我們來了解一下,最常見的一種操作,要讀取或往磁碟檔案中寫入資料,第一步就是要訪問磁碟檔案。為了完成這個操作,java專門提供了一個File檔案類。顧名思義,這個類可以用來建立、判斷某個路徑檔案是否存在等常見的一些檔案操作。用法如下所示:

File file = new File(path);  //path為目錄路徑
String[] list = file.list(); //列出目錄下所有檔名

注意:這個類雖然很有用,但是違背了Java (once write run anywhere)的口號,因為在類unix系統的path和windows系統的path絕對路徑格式顯然是不一致的。這就導致了不同作業系統之間的可移植問題。這個問題在JDK1.7中引入新的功能類得以解決。詳細資訊可以參考JDK1.7新io的api。

訪問到了具體檔案,接下就是向檔案寫入或者讀取資料了,為此Java提供了位元組流輸入類InputStream,這個類是所有輸入位元組流的父類,也就是所有硬體裝置抽象出來的輸入操作。根據資料來源[包括:位元組陣列、String物件、檔案、管道、一個由其他種類流組成的序列、其他資料來源]的不同,Java為我們提供了每種資料來源相對應的InputStream子類。具體如下表所示:

InputStream子類                                      資料來源

ByteArrayInputStream                      允許將記憶體緩衝區當InputStream

StringBufferInputStream                   將String轉化成InputStream

FileInputStream                           用於從檔案中讀取InputStream

PipedInputStream                          實現管道化

SequenceInputStream                     將兩個或多個InputStream物件轉化成單一的InputStream

FilterInputStream                  抽象類,作為裝飾器的介面,為其他InputStream類提供有用的功能

從上表裡面,可以看到兩個常見的輸入流,分別從記憶體和檔案中取得資料。一般情況下,用的最多的就屬FileInputStream了,對於這個類大家也一定很眼熟,這裡就不在給出程式碼示例了。

有了輸入流那就一定有輸出流,畢竟有進有出才能流動不是? Java中的輸出流:OutputStream和InputStream相類似,根據輸出裝置不同,它也提供了不同的子類,具體如下表所示:

OutputStream子類                            資料來源

ByteArrayOutputStream                 在記憶體中建立緩衝區

FileOutputStream                      用於將位元組流寫入磁碟檔案

PipedOutputStream                     管道化概念

FilterOutputStream                   抽象類作為“裝飾器”的介面,為其他OutputStream提供有用功能

說完了[輸入/輸出]流的基本實現,如果細心的話,大家會發現在位元組流輸入子類(InputStream)和位元組輸出子類(OutputStream)都有一個FilterXXXputStream類,那這個類是幹嘛的呢?

用過IO的同學可能知道,計算機讀取檔案的定址操作是很浪費時間的,但是預設提供的InputStream和OutputStream都只是提供了最原始的檔案讀取、寫入操作(每次都只能一個位元組一個位元組的讀取/寫入),這顯然很浪費效能,為了提高效率,需要在原始操作上新增一些特性(這些特性一般是根據情景需要,自主選擇的),就像我們買了一套新房,怎麼裝修是我們自己的事情,開發商如果只提供蓋好的簡易房,而不提供裝修房子的功能,那麼對我們來說,無疑就是噩夢(好吧!那也得先有房再說!),顯然java開發團隊都是優秀的開發商,所以在提供原始位元組[輸入/輸出]子類操作的基礎上,他們又為我們提供了一些裝修方案(在此,為偉大的java API開發者致敬!)。

FilterInputStream和FilterOutputStream就是用來提供裝飾器類的介面,以控制特定的輸入流(InputStream)和輸出流(OutputStream)行為的兩個類。

FilterInputStream和FilterOutputStream在內部修改了InputStream和OutputStream的行為方式,具體修改細節見下表:

FilterInputStream                    型別

DataInputStream              與DataOutputStream搭配使用,可以按照移植方式從流中讀取基本資料型別(int,char,long等)

BufferedInputStream          使用緩衝區

LineNumberInputStream        跟蹤輸入流中的行號。可呼叫getLineNumber()和setLineNumber(int …)

PushbackInputStream         能彈出一個位元組緩衝區

FilterOutputStream             型別

DataOutputStream          與DataInputStream搭配使用

PrintStream               用於產生格式化輸出,處理顯示

BufferedOutputStream      使用緩衝區,可以呼叫flush()清空緩衝區。

下面就給出一個讀取“path”路徑下磁碟檔案的緩衝位元組輸入流:

BufferedInputStream bufferInputStream = new BufferedInputStream(new FileInputStream(new File("path")));

只需要將原始的FileInputStream流放入這些具體的裝飾器裡面,就能得到我們需要的帶有新特性的[輸入/輸出]流,看起來是不是很棒?

好了,到現在為止,我們已經瞭解JDK1.0所提供的I/O API,對!大家沒有看錯,是JDK1.0就自帶的I/O操作API,雖然Java現在已經到了1.8,中間也提供了很多更強大的I/O API(以前我以為1.0提供的就是JavaIO所有的API,請上天原諒我此前的無知。)可是目前大部分程式還是隻用到了JDK1.0所提供的IO API(所以還沒有體會新加入IO魅力的不防去嘗試一下!)。

位元組流[輸入/輸出]類,是以位元組(二進位制)方式,進行讀取和寫入的,在每次讀取文字檔案中,都不得不用(char)強轉一下,這顯然很煩人,而且沒有多大意義,於是,在Java1.1中JavaIO 加入了對文字[輸入/輸出]的字元流:Reader和Writer。

它們提供了相容unicode與面向字元的I/O功能。在功能上位元組流和字元流基本相同,只不過一個是以二進位制,另一個是以字元傳輸的。所以這裡就不在詳細介紹了,只給出字元流和位元組流相應類之間的對應關係。如下表:

Java1.0類                          Java1.1類
InputStream                       Reader
OutputStream                      Writer
FileInputStream                   FileReader
FileOutputStream                  FileWriter
StringBufferInputStream(棄用)      StringReader  StringWriter
ByteArrayInputStream              CharArrayReader
ByteArrayOutputStream             CharArrayWriter
PipedInputStream                  PipedReader
PipedOutputStream                 PipedWriter

Reader和Write的更改流行為:

Java1.0類                 Java1.1類

FilterInputStream        FilterReader
FilterOutputStream       FilterWriter
BufferedInputStream      BufferedReader
BufferedOutputStream     BufferedWriter
DataInputStream          DataReader
PrintStream              PrintWriter
LineNumberInputStream    LineNumberReader
StreamTokenizer          StreamTokenizer
PushbackInputStream      PushbackReader 

以下Java1.0類在Java1.1沒有相應類: DataOutputStream、File、RandomAccessFile、SequenceInputStream

注意:字元流和位元組流並沒有誰高誰低之別,雖然字元流是在jdk1.1提出,但其是為了解決文字層面的傳輸。而位元組流更傾向於處理圖片,壓縮檔案等情況。

顯然,流的[輸入/輸出]操作,提供各自不同的兩個類,在很大程度上,已經可以滿足我們的要求,可在一些特定的場合下,我們要求一個類能兼有[輸入/輸出]這兩種特性。這個時候,我們就要用到一個特殊的自我獨立類:RandomAccessFile

從本質上來說,RandomAccessFile類似於將DataInputStream和DataOutputStream組合起來使用,並新增了一些方法。不過在JDK1.4中,RandomAccessFile大多數功能由NIO儲存對映檔案所取代,所以在這裡就不做過多介紹了,感興趣的讀者可以在網上查詢這個類的相關資料。

接下來,就是讓我自慚形穢的在JDK1.4中提供的Java NIO了,以前總傻逼的認為對java已經瞭如執掌,但自從瞭解到Java NIO真是為自己的無知無語凝咽啊!當時的自己可真是無知者無畏啊!

在jdk1.4之前,因為Java IO的限制,服務端程式的開發是不足以和c、c++抗衡的,但自從加入了NIO,可以負責任的說,因為網路傳輸的限制,Java在web伺服器端的開發已經足夠和c++這種傳統的web服務端開發語言一較高下了,那麼接下來,就讓我們懷著朝聖的心態,來看看JDK1.4所提供的NIO到底有多麼的神奇。

JDK1.4的Java.nio.*包中引入了新的Java I/O類庫,其目的在於提高速度(舊I/O包已經用新NIO重新實現過)。NIO速度的提高來自於使用結構更接近於作業系統執行I/O的方式:通道和緩衝器。

通道是用於磁碟檔案的一種抽象。它可以訪問諸如:記憶體對映、檔案加鎖機制、以及檔案間快速傳遞資料等作業系統特性。NIO只和緩衝器互動,緩衝器在和通道進行互動。而唯一一個能直接和通道互動的緩衝器就是ByteBuffer。

在Java裡,想要獲取檔案通道一般有兩種方式:

<1. 宣告式

FileChannel  channel =  new  FileChannel.open(path,options);

引數:

1.path 開啟通道檔案所在路徑。

2.options: StandardOpenOption列舉中的WRITE 、APPEND 、TRUNCATE_EXISTING、CREATE值

<2. 舊I/O庫三個類被修改用以產生FileChannel FileInputStream、 FileOutputStream 、RandomAccessFile

注意:字元模式不能產生通道。不過可用Java.nio.channels.channels類提供實用方法可在通道產生Reader和Writer。

FileChannel  fs  = new  FileOutputStream(“file”).getchannel();
fs.write(ByteBuffer.wrap(value));

getchannel()將會產生一個FileChannel。

warp()將已經存在的位元組陣列包裝到ByteBuffer。

對於只讀訪問,必須顯式使用靜態的allicate()方法來分配ByteBuffer緩衝區大小(重要)。

ByteBuffer  buffer = ByteBuffer. allicate(1024);

為了提高速度我們甚至可以用allocateDirect();產生一個與作業系統高度耦合的直接緩衝器(速度由各作業系統而定)。

一旦呼叫read()來告知FileChannel向ByteBuffer儲存位元組,就必須呼叫緩衝區上的flip(),讓它做好讓別人讀取位元組的準備。下面示例:就是將一個檔案通道讀取到的資訊寫入另一個通道檔案。

FileChannel  in  = new  FileInputStream(args[0]).getchannel();
FileChannel  out  = new  FileOutputStream(args[1]).getchannel();
ByteBuffer  buffer = ByteBuffer. allicate(1024);
while(in.read(buffer)!=-1){
buffer.flip();
out.write(buffer);
buffer.clear();      }

備註:這個例子只是為了演示使用方法,實際上真正應用的話,特殊方法transferTo()和transferFrom()是允許一個通道和另一個通道直接相連的。

從上面的例子中,大家就可以瞭解到,在NIO中,緩衝器的重要作用了,那麼接下來我們就深入瞭解一下緩衝器的相關細節:

Buffer由資料和可以高效訪問及操作這些資料的四個索引構成。四個索引分別是:mark(標記) position(位置) limit(界限) capacity(容量)。

復位索引和查詢資料值方法

capacity()       返回緩衝區容量

clear()          清空緩衝區,將position設為0,limit設為容量,可以呼叫此方法覆寫緩衝區。

flip()           將limit設為position,position設為0,用於準備從緩衝區讀取已經寫入的資料

limit()          返回limit值

limit(int limit) 設定limit值

mark()           將mark設為position

position()       返回position值

position(int pos) 設定position值

remaining()      返回limit-position

hasremaining()   若有介於position和limit之間元素則返回true

remain()         和flip()不同它只設定position為0不改變limit值

如果我們想檢視底層的ByteBuffer的型別就可以通過檢視緩衝器來檢視,下面來先看下它的定義:

檢視緩衝器(view buffer),可以通過某個特定的基本資料型別的視窗檢視底層的ByteBuffer。示例如下:

ByteBuffer  buffer = ByteBuffer. allicate(1024);
IntBuffer  intbuffer = buffer.asIntBuffer();
intbuffer.put(new Int[]{1,2,3});
while(intbuffer. hasremaining()){
int  i = intbuffer.get();
system.out.println(i);
}

通過在同一個ByteBuffer上建立不同的檢視緩衝器,將同一位元組序列可翻譯成short、int、float、long 和double資料型別。

緩衝器容納的普通位元組,為了把它轉化成字元,要麼在輸入時對其進行編碼。要麼將其從緩衝器輸出時對它們進行解碼,用Java.nio.charset.charset類實現這些功能。

講到這裡,關於Java IO就先介紹到這裡,由於字數限制,其實還有很多內容沒有講到。我這邊已經整理好了,如果大家喜歡這篇乾貨文章的話,下一篇就將沒有講完的都彌補上!如果不喜歡也就不發了!同時也歡迎大家的批評指正,以及建議!畢竟本人是一個在中二路上漸行漸遠的程式設計師!

相關文章