一、什麼是IO
Java中I/O操作主要是指使用Java進行輸入,輸出操作。 Java所有的I/O機制都是基于資料流進行輸入輸出,這些資料流表示了字元或者位元組資料的流動序列。
Java的I/O流提供了讀寫資料的标準方法。任何Java中表示資料源的對象都會提供以資料流的方式讀寫它的資料的方法。
二、Java IO模型 :
java I/O 的設計使用到了 Decorator(裝飾器)模式,按功能劃分Stream,您可以動态裝配這些 Stream,以便獲得您需要的功能。
例如,您需要一個具有緩沖的檔案輸入流,則應當組合使用FileInputStream和BufferedInputStream。
三、資料流的基本概念
資料流是一串連續不斷的資料的集合,就象水管裡的水流,在水管的一端一點一點地供水,而在水管的另一端看到的是一股連續不斷的水流。
資料寫入程式可以是一段、一段地向資料流管道中寫入資料,這些資料段會按先後順序形成一個長的資料流。
對資料讀取程式來說,看不到資料流在寫入時的分段情況,每次可以讀取其中的任意長度的資料,但隻能先讀取前面的資料後,再讀取後面的資料。
不管寫入時是将資料分多次寫入,還是作為一個整體一次寫入,讀取時的效果都是完全一樣的。
外存、記憶體、緩存的比較
基本流:
輸入流:
輸出流:
為什麼設計成資料流呢?
Input Stream不關心資料源來自何種裝置(鍵盤,檔案,網絡)
Output Stream不關心資料的目的是何種裝置(鍵盤,檔案,網絡)
采用資料流的目的就是使得輸出輸入獨立于裝置。
四、I/O體系結構

