終於結束了集合的學習,今天我們就開始學習 I/O的操作了。 I/O 系列的內容分為 I/O概述、字元流、位元組流。今天要學的是 I/O和字元流的操作
由於概述篇幅較短,所以就把概述壓縮到這裡來了。
I/O 概述
I/O:即 Input Output,輸入輸出的意思。
- IO 流用來處理裝置之間的資料傳輸。
- JAVA 對資料的操作都是通過流的方式
- JAVA 用於操作流的物件都在 IO 包裡面
- 流的操作分兩種:字元流、位元組流
- 流的流向分兩種:輸入流、輸出流
對資料的操作,其實就是對 File 檔案。我偷了一張祖師爺傳下來的圖來描述 IO 流類結構關係。
從圖中可以看出,都是從這以下四個類中派生出來的子類,子類的型別也好區分,字尾都是抽象基類名。
- 位元組流抽象基類
- InputStream
- OutputStream
- 字元流抽象基類
- Reader
- Writer
IOException
IO 異常大致分為三種,一是 IO 異常、二是找不到檔案異常、三是沒有物件異常。
因此,我們在異常處理的時候,比較嚴峻的寫法應該這樣
FileWriter fileWriter = null;
try {
fileWriter = new FileWriter("demo.txt");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
try {
if (fileWriter != null) {
fileWriter.close();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}複製程式碼
字元流-Reader/Writer
Reader 和 Writer的定義
Reader: 用於讀取字元流的抽象類。子類必須實現的方法只有 read(char[], int, int) 和 close()。但是,多數子類將重寫此處定義的一些方法,以提供更高的效率和/或其他功能。
Writer: 寫入字元流的抽象類。子類必須實現的方法僅有 write(char[], int, int)、flush() 和 close()。但是,多數子類將重寫此處定義的一些方法,以提供更高的效率和/或其他功能。
字元流的讀寫FileReader 和 FileWriter。
先看看基本運用吧~
try {
// 建立讀取流和寫入流
FileReader fileReader = new FileReader("raw.txt");
FileWriter fileWriter = new FileWriter("target.txt");
// 讀取單個字元,自動往下讀
int ch;
while ((ch = fileReader.read()) != -1) {
fileWriter.write(ch);
}
fileReader.close();
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}複製程式碼
每次讀取一個字元再執行寫入操作,效率比較慢,我們可以嘗試一次讀取更多的資料再一次性寫入。
try {
// 建立讀取流和寫入流
FileReader fileReader = new FileReader("raw.txt");
FileWriter fileWriter = new FileWriter("target.txt");
// 讀取1024個字元,自動往下讀
char[] buf = new char[1024];
while (fileReader.read(buf) != -1) {
fileWriter.write(buf);
}
fileReader.close();
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}複製程式碼
以上兩種方法實現了檔案的讀寫操作,將 raw.txt 讀取寫入 targer.txt 檔案裡面。
同時,這步操作也可以當成是檔案的拷貝。
需要注意的是:
- 讀取檔案時,如果路徑對應的檔案不存在會報 IOException
- 寫入檔案時,如果路徑對應的檔案不存在會自動在該目錄下建立檔案
如果要實現追加寫入檔案的操作,比如將 raw1.txt 和 raw2.txt 共同寫入 target.txt 裡面,只需要在建立 FileWriter 的時候使用兩個引數的構造方法即可,如:FileWriter fileWriter = new FileWriter("target.txt",true);
至於原始碼讀寫的實現,我簡單說一下吧。其實 FileReader/FileWriter 的構造方法裡面都有 new FileInputStream/FileOutputStream 的物件,然後繼承的是InputStreamReader/OutputStreamWriter.這說明啥,我想大家心裡應該有數了吧,其實還是呼叫了位元組流的讀取,然後使用StreamEncoder/StreamDecoder進行編碼解碼操作。
具體流程是這樣的,這裡每次讀/寫
太長了,我只說讀取操作了,反正都是相對的。
- 首先建立 FileReader 物件,FileReader物件構造方法裡面建立了FileInputStream。然後再使用 FileInputStream 作為引數,建立 StreamDecoder物件。
- 呼叫FileReader.read()讀取一個字元,實際上就是呼叫了 StreamDecoder 同時讀取兩個位元組,再使用這兩個位元組組成一個字元返回。
字元流的緩衝區
字元流的緩衝區,提高了對資料的讀寫效率,他有兩個子類
- BufferedWriter
- BufferedReader
緩衝區要結合流才可以使用
在流的基礎上對流的功能進行了增強
原始碼就不帶著大家一起讀了,我給大家分析一下 BufferedWriter 的思想。以下內容劃重點,期末考試要考!
要想理解 BufferReader,就要先理解它的思想。BufferReader 的作用是為其它Reader 提供緩衝功能。建立BufferReader 時,我們會通過它的建構函式指定某個Reader 為引數。BufferReader 會將該Reader 中的資料分批讀取,每次讀取一部分到緩衝中;操作完緩衝中的這部分資料之後,再從Reader 中讀取下一部分的資料。
為什麼需要緩衝呢?原因很簡單,效率問題!緩衝中的資料實際上是儲存在記憶體中,而原始資料可能是儲存在硬碟中;而我們知道,從記憶體中讀取資料的速度比從硬碟讀取資料的速度至少快10倍以上。
那幹嘛不乾脆一次性將全部資料都讀取到緩衝中呢?第一,讀取全部的資料所需要的時間可能會很長。第二,記憶體價格很貴,容量不想硬碟那麼大。
通過字元流緩衝區來複制檔案操作
還用上面那個案例
BufferedReader bufferedReader;
BufferedWriter bufferedWriter;
try {
// 建立讀取流和寫入流
bufferedReader = new BufferedReader(new FileReader("raw.txt"));
bufferedWriter = new BufferedWriter(new FileWriter("target.txt", true));
// 讀取一行字串
String line;
while ((line = bufferedReader.readLine()) != null) {
bufferedWriter.write(line);
}
bufferedReader.close();
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}複製程式碼
沒什麼特別的,很簡單
仿寫一個 readLine
上文中,出現了一個 readLine 方法,可以一次讀取一行字串。
其實這個一次讀取一行字串還是蠻有用的,比如說讀取一些Key-value 形式的配置檔案。
我們來看看 JDK 中關於 readLine 的描述
讀取一個文字行。通過下列字元之一即可認為某行已終止:換行 ('\n')、回車 ('\r') 或回車後直接跟著換行。
返回:包含該行內容的字串,不包含任何行終止符,如果已到達流末尾,則返回 null
丟擲:IOException - 如果發生 I/O 錯誤
所以,我們自己要封裝一個 readLine,只需要判斷讀取到的字元是否為'\n'、'\r',再一次性返回就行了。
class MyBufferReaderLine {
private Reader fr;
public MyBufferReaderLine(Reader fr) {
this.fr = fr;
}
// 一次讀取一行的方法
public String readLine() throws IOException {
// 定義臨時容器
StringBuilder sb = new StringBuilder();
int ch = 0;
while ((ch = fr.read()) != -1) {
if (ch == '\r' || ch == '\n') {
return sb.toString();
} else {
sb.append((char) ch);
}
}
if(sb.length() != 0){
return sb.toString();
}
return null;
}
public void close() throws IOException {
fr.close();
}
}複製程式碼
程式碼實現很簡單,就是參考 BufferedReader 寫的一個包裝類。
LineNumberReader
你們先感受一下這個類的用法
FileReader fr;
try {
fr = new FileReader("test.txt");
LineNumberReader lnr = new LineNumberReader(fr);
String line;
while ((line = lnr.readLine()) != null) {
System.out.println(lnr.getLineNumber() + ":" + line);
}
lnr.close();
} catch (IOException e) {
e.printStackTrace();
}複製程式碼
這貨和我們剛剛手寫的MyBufferReaderLine 基本沒啥區別,繼承自BufferedReader ,然後多了一個lineNumber 屬性,lineNumber用來記錄當前行數。
實現沒有什麼意義,我們在MyBufferReaderLine 上新增一個欄位lineNumber,每次 readLine 成功之後 lineNumber++ 即可。
但是,為什麼要講他呢,因為他和 BufferedReader 一樣,也是一個包裝類啊。
包裝類就是裝飾設計模式啊~
好了,你們都知道了,就是提一下裝飾模式。
裝飾模式
當想要對已有的物件進行功能增強時,可以定義一個類,將已有物件傳入,並且提供加強功能,那麼自定義的該類就稱為裝飾類。
裝飾模式又名包裝(Wrapper)模式。裝飾模式以對客戶端透明的方式擴充套件物件的功能,是繼承關係的一個替代方案。
可能有的同學會問,那麼為什麼不用繼承呢?我寫一個新的類,繼承、再新增擴充套件方法或者重寫方法也可以實現呀。
假如咖啡廳賣咖啡,運營可一段時間,發現客戶對咖啡的甜度有不同的需求,有如下三種需求加少量糖、一般糖、多糖。程式碼實現可以給 Coffee 新增一個糖量的屬性,但是一開始設計 Coffee 這個類的時候沒有加這個屬性,根據開發守則,我們是不應該去修改原 Coffee 類,此時可以選擇新增三個子類,LowSurgeCoffee、MidSurgeCoffee、HightSurgeCoffee,或者使用裝飾模式,新增3個不同糖量的 SurgeDecorator。此時,使用裝飾模式和繼承沒什麼區別。但是執行了一段時間之後,需求又加了,咖啡需要新增口味卡布奇諾和摩卡。此時再組合之前的三種糖量,一共需要9個咖啡類。但是如果使用裝飾模式,只需要新增摩卡和卡布奇諾裝飾器就行了。一共6個裝飾類。之後再擴充套件新的口味需要的子類是乘算,但是如果是裝飾類,就只是加算。
以上這個例子沒有程式碼實現,因為我懶。。。。。。
針對的問題:
動態地給一個物件新增一些額外的職責。就增加功能來說,Decorator模式相比生成子類更為靈活。不改變介面的前提下,增強所考慮的類的效能。
何時使用
- 需要擴充套件一個類的功能,或給一個類增加附加責任。
- 需要動態的給一個物件增加功能,這些功能可以再動態地撤銷。
- 需要增加一些基本功能的排列組合而產生的非常大量的功能,從而使繼承變得不現實。
優缺點
- 裝飾者模式比繼承要靈活,避免了繼承體系的臃腫,而且降低了類與類之間的關係
- 裝飾類因為增強已有物件,具備功能和已有的想相同,只不過提供了更強的功能,所以裝飾類和被裝飾類通常屬於一個體系中的