建議閱讀
重要性由高到低
本文簡要的這些文章做了一些總結
基本概念
IO,即in
和out
,也就是輸入和輸出,指應用程式和外部裝置之間的資料傳遞,常見的外部裝置包括檔案(file)、管道 (pipe)、網路連線 (network)。
流(Stream
),是一個抽象的概念,是指一連串的資料(字元或位元組),是以先進先出的方式傳送資訊的通道。
流的特性:
- 先進先出:最先寫入輸出流的資料最先被輸入流讀取到。
- 順序存取:可以一個接一個地往流中寫入一串位元組,讀出時也將按寫入順序讀取一串位元組,不能隨機訪問中間的資料。(RandomAccessFile除外)
- 只讀或只寫:每個流只能是輸入流或輸出流的一種,不能同時具備兩個功能,輸入流只能進行讀操作,對輸出流只能進行寫操作。在一個資料傳輸通道中,如果既要寫入資料,又要讀取資料,則要分別提供兩個流。
IO流主要的分類方式有以下3種:
- 按資料流的方向:輸入流、輸出流
- 按處理資料單位:位元組流、字元流
- 按功能:節點流、處理流
輸入流和輸出流
輸入與輸出是相對於應用程式而言的,比如檔案讀寫,讀取檔案是輸入流,寫檔案是輸出流,這點很容易搞反。
位元組流和字元流
位元組流和字元流的用法幾乎完成全一樣,區別在於位元組流和字元流所操作的資料單元不同,位元組流操作的單元是資料單元是8位的位元組,字元流操作的是資料單元為16位的字元。
為什麼要有字元流?
Java中字元是採用Unicode標準,Unicode 編碼中,一個英文為一個位元組,一箇中文為兩個位元組。
而在UTF-8編碼中,一箇中文字元是3個位元組。例如下面圖中,“雲深不知處”5箇中文對應的是15個位元組:-28-70-111-26-73-79-28-72-115-25-97-91-27-92-124
那麼問題來了,如果使用位元組流處理中文,如果一次讀寫一個字元對應的位元組數就不會有問題,一旦將一個字元對應的位元組分裂開來,就會出現亂碼了。為了更方便地處理中文這些字元,Java就推出了字元流。
位元組流和字元流的其他區別:
- 位元組流一般用來處理影像、視訊、音訊、PPT、Word等型別的檔案。字元流一般用於處理純文字型別的檔案,如TXT檔案等,但不能處理影像視訊等非文字檔案。用一句話說就是:位元組流可以處理一切檔案,而字元流只能處理純文字檔案。
- 位元組流本身沒有緩衝區,緩衝位元組流相對於位元組流,效率提升非常高。而字元流本身就帶有緩衝區,緩衝字元流相對於字元流效率提升就不是那麼大了。詳見文末效率對比。
節點流和處理流
節點流:直接運算元據讀寫的流類,比如FileInputStream
處理流:對一個已存在的流的連結和封裝,通過對資料進行處理為程式提供功能強大、靈活的讀寫功能,例如BufferedInputStream
(緩衝位元組流)
處理流和節點流應用了Java的裝飾者設計模式。
下圖就很形象地描繪了節點流和處理流,處理流是對節點流的封裝,最終的資料處理還是由節點流完成的。
緩衝流 是一個非常重要的處理流。
我們知道,程式與磁碟的互動相對於記憶體運算是很慢的,容易成為程式的效能瓶頸。減少程式與磁碟的互動,是提升程式效率一種有效手段。緩衝流,就應用這種思路:普通流每次讀寫一個位元組,而緩衝流在記憶體中設定一個快取區,緩衝區先儲存足夠的待運算元據後,再與記憶體或磁碟進行互動。這樣,在總資料量不變的情況下,通過提高每次互動的資料量,減少了互動次數。
然而緩衝流的效率卻不一定高,在某些情形下,緩衝流的效率反而更低
IO流常用物件
File 物件
在計算機系統中,檔案是非常重要的儲存方式。Java的標準庫java.io
提供了File
物件來操作檔案和目錄。
構造File物件時,既可以傳入絕對路徑,也可以傳入相對路徑。絕對路徑是以根目錄開頭的完整路徑,例如:
File f = new File("C:\\Windows\\notepad.exe");
注意Windows平臺使用\
作為路徑分隔符,在Java字串中需要用\\
表示一個\
。Linux平臺使用/
作為路徑分隔符:
File f = new File("/usr/bin/javac");
傳入相對路徑時,相對路徑前面加上當前目錄就是絕對路徑:
// 假設當前目錄是C:\Docs
File f1 = new File("sub\\javac"); // 絕對路徑是C:\Docs\sub\javac
File f3 = new File(".\\sub\\javac"); // 絕對路徑是C:\Docs\sub\javac
File f3 = new File("..\\sub\\javac"); // 絕對路徑是C:\sub\javac
可以用.
表示當前目錄,..
表示上級目錄。
File物件有3種形式表示的路徑,一種是getPath()
,返回構造方法傳入的路徑,一種是getAbsolutePath()
,返回絕對路徑,一種是getCanonicalPath
,它和絕對路徑類似,但是返回的是規範路徑。
public class Main {
public static void main(String[] args) throws IOException {
File f = new File("..");
System.out.println(f.getPath());
System.out.println(f.getAbsolutePath());
System.out.println(f.getCanonicalPath());
}
}
..
/app/..
/
絕對路徑可以表示成C:\Windows\System32\..\notepad.exe
,而規範路徑就是把.
和..
轉換成標準的絕對路徑後的路徑:C:\Windows\notepad.exe
。
檔案和目錄
File
物件既可以表示檔案,也可以表示目錄。特別要注意的是,構造一個File
物件,即使傳入的檔案或目錄不存在,程式碼也不會出錯,因為構造一個File
物件,並不會導致任何磁碟操作。只有當我們呼叫File
物件的某些方法的時候,才真正進行磁碟操作。
例,呼叫isFile()
,判斷該File
物件是否是一個已存在的檔案,呼叫isDirectory()
,判斷該File
物件是否是一個已存在的目錄。
用File
物件獲取到一個檔案時,還可以進一步判斷檔案的許可權和大小:
boolean canRead()
:是否可讀;boolean canWrite()
:是否可寫;boolean canExecute()
:是否可執行;long length()
:檔案位元組大小。
建立和刪除檔案
當File物件表示一個檔案時,可以通過createNewFile()
建立一個新檔案,用delete()
刪除該檔案:
File file = new File("/path/to/file");
if (file.createNewFile()) {
// 檔案建立成功:
// TODO:
if (file.delete()) {
// 刪除檔案成功:
}
}
有些時候,程式需要讀寫一些臨時檔案,File物件提供了createTempFile()
來建立一個臨時檔案,以及deleteOnExit()
在JVM退出時自動刪除該檔案。
public class Main {
public static void main(String[] args) throws IOException {
File f = File.createTempFile("tmp-", ".txt"); // 提供臨時檔案的字首和字尾
f.deleteOnExit(); // JVM退出時自動刪除
System.out.println(f.isFile());
System.out.println(f.getAbsolutePath());
}
}
遍歷檔案和目錄
當File物件表示一個目錄時,可以使用list()
和listFiles()
列出目錄下的檔案和子目錄名。listFiles()
提供了一系列過載方法,可以過濾不想要的檔案和目錄:
public class Main {
public static void main(String[] args) throws IOException {
File f = new File("C:\\Windows");
File[] fs1 = f.listFiles(); // 列出所有檔案和子目錄
printFiles(fs1);
File[] fs2 = f.listFiles(new FilenameFilter() { // 僅列出.exe檔案
public boolean accept(File dir, String name) {
return name.endsWith(".exe"); // 返回true表示接受該檔案
}
});
printFiles(fs2);
}
static void printFiles(File[] files) {
System.out.println("==========");
if (files != null) {
for (File f : files) {
System.out.println(f);
}
}
System.out.println("==========");
}
}
和檔案操作類似,File物件如果表示一個目錄,可以通過以下方法建立和刪除目錄:
boolean mkdir()
:建立當前File物件表示的目錄;boolean mkdirs()
:建立當前File物件表示的目錄,並在必要時將不存在的父目錄也建立出來;boolean delete()
:刪除當前File物件表示的目錄,當前目錄必須為空才能刪除成功。
Path 物件
Java標準庫還提供了一個Path
物件,它位於java.nio.file
包。Path
物件和File
物件類似,但操作更加簡單:
public class Main {
public static void main(String[] args) throws IOException {
Path p1 = Paths.get(".", "project", "study"); // 構造一個Path物件
System.out.println(p1);
Path p2 = p1.toAbsolutePath(); // 轉換為絕對路徑
System.out.println(p2);
Path p3 = p2.normalize(); // 轉換為規範路徑
System.out.println(p3);
File f = p3.toFile(); // 轉換為File物件
System.out.println(f);
for (Path p : Paths.get("..").toAbsolutePath()) { // 可以直接遍歷Path
System.out.println(" " + p);
}
}
}
./project/study
/app/./project/study
/app/project/study
/app/project/study
app
..
練習
請利用File
物件列出指定目錄下的所有子目錄和檔案,並按層次列印。
例如,輸出:
Documents/
word/
1.docx
2.docx
work/
abc.doc
ppt/
other/
import java.io.*;
import java.nio.file.*;
public class fasta {
public static void main(String[] args) throws IOException {
File pwd = new File("./src");
System.out.println(pwd);
printFiles(pwd, 1);
}
public static void printFiles(File pwd, int depth) throws IOException {
String[] fs = pwd.list();
if (fs != null) {
for (String f : fs) {
for (int i = 0; i < depth; i++) {
System.out.print(" ");
}
System.out.println(f+'/');
Path temp = Paths.get(pwd.toString(), f);
printFiles(temp.toFile(), depth + 1);
}
}
}
}
InputStream
InputStream
就是Java標準庫提供的最基本的輸入流。它位於java.io
這個包裡。java.io
包提供了所有同步IO的功能。
要特別注意的一點是,InputStream
並不是一個介面,而是一個抽象類,它是所有輸入流的超類。這個抽象類定義的一個最重要的方法就是int read()
,簽名如下:
public abstract int read() throws IOException;
這個方法會讀取輸入流的下一個位元組,並返回位元組表示的int
值(0~255)。如果已讀到末尾,返回-1
表示不能繼續讀取了。
FileInputStream
FileInputStream
是InputStream
的一個子類。顧名思義,FileInputStream
就是從檔案流中讀取資料。下面的程式碼演示瞭如何完整地讀取一個FileInputStream
的所有位元組:
public void readFile() throws IOException {
// 建立一個FileInputStream物件:
InputStream input = new FileInputStream("src/readme.txt");
for (;;) {
int n = input.read(); // 反覆呼叫read()方法,直到返回-1
if (n == -1) {
break;
}
System.out.println(n); // 列印byte的值
}
input.close(); // 關閉流
}
InputStream
和OutputStream
都是通過close()
方法來關閉流。關閉流就會釋放對應的底層資源。
我們還要注意到在讀取或寫入IO流的過程中,可能會發生錯誤,例如,檔案不存在導致無法讀取,沒有寫許可權導致寫入失敗,等等,這些底層錯誤由Java虛擬機器自動封裝成IOException
異常並丟擲。因此,所有與IO操作相關的程式碼都必須正確處理IOException
。
仔細觀察上面的程式碼,會發現一個潛在的問題:如果讀取過程中發生了IO錯誤,InputStream
就沒法正確地關閉,資源也就沒法及時釋放。
因此,我們需要用try ... finally
來保證InputStream
在無論是否發生IO錯誤的時候都能夠正確地關閉:
public void readFile() throws IOException {
InputStream input = null;
try {
input = new FileInputStream("src/readme.txt");
int n;
while ((n = input.read()) != -1) { // 利用while同時讀取並判斷
System.out.println(n);
}
} finally {
if (input != null) { input.close(); }
}
}
用try ... finally
來編寫上述程式碼會感覺比較複雜,更好的寫法是利用Java 7引入的新的try(resource)
的語法,只需要編寫try
語句,讓編譯器自動為我們關閉資源。推薦的寫法如下:
public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
int n;
while ((n = input.read()) != -1) {
System.out.println(n);
}
} // 編譯器在此自動為我們寫入finally並呼叫close()
}
實際上,編譯器並不會特別地為InputStream
加上自動關閉。編譯器只看try(resource = ...)
中的物件是否實現了java.lang.AutoCloseable
介面,如果實現了,就自動加上finally
語句並呼叫close()
方法。InputStream
和OutputStream
都實現了這個介面,因此,都可以用在try(resource)
中。
緩衝
在讀取流的時候,一次讀取一個位元組並不是最高效的方法。很多流支援一次性讀取多個位元組到緩衝區,對於檔案和網路流來說,利用緩衝區一次性讀取多個位元組效率往往要高很多。InputStream
提供了兩個過載方法來支援讀取多個位元組:
int read(byte[] b)
:讀取若干位元組並填充到byte[]
陣列,返回讀取的位元組數int read(byte[] b, int off, int len)
:指定byte[]
陣列的偏移量和最大填充數
利用上述方法一次讀取多個位元組時,需要先定義一個byte[]
陣列作為緩衝區,read()
方法會盡可能多地讀取位元組到緩衝區, 但不會超過緩衝區的大小。read()
方法的返回值不再是位元組的int
值,而是返回實際讀取了多少個位元組。如果返回-1
,表示沒有更多的資料了。
利用緩衝區一次讀取多個位元組的程式碼如下:
public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
// 定義1000個位元組大小的緩衝區:
byte[] buffer = new byte[1000];
int n;
while ((n = input.read(buffer)) != -1) { // 讀取到緩衝區
System.out.println("read " + n + " bytes.");
}
}
}
阻塞
在呼叫InputStream
的read()
方法讀取資料時,我們說read()
方法是阻塞(Blocking)的。它的意思是,對於下面的程式碼:
int n;
n = input.read(); // 必須等待read()方法返回才能執行下一行程式碼
int m = n;
執行到第二行程式碼時,必須等read()
方法返回後才能繼續。因為讀取IO流相比執行普通程式碼,速度會慢很多,因此,無法確定read()
方法呼叫到底要花費多長時間。
OutputStream
和InputStream
相反,OutputStream
是Java標準庫提供的最基本的輸出流。
和InputStream
類似,OutputStream
也是抽象類,它是所有輸出流的超類。這個抽象類定義的一個最重要的方法就是void write(int b)
,簽名如下:
public abstract void write(int b) throws IOException;
這個方法會寫入一個位元組到輸出流。要注意的是,雖然傳入的是int
引數,但只會寫入一個位元組,即只寫入int
最低8位表示位元組的部分(相當於b & 0xff
)。
Flush
和InputStream
類似,OutputStream
也提供了close()
方法關閉輸出流,以便釋放系統資源。要特別注意:OutputStream
還提供了一個flush()
方法,它的目的是將緩衝區的內容真正輸出到目的地。
為什麼要有
flush()
?因為向磁碟、網路寫入資料的時候,出於效率的考慮,作業系統並不是輸出一個位元組就立刻寫入到檔案或者傳送到網路,而是把輸出的位元組先放到記憶體的一個緩衝區裡(本質上就是一個byte[]
陣列),等到緩衝區寫滿了,再一次性寫入檔案或者網路。對於很多IO裝置來說,一次寫一個位元組和一次寫1000個位元組,花費的時間幾乎是完全一樣的,所以OutputStream
有個flush()
方法,能強制把緩衝區內容輸出。
通常情況下,我們不需要呼叫這個flush()
方法,因為緩衝區寫滿了OutputStream
會自動呼叫它,並且,在呼叫close()
方法關閉OutputStream
之前,也會自動呼叫flush()
方法。
但是,在某些情況下,我們必須手動呼叫flush()
方法。舉個例子:
小明正在開發一款線上聊天軟體,當使用者輸入一句話後,就通過OutputStream
的write()
方法寫入網路流。小明測試的時候發現,傳送方輸入後,接收方根本收不到任何資訊,怎麼肥四?
原因就在於寫入網路流是先寫入記憶體緩衝區,等緩衝區滿了才會一次性傳送到網路。如果緩衝區大小是4K,則傳送方要敲幾千個字元後,作業系統才會把緩衝區的內容傳送出去,這個時候,接收方會一次性收到大量訊息。
解決辦法就是每輸入一句話後,立刻呼叫flush()
,不管當前緩衝區是否已滿,強迫作業系統把緩衝區的內容立刻傳送出去。
實際上,InputStream
也有緩衝區。例如,從FileInputStream
讀取一個位元組時,作業系統往往會一次性讀取若干位元組到緩衝區,並維護一個指標指向未讀的緩衝區。然後,每次我們呼叫int read()
讀取下一個位元組時,可以直接返回緩衝區的下一個位元組,避免每次讀一個位元組都導致IO操作。當緩衝區全部讀完後繼續呼叫read()
,則會觸發作業系統的下一次讀取並再次填滿緩衝區。
FileOutputStream
我們以FileOutputStream
為例,演示如何將若干個位元組寫入檔案流:
public void writeFile() throws IOException {
OutputStream output = new FileOutputStream("out/readme.txt");
output.write(72); // H
output.write(101); // e
output.write(108); // l
output.write(108); // l
output.write(111); // o
output.close();
}
每次寫入一個位元組非常麻煩,更常見的方法是一次性寫入若干位元組。這時,可以用OutputStream
提供的過載方法void write(byte[])
來實現:
public void writeFile() throws IOException {
OutputStream output = new FileOutputStream("out/readme.txt");
output.write("Hello".getBytes("UTF-8")); // Hello
output.close();
}
和InputStream
一樣,上述程式碼沒有考慮到在發生異常的情況下如何正確地關閉資源。寫入過程也會經常發生IO錯誤,例如,磁碟已滿,無許可權寫入等等。我們需要用try(resource)
來保證OutputStream
在無論是否發生IO錯誤的時候都能夠正確地關閉:
public void writeFile() throws IOException {
try (OutputStream output = new FileOutputStream("out/readme.txt")) {
output.write("Hello".getBytes("UTF-8")); // Hello
} // 編譯器在此自動為我們寫入finally並呼叫close()
}
阻塞
和InputStream
一樣,OutputStream
的write()
方法也是阻塞的。
同時操作多個AutoCloseable
資源時,在try(resource) { ... }
語句中可以同時寫出多個資源,用;
隔開。例如,同時讀寫兩個檔案:
// 讀取input.txt,寫入output.txt:
try (InputStream input = new FileInputStream("input.txt");
OutputStream output = new FileOutputStream("output.txt"))
{
input.transferTo(output); // transferTo的作用是?
}
Reader
Reader
是Java的IO庫提供的另一個輸入流介面。和InputStream
的區別是,InputStream
是一個位元組流,即以byte
為單位讀取,而Reader
是一個字元流,即以char
為單位讀取:
InputStream | Reader |
---|---|
位元組流,以byte 為單位 |
字元流,以char 為單位 |
讀取位元組(-1,0~255):int read() |
讀取字元(-1,0~65535):int read() |
讀到位元組陣列:int read(byte[] b) |
讀到字元陣列:int read(char[] c) |
java.io.Reader
是所有字元輸入流的超類,它最主要的方法是:
public int read() throws IOException;
FileReader
FileReader
是Reader
的一個子類,它可以開啟檔案並獲取Reader
。下面的程式碼演示瞭如何完整地讀取一個FileReader
的所有字元:
public void readFile() throws IOException {
// 建立一個FileReader物件:
Reader reader = new FileReader("src/readme.txt"); // 字元編碼是???
for (;;) {
int n = reader.read(); // 反覆呼叫read()方法,直到返回-1
if (n == -1) {
break;
}
System.out.println((char)n); // 列印char
}
reader.close(); // 關閉流
}
如果我們讀取一個純ASCII編碼的文字檔案,上述程式碼工作是沒有問題的。但如果檔案中包含中文,就會出現亂碼,因為FileReader
預設的編碼與系統相關,例如,Windows系統的預設編碼可能是GBK
,開啟一個UTF-8
編碼的文字檔案就會出現亂碼。
要避免亂碼問題,我們需要在建立FileReader
時指定編碼:
Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8);
和InputStream
類似,Reader
也是一種資源,需要保證出錯的時候也能正確關閉,所以我們需要用try (resource)
來保證Reader
在無論有沒有IO錯誤的時候都能夠正確地關閉:
try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8) {
// TODO
}
Reader
還提供了一次性讀取若干字元並填充到char[]
陣列的方法:
public int read(char[] c) throws IOException
它返回實際讀入的字元個數,最大不超過char[]
陣列的長度。返回-1
表示流結束。
利用這個方法,我們可以先設定一個緩衝區,然後,每次儘可能地填充緩衝區:
public void readFile() throws IOException {
try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8)) {
char[] buffer = new char[1000];
int n;
while ((n = reader.read(buffer)) != -1) {
System.out.println("read " + n + " chars.");
}
}
}
小結
Reader
定義了所有字元輸入流的超類:
FileReader
實現了檔案字元流輸入,使用時需要指定編碼;CharArrayReader
和StringReader
可以在記憶體中模擬一個字元流輸入。
Reader
是基於InputStream
構造的:可以通過InputStreamReader
在指定編碼的同時將任何InputStream
轉換為Reader
。
總是使用try (resource)
保證Reader
正確關閉。
Writer
Reader
是帶編碼轉換器的InputStream
,它把byte
轉換為char
,而Writer
就是帶編碼轉換器的OutputStream
,它把char
轉換為byte
並輸出。
Writer
和OutputStream
的區別如下:
OutputStream | Writer |
---|---|
位元組流,以byte 為單位 |
字元流,以char 為單位 |
寫入位元組(0~255):void write(int b) |
寫入字元(0~65535):void write(int c) |
寫入位元組陣列:void write(byte[] b) |
寫入字元陣列:void write(char[] c) |
無對應方法 | 寫入String:void write(String s) |
Writer
是所有字元輸出流的超類,它提供的方法主要有:
- 寫入一個字元(0~65535):
void write(int c)
; - 寫入字元陣列的所有字元:
void write(char[] c)
; - 寫入String表示的所有字元:
void write(String s)
。
FileWriter
FileWriter
就是向檔案中寫入字元流的Writer
。它的使用方法和FileReader
類似:
try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
writer.write('H'); // 寫入單個字元
writer.write("Hello".toCharArray()); // 寫入char[]
writer.write("Hello"); // 寫入String
}
小結
Writer
定義了所有字元輸出流的超類:
FileWriter
實現了檔案字元流輸出;CharArrayWriter
和StringWriter
在記憶體中模擬一個字元流輸出。
使用try (resource)
保證Writer
正確關閉。
Writer
是基於OutputStream
構造的,可以通過OutputStreamWriter
將OutputStream
轉換為Writer
,轉換時需要指定編碼。
Filter 模式
又稱裝飾者模式
定義:動態給一個物件新增一些額外的職責,就象在牆上刷油漆.使用Decorator模式相比用生成子類方式達到功能的擴充顯得更為靈活。
設計初衷: 通常可以使用繼承來實現功能的擴充,如果這些需要擴充的功能的種類很繁多,那麼勢必生成很多子類,增加系統的複雜性,同時,使用繼承實現功能擴充,我們必須可預見這些擴充功能,這些功能是編譯時就確定了,是靜態的。
要點: 裝飾者與被裝飾者擁有共同的超類,繼承的目的是繼承型別,而不是行為
Java的IO標準庫提供的InputStream
根據來源可以包括:
FileInputStream
:從檔案讀取資料,是最終資料來源;ServletInputStream
:從HTTP請求讀取資料,是最終資料來源;Socket.getInputStream()
:從TCP連線讀取資料,是最終資料來源;
如果我們要給FileInputStream
新增緩衝功能,則可以從FileInputStream
派生一個類:
BufferedFileInputStream extends FileInputStream
如果要給FileInputStream
新增計算簽名的功能,類似的,也可以從FileInputStream
派生一個類:
DigestFileInputStream extends FileInputStream
如果要給FileInputStream
新增加密/解密功能,還是可以從FileInputStream
派生一個類:
CipherFileInputStream extends FileInputStream
這還只是針對FileInputStream
設計,如果針對另一種InputStream
設計,很快會出現子類爆炸的情況。
因此,直接使用繼承,為各種InputStream
附加更多的功能,根本無法控制程式碼的複雜度,很快就會失控。
為了解決這個問題,JDK首先將InputStream
分為兩大類:
一類是直接提供資料的基礎InputStream
,例如:
- FileInputStream
- ByteArrayInputStream
- ServletInputStream
- ...
一類是提供額外附加功能的InputStream
,例如:
- BufferedInputStream
- DigestInputStream
- CipherInputStream
- ...
上述這種通過一個“基礎”元件再疊加各種“附加”功能元件的模式,稱之為Filter模式(或者裝飾器模式:Decorator)。它可以讓我們通過少量的類來實現各種功能的組合:
簡單來說,裝飾模式在基類上增加的每一個功能(簡單稱做功能類)都能夠互相呼叫,每一個功能類之間都是平行層級的,與直接使用extend不同,直接繼承的類之間是樹狀結構而不是平行的。這樣就避免功能之間的巢狀。
假如,我們基於A類,又實現了三個不同的功能類(A1,A2,A3),但是此時我們需要同時用到A1和A2的功能,按照直接繼承的思路而言,就要繼承A1或者A2實現A12的一個新類。但是對裝飾模式而言,我們不需要新建一個類,直接A1(A2),相當於A1去呼叫A2,這樣就可以同時實現A1A2的功能。
例子
下面舉個例子:
假如我們要去買一個漢堡,漢堡有多種類,還可以選擇是否新增生菜、辣椒等配料。這樣給漢堡定價格,就可以使用裝飾者模式。
這裡如果我們直接使用繼承來做的話,假如有n種配料,我們就需要將n種配料之間的不同組合的類全部實現出來,直接爆炸。
如果使用裝飾者模式來做,我們只需要定義n個類就可以完成漢堡定價的功能,因為n個類之間可以相互呼叫,我們可以很方便的類的組合。
下面是程式碼:
首先是漢堡的基類,這裡定義了一個抽象類,返回了漢堡的名字和價格。
package decorator;
public abstract class Humburger {
protected String name;
public String getName(){ return name; }
public abstract double getPrice();
}
然後是漢堡的種類,這裡用的雞腿堡
package decorator;
public class ChickenBurger extends Humburger {
public ChickenBurger(){ name = "雞腿堡"; }
@Override
public double getPrice() { return 10; }
}
配料的基類,返回配料的名稱
package decorator;
public abstract class Condiment extends Humburger {
public abstract String getName();
}
生菜(裝飾的第一層)
package decorator;
public class Lettuce extends Condiment {
Humburger hburger;
public Lettuce(Humburger burger){
this.hburger = burger;
}
@Override
public String getName() {
return hburger.getName()+" 加生菜";
}
@Override
public double getPrice() {
return hburger.getPrice()+1.5;
}
}
辣椒(裝飾者的第二層)
package decorator;
public class Chilli extends Condiment {
Humburger hburger;
public Chilli(Humburger burger){
this.hburger = burger;
}
@Override
public String getName() {
return hburger.getName()+" 加辣椒";
}
@Override
public double getPrice() {
return hburger.getPrice(); //辣椒是免費的哦
}
}
測試類
package decorator;
public class Test {
public static void main(String[] args) {
// 只要一個雞肉堡
Humburger humburger = new ChickenBurger();
System.out.println(humburger.getName()+" 價錢:"+humburger.getPrice());
// 雞肉堡加生菜,呼叫雞肉堡
Lettuce lettuce = new Lettuce(humburger);
System.out.println(lettuce.getName()+" 價錢:"+lettuce.getPrice());
// 雞肉堡加辣椒,呼叫雞肉堡
Chilli chilli = new Chilli(humburger);
System.out.println(chilli.getName()+" 價錢:"+chilli.getPrice());
// 雞肉堡加生菜加辣椒,呼叫雞肉生菜堡
Chilli chilli2 = new Chilli(lettuce);
System.out.println(chilli2.getName()+" 價錢:"+chilli2.getPrice());
}
}
雞腿堡 價錢:10.0
雞腿堡 加生菜 價錢:11.5
雞腿堡 加辣椒 價錢:10.0
雞腿堡 加生菜 加辣椒 價錢:11.5