簡單介紹下上圖:
你有沒有發現,都是成對出現的, 初學者就很容易混淆,分不清是位元組流還是字元流,大家隻需要看最後這個單詞,如果是 Stream 的話就是位元組流,如果是 Reader/Writer 的話就是字元流。
在整個Java.io包中最重要的就是5個類和一個接口。5個類指的是File、OutputStream、InputStream、Writer、Reader;一個接口指的是Serializable.掌握了這些IO的核心操作那麼對于Java中的IO體系也就有了一個初步的認識了
Java I/O主要包括如下幾個層次,包含三個部分:
1.流式部分――IO的主體部分;
2.非流式部分――主要包含一些輔助流式部分的類,如:File類、RandomAccessFile類和FileDescriptor等類;
3.其他類–檔案讀取部分的與安全相關的類,如:SerializablePermission類,以及與本地作業系統相關的檔案系統的類,如:FileSystem類和Win32FileSystem類和WinNTFileSystem類。
流式部分主要類:
Java中字元是采用Unicode标準,一個字元是16位,即一個字元使用兩個位元組來表示。為此,JAVA中引入了處理字元的流。
對檔案進行操作:
對管道進行操作:
PipedInputStream的一個執行個體要和PipedOutputStream的一個執行個體共同使用,共同完成管道的讀取寫入操作。主要用于線程操作。
位元組/字元數組:
Buffered緩沖流:
轉化流:
資料流:
列印流:
對象流:
序列化流:
注:
使用對象流需要實作Serializable接口,否則會報錯。
而若用transient關鍵字修飾成員變量,不寫入該成員變量,若是引用類型的成員變量為null,值類型的成員變量為0.
非流式部分主要類:
File(檔案特征與管理):用于檔案或者目錄的描述資訊,例如生成新目錄,修改檔案名,删除檔案,判斷檔案所在路徑等。
RandomAccessFile(随機檔案操作):它的功能豐富,可以從檔案的任意位置進行存取(輸入輸出)操作。
File類:
在Java語言的java.io包中,由File類提供了描述檔案和目錄的操作與管理方法。
但File類不是InputStream、OutputStream或Reader、Writer的子類,因為它不負責資料的輸入輸出,而專門用來管理磁盤檔案與目錄。
作用:File類主要用于命名檔案、查詢檔案屬性和處理檔案目錄。
File類共提供了四個不同的構造函數,以不同的參數形式靈活地接收檔案和目錄名資訊。構造函數:
1)File (String pathname)
2)File(URI uri)
3)File (String parent , String child)
4)File (File parent , String child)
一個對應于某磁盤檔案或目錄的File對象一經建立, 就可以通過調用它的方法來獲得檔案或目錄的屬性。
例子:輸出一個目錄中的所有檔案名(目錄可能是多級目錄,如a目錄中有b、c目錄。。。)
FileUtils.java
Main.java
結果:
這裡就不多解釋了,應該比較簡單,代碼中也有詳細注釋。
五、幾種流的介紹
1、InputStream抽象類
InputStream 為位元組輸入流,它本身為一個抽象類,必須依靠其子類實作各種功能,此抽象類是表示位元組輸入流的所有類的超類。
繼承自InputStream 的流都是向程式中輸入資料的,且資料機關為位元組(8bit);
InputStream是輸入位元組資料用的類,是以InputStream類提供了3種重載的read方法.Inputstream類中的常用方法:
(1) public abstract int read( ):讀取一個byte的資料,傳回值是高位補0的int類型值。若傳回值=-1說明沒有讀取到任何位元組讀取工作結束。
(2) public int read(byte b[ ]):讀取b.length個位元組的資料放到b數組中。傳回值是讀取的位元組數。該方法實際上是調用下一個方法實作的
(3) public int read(byte b[ ], int off, int len):從輸入流中最多讀取len個位元組的資料,存放到偏移量為off的b數組中。
(4) public int available( ):傳回輸入流中可以讀取的位元組數。注意:若輸入阻塞,目前線程将被挂起,如果InputStream對象調用這個方法的話,它隻會傳回0,這個方法必須由繼承InputStream類的子類對象調用才有用,
(5) public long skip(long n):忽略輸入流中的n個位元組,傳回值是實際忽略的位元組數, 跳過一些位元組來讀取
(6) public int close( ) :我們在使用完後,必須對我們打開的流進行關閉.
主要的子類:
2、OutputStream抽象類
OutputStream提供了3個write方法來做資料的輸出,這個是和InputStream是相對應的。
(1) public void write(byte b[ ]):将參數b中的位元組寫到輸出流。
(2)public void write(byte b[ ], int off, int len) :将參數b的從偏移量off開始的len個位元組寫到輸出流。
(3) public abstract void write(int b) :先将int轉換為byte類型,把低位元組寫入到輸出流中。
(4)public void flush( ) : 将資料緩沖區中資料全部輸出,并清空緩沖區。
(5) public void close( ) : 關閉輸出流并釋放與流相關的系統資源。
流結束的判斷:方法read()的傳回值為-1時;readLine()的傳回值為null時。
3、FileInputStream檔案輸入流
FileInputStream可以使用read()方法一次讀入一個位元組,并以int類型傳回,或者是使用read()方法時讀入至一個byte數組,byte數組的元素有多少個,就讀入多少個位元組。
在将整個檔案讀取完成或寫入完畢的過程中,這麼一個byte數組通常被當作緩沖區,因為這麼一個byte數組通常扮演承接資料的中間角色。
作用:以檔案作為資料輸入源的資料流。或者說是打開檔案,從檔案讀資料到記憶體的類。
使用方法:
FileInputStream fis = new FileInputStream(“E:\a.txt”);
或 FileInputStream fis = new FileInputStream(“E:/a.txt”);
當然也可以傳一個 File ,它還有好多個構造器。大家可以看看源碼應該就懂了。
例子:
将a.txt的内容顯示在顯示器上
如果将 System.out.print((char)a) 改成 System.out.print(a) 又會是怎樣呢?
怎麼回事呢?不是位元組流嗎?讀出來不就是一個位元組,怎麼輸出一個整數了呢?
答:read()方法确實是讀取一個位元組,但傳回值是 int 類型的,會将取出來的一個位元組高位會補 0 成一個 int 型。是以輸出來的應該這個資料的 ASCII 碼。
使用鍵盤輸入一段中文并輸出到控制台,看看會是怎樣?
可以猜到應該會輸出一些整數即 ASCII 碼,因為讀的時候是一個位元組一個位元組的讀,而中文又不止一個位元組,到底要幾個位元組來表示,這要取決于編碼集(我這裡的是拆成了三個位元組,兩個位元組是用來标記一行的)。反正會拆成幾個位元組來讀。
那麼如果強轉成 char 類型又會怎樣呢?
我們看到輸入哈,輸出å,但這并不是我們想要的。為什麼會這樣呢?我們來看一下 å 的 ASCII 碼,剛好是 229,這我們就知道它隻取了第一位元組ASCII碼對應的字元。
ASCII 碼表可在文章最後檢視。
4、FileOutputStream檔案輸出流
FileOutputStream類用來處理以檔案作為資料輸出目的資料流;一個表示檔案名的字元串,也可以是File或FileDescriptor對象。
作用:用來處理以檔案作為資料輸出目的資料流;或者說是從記憶體區讀資料到檔案
建立檔案流的方式:
1)FileOutputStream(File file)
建立一個向指定 File 對象表示的檔案中寫入資料的檔案輸出流。
例: File f=new File (“d:/myjava/write.txt “);
FileOutputStream out= new FileOutputStream (f);
2)FileOutputStream(File file, boolean append)
建立一個向指定 File 對象表示的檔案中寫入資料的檔案輸出流。 append表示内容是否追加
3)FileOutputStream(FileDescriptor fdObj)
建立一個向指定檔案描述符處寫入資料的輸出檔案流,該檔案描述符表示一
個到檔案系統中的某個實際檔案的現有連接配接。
4)FileOutputStream(String name)
建立一個向具有指定名稱的檔案中寫入資料的輸出檔案流。
例:FileOutputStream out=new FileOutputStream(“d:/myjava/write.txt “);
5)FileOutputStream(String name, boolean append)
建立一個向具有指定 name 的檔案中寫入資料的輸出檔案流。 append表示内容是否追加
(1)檔案中寫資料時,若檔案已經存在,則覆寫存在的檔案;
(2)的讀/寫操作結束時,應調用close方法關閉流。
例子:
使用鍵盤輸入一段内容,将内容儲存在檔案write.txt中
有同學會問了,剛才上面漢字的讀取不是一個位元組一個位元組讀,存到檔案中的應該也是一個位元組一個位元組的,那麼檢視的時候不應該是亂碼嗎?
答:從鍵盤讀取的時候确實是一個位元組一個位元組讀取,存的時候也是一個位元組一個位元組存,隻不過它會加點标志(不同的編碼集标記的方式可能不一樣)。(我這裡的編碼是每個漢字用三個位元組編碼,用兩個位元組來标記一行),到時候輸出的時候它就會根據标記和漢字所占位元組數來拼接成漢字。
我們來輸出看一下:
這裡的結果是哈哈(兩行)哈哈的輸出結果,當然也分别測了四種情況,為了更好的對比:
當 a.txt 的内容為哈、哈哈、哈哈、哈哈(兩行)哈哈的時候,存的時候各是什麼情況,代碼中都寫了,大家可以看看。可以發現标記一行的是13 10 這兩個位元組。不要問我這明明就是個整數不是占4個位元組,為什麼說是一個位元組?read() 的傳回值為 int,取出的 byte 高位補0成int。那麼怎麼拼接的呢?比如:内容為哈的時候,首先找到标記的位置,用(所占位元組數-2)/3=漢字的個數,而每個漢字占三個位元組,這不就可以三個三個位元組拼接出來了嗎。
5、緩沖輸入輸出流 BufferedInputStream/ BufferedOutputStream(也稱包裝流)
計算機通路外部裝置非常耗時。通路外存的頻率越高,造成CPU閑置的機率就越大。
為了減少通路外存的次數,應該在一次對外設的通路中,讀寫更多的資料。
為此,除了程式和流節點間交換資料必需的讀寫機制外,還應該增加緩沖機制。
緩沖流就是每一個資料流配置設定一個緩沖區,一個緩沖區就是一個臨時存儲資料的記憶體。
這樣可以減少通路硬碟的次數,提高傳輸效率。
BufferedInputStream:當向緩沖流寫入資料時候,資料先寫到緩沖區,待緩沖區寫滿後,系統一次性将資料發送給輸出裝置。
BufferedOutputStream :當從向緩沖流讀取資料時候,系統先從緩沖區讀出資料,待緩沖區為空時,系統再從輸入裝置讀取資料到緩沖區。
a、将檔案讀入記憶體:
将BufferedInputStream與FileInputStream相接
FileInputStream in=new FileInputStream( “file1.txt “);
BufferedInputStream bin=new BufferedInputStream(in);
b、将記憶體寫入檔案:
将BufferedOutputStream與 FileOutputStream相接
FileOutputStreamout=new FileOutputStream(“file2.txt”);
BufferedOutputStream bin=new BufferedInputStream(out);
c、鍵盤輸入流讀到記憶體
将BufferedReader與标準的資料流相接
InputStreamReader sin=new InputStreamReader (System.in) ;
BufferedReader bin=new BufferedReader(sin);
例子:從鍵盤輸入一串内容存到file1.txt檔案中
大家可以看到,我們剛才在鍵盤上輸入的内容并沒有存入到這個檔案中,這不是操蛋嗎?這樣隻是為了引出包裝流的一個特性即因為它是包裝流擁有緩存區,每次的讀取的資料都存在緩存區中,當緩存區滿了的時候才會寫入到硬碟上。但是預設的緩存區大小為8192個位元組,當然你也可以指定緩存區的大小。顯然剛才輸入的字元串并沒有裝滿。
指定緩存區的大小:包裝流的構造器
如 BufferedWriter(Writer out, int size)
建立一個使用給定大小輸出緩沖區的新緩沖字元輸出流。size為緩存區的大小
是以就需要我們手動的去重新整理緩沖區。調用包裝流的 flush() 方法。這個方法的作用就是将緩存區的内容寫入到硬碟上并清空緩存區。
當然能不能不寫 flush()方法也讓它重新整理呢?
答:可以,調用 close() 方法關閉流即可。當你調用 close()方法時它會先重新整理緩存區。這就是剛才上面所說要記得用完之後要關閉流。不過最好有使用到包裝流的時候兩個方法都記得寫上。
程式說明:
從鍵盤讀入字元,并寫入到檔案中BufferedReader類的方法:String readLine()
作用:讀一行字元串,以回車符為結束。
BufferedWriter類的方法:bout.write(String s,offset,len)
作用:從緩沖區将字元串s從offset開始,len長度的字元串寫到某處。
當然包裝流中還有許多方法(主要方法):
6、Writer/Reader字元流
a、Reader抽象類
用于讀取字元流的抽象類。子類必須實作的方法隻有 read(char[], int, int) 和 close()。但是,多數子類将重寫此處定義的一些方法,以提供更高的效率和/或其他功能。
子類簡單介紹:
主要方法:
b、 Writer抽象類
寫入字元流的抽象類。子類必須實作的方法僅有 write(char[], int, int)、flush() 和 close()。但是,多數子類将重寫此處定義的一些方法,以提供更高的效率和/或其他功能。
将字元類型資料寫入檔案,使用預設字元編碼和緩沖器大小。
Public FileWrite(file f);
Public CharArrayWrite();
3) PrintWrite:生成格式化輸出
public PrintWriter(outputstream os);
4) filterWriter:用于寫入過濾字元流
protected FilterWriter(Writer w);
5) PipedWriter:與PipedOutputStream對應
當然這些子類我這裡就不一一去詳細介紹了,我相信大家看懂了字元流的用法,再去學習字元流的話應該不在話下的。
7、輸入流和輸出流的應用
這裡就介紹的檔案輸入流和檔案輸出流一起使用的情況。
例子:利用程式将檔案a.CHM 拷貝到a.chm中。
這裡分别用了四種方式,也算對這些的流的速率的一個對比,這四種情況分别是:
1)基本位元組流每次讀一個位元組
2)基本位元組流每次讀一組位元組
3)高效位元組流每次讀一個位元組(即包裝流)
4)高效位元組流每次讀一組位元組(即包裝流)
當然測試的檔案盡量大點,不然差别不是很明顯,我這裡的a.CHM檔案的大小為35.2 MB。當然用時也跟計算機性能有點關系。當然檔案大點差别就更加明顯點。
在我電腦上的四種情況的用時為:
1.221196毫秒
2.346毫秒
3.220454毫秒
4.335毫秒
這裡1和3,2和4比較,可以發現包裝流更加高效,為什麼呢?
比較下2和4,把這個搞懂,其他的也就懂了。
這兩個從硬碟上讀取内容的時間應該差不多,但時間就差在寫的時間上了,緩存區一般在記憶體,記憶體的速度是很快的,将資料寫入到硬碟的速度是很慢的,是以我們隻需要減少寫入硬碟的次數即可。包裝流的預設緩存區大小為8192位元組,包裝流讀取8次才寫一次,是以包裝流的效率是大大增加了。
六、Java IO 的一般使用原則
一)按資料來源(去向)分類:
二)按是否格式化輸出分:
三)按是否要緩沖分:
四)按資料格式分:
五)按輸入輸出分:
六)特殊需要:
由于篇幅有限,還有一些細節的這裡就不多說了,想要了解的話可以加我QQ460821714一起讨論。
ASCII 碼對照表
下表列出了字元集中的 0 - 127 (0x00 - 0x7F)。
下表列出了字元集中的 128 - 255 (0x80 - 0xFF)。
數值 8、9、10 和 13 可以分别轉換為倒退符、制表符、換行符和回車符。這些字元都沒有圖形表示,但是對于不同的應用程式,這些字元可能會影響文本的顯示效果。
“空” 表示在目前平台上不支援的字元。