一.I/O流
I/O流表示輸入源或輸出目標。流可以表示許多不同類型的源和目標,例如磁盤文件、設備、其他程序等。
流支持許多不同類型的數據,包括字節、原始數據類型、字符和對象等。有些流只傳遞數據; 有些流則可以操縱和轉換數據。
無論各種流的內部是如何工作的,所有流都提供相同的簡單模型:流是一系列數據。程序使用輸入流從源頭獲取數據,一次一項:
程序使用輸出流將數據寫入目的地,一次一項:
在本文中,我們會看到流可以處理各種各樣的數據,無論是基本數據還是複雜對象。先來幾張IO流的全家福:
InputStream家族:
OutputStream家族:
Reader家族:
Writer家族:
點擊此處可查看完整大圖。
1.字節流
一個字節(byte)代表8個二進制位(bit)。字節流,顧名思義,它所傳遞和操作的最小單位是字節。所有的字節流類都是抽象類InputStream和OutputStream的子類。
I/O體系中有許多字節流類。爲了演示字節流如何工作,我們選擇了文件I/O字節流——FileInputStream和FileOutputStream。其他字節流類的使用方式都大致相同,不同之處主要在於它們的構造方式。
下面的程序使用字節流將src.txt中的文本複製到dest.txt中,每次一個字節:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class CopyBytes {
public static void main(String[] args) throws IOException {
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream("src.txt");
out = new FileOutputStream("dest.txt");
int c;
while ((c = in.read()) != -1) {
out.write(c);
}
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}
}
在while循環中,程序每次從輸入流in中讀取一個字節,然後再將這個字節寫入輸出流out中,直到輸入流中的字節全部被讀取完。
在不再需要流時關閉流是非常重要的。上面的程序中使用了finally塊來保證無論是否發生錯誤都要關閉流。
雖然上面的程序看起來很正常,但是實際上我們應該避免使用這種低級的,或者說底層的I/O。由於src.txt文件中存儲的是字符數據,因此我們應該使用字符流,我們馬上會在下一節中見到它。字節流應該僅用於最原始的I/O。那麼爲什麼要談論字節流呢?因爲所有其他流類型都是基於字節流構建的。
2.字符流
所有的字符流類都是Reader和Writer的子類。爲了演示字符流如何工作,和上一節一樣,這裏我們選擇文件I/O字符流——FileReader和FileWriter。
下面的程序使用字符流將src.txt中的文本複製到dest.txt中,每次一個char(注意,這裏不是每次一個字符,因爲有些字符無法使用一個char類型來表示,具體可以參考我之前的文章《Java基礎教程(5)--變量》中關於char類型的介紹):
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class CopyCharacters {
public static void main(String[] args) throws IOException {
FileReader inputStream = null;
FileWriter outputStream = null;
try {
inputStream = new FileReader("src.txt");
outputStream = new FileWriter("dest.txt");
int c;
while ((c = inputStream.read()) != -1) {
outputStream.write(c);
}
} finally {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
}
}
}
字符流通常是字節流的“包裝器”。字符流使用字節流來執行物理I/O,而字符流則負責處理字符和字節之間的轉換。FileReader內部使用FileInputStream來讀取數據,而FileWriter則使用FileOutputStream來寫入數據。
FileReader和FileWriter是專門用於文件讀寫的字符流。如果需要從其他的源讀取字符,或者需要向其他目標寫入字符,可以使用InputStreamReader和OutputStreamWriter來定製自己的流。這兩個類只是簡單的從輸入源讀取字符和向輸出目標寫入字符,我們可以使用它們自定義輸入源和輸出目標。
FileReader和FileWriter所處理的最小單位是char類型。實際上,還可以每次處理一行字符。行是指一串字符與末尾的行終止符。現在我們修改上面的程序,來讓我們的程序每次處理一行字符。這裏會使用到兩個新的類——BufferedReader和PrintWriter,我們會在下一節更深入地討論這些類:
import java.io.FileReader;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.IOException;
public class CopyLines {
public static void main(String[] args) throws IOException {
BufferedReader inputStream = null;
PrintWriter outputStream = null;
try {
inputStream = new BufferedReader(new FileReader("src.txt"));
outputStream = new PrintWriter(new FileWriter("dest.txt"));
String l;
while ((l = inputStream.readLine()) != null) {
outputStream.println(l);
}
} finally {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
}
}
}
3.緩衝流
到目前爲止,我們上面的例子使用的都是無緩衝的I/O。這意味着每次讀取或寫入請求都由底層操作系統直接處理。這樣會使得程序的效率低很多,因爲每個請求都會觸發磁盤訪問、網絡請求或其他代價相對較高的操作。
爲了減少這種開銷,Java平臺實現了使用緩衝的I/O流。緩衝輸入流從稱爲緩衝區的存儲區讀取數據,僅當緩衝區爲空時纔會重新獲取數據。類似地,緩衝輸出流將數據寫入緩衝區,並且僅在緩衝區已滿時纔將數據寫入輸出目標。
下面的兩個例子將無緩衝的流轉換爲緩衝流:
inputStream = new BufferedReader(new FileReader("src.txt"));
outputStream = new BufferedWriter(new FileWriter("dest.txt"));
有四個緩衝流類用於包裝無緩衝流:BufferedInputStream與BufferedOutputStream創建緩衝字節流,而 BufferedReader與BufferedWriter創建緩衝字符流。
有時候我們需要再緩衝區未填充滿的時候就將它寫出緩衝區,此時就需要刷新緩衝區。一些緩衝輸出流支持自動刷新,這個行爲由可選的構造方法參數指定。在啓用自動刷新時,某些關鍵事件會導致刷新緩衝區。例如,PrintWriter會在每次調用println和format的時候刷新緩衝區。如果要手動刷新緩衝區,可以調用該輸出流的flush方法。
4.格式化
通過使用具有格式化功能的流可以將數據轉換爲格式標準、易於閱讀的形式。提供格式化功能的流是是字符流類PrintWriter和字節流類PrintStream。
注意,唯一需要使用PrintStream的地方是System.out和System.err(具體內容見下一小節)。當你需要格式化輸出流,應該使用PrintWriter而不是PrintStream。
正如其他輸出流一樣,PrintStream和PrintWriter爲簡單的字節或字符輸出提供了一組write方法。除此之外,它們還提供了兩種格式化方法來對輸出內容進行格式化:
- print和println——以標準方式格式化單個值。
- format——基於格式化字符串來對任意數量的值進行格式化。
print和println方法
print和println用於打印單個變量的值,如果是對象則會打印對該對象調用toString方法後返回的字符串,它們唯一的區別是println會在每次調用後換行,而print則不會。下面是一個使用print和println的例子:
public class Root {
public static void main(String[] args) {
int i = 2;
double r = Math.sqrt(i);
System.out.print("The square root of ");
System.out.print(i);
System.out.print(" is ");
System.out.print(r);
System.out.println(".");
i = 5;
r = Math.sqrt(i);
System.out.println("The square root of " + i + " is " + r + ".");
}
}
該程序將會輸出:
The square root of 2 is 1.4142135623730951.
The square root of 5 is 2.23606797749979.
format方法
format方法使用指定的格式字符串對多個參數進行格式化。格式字符串是指嵌入格式說明符的字符串。格式字符串支持非常多的格式,本文中只會介紹一些基礎知識。有關完整說明請參考官方提供的格式字符串語法。
下面的例子調用了一次format方法,但同時格式化了兩個值:
public class Root2 {
public static void main(String[] args) {
int i = 2;
double r = Math.sqrt(i);
System.out.format("The square root of %d is %f.%n", i, r);
}
}
下面是輸出:
The square root of 2 is 1.414214.
上面的例子中,%d、%f和%n是三個格式說明符。所有的格式說明符都以一個%開頭,並以一個或兩個字符結束。這裏使用的三個格式說明符是:
- %d——將整數值轉換爲十進制數。
- %f——將浮點數值轉換爲十進制數。
- %n——表示基於當前平臺的行結束符。
還有很多格式說明符,由於篇幅所限,再加上本文的重點是I/O流,因此這裏就不再列舉其他格式說明符和其他格式化的細節,感興趣的讀者可以自行查閱官方文檔。
5.標準流
標準流是許多操作系統的特性。默認情況下,它們從鍵盤讀取輸入並將輸出寫入顯示器。它們還支持文件I/O以及程序之間的I/O,但該功能由命令行解釋器控制,而不是程序。
Java平臺支持三種標準流:標準輸入,通過System.in訪問;標準輸出,通過System.out訪問;標準錯誤,通過System.err訪問。這些流是自動定義的並且不需要打開。標準輸出和標準錯誤均用於輸出。
你可能覺得標準流是字符流,但由於歷史原因,它們實際上是字節流。System.out和System.err都是PrintStream類型。雖然從技術上講它們是字節流,但是PrintStream內部利用字符流對象來模擬字符流的許多功能。
相比之下,System.in是一個沒有字符流功能的字節流。如果要使用標準輸入作爲字符流,可以使用InputStreamReader或Scanner對它進行包裝。
6.數據流
文本格式是易於人類閱讀的,因此使用起來很方便。但是它並不像以二進制格式傳遞數據那樣高效。下面我們將會學習如何用二進制數據來完成輸入和輸出。
DataOutput接口定義了一組用於以二進制格式寫各種類型的值的方法。例如,writeInt總是將一個整數寫出爲4字節的二進制整數,writeDouble總是將一個Double值寫出爲8字節的二進制浮點數。這樣產生的結果並非人類可閱讀的,但是對於給定類型的每個值,所需的空間都是相同的,而且將其讀回也比解析文本要更快。
同理,爲了讀取二進制數據,可以使用在DataInput接口中定義的一組方法。DataInputStream類實現了DataInput接口。爲了讀入二進制數據,可以將DataInputStream與某個字節流相結合,例如:
DataInputStream in = new DataInputStream(new FileInputStream("a.dat"));
類似的,如果要寫出二進制數據,可以使用實現了DataOutput接口的DataOutputStream類:
DataOutputStream out = new DataOutputStream(new FileOutputStream("a.dat"));
7.對象流
就像數據流支持基本數據類型的I/O,對象流支持對象的I/O。Java支持一種稱爲對象序列化的機制,它可以將對象寫出到對象輸出流中,也可以從對象輸入流中將對象讀入。這樣我們就可以將對象通過文件、網絡等環境來傳遞並隨時將其恢復。所有需要在對象輸出流中存儲或從對象輸入流中恢復的類都必須實現Serializable接口,這只是一個標記接口,它沒有定義任何方法。
Java中提供的對象輸入流和輸出流分別是ObjectInputStream和ObjectOutputStream。通過ObjectOutputStream的writeObject方法可以將一個對象寫入到輸出流中,通過ObjectInputStream的readObject方法則可以從輸出流中讀取一個對象。
對於一些所有域都是基本數據類型的對象來說,對其進行序列化是很簡單的。但是對於某些域是引用類型的對象來說,在對這些對象進行序列化時,還要對其引用的對象也進行序列化,並且這些引用的對象內部可能還含有對其他對象的引用。在這種情況下,writeObject方法將會遍歷該對象內部所有的引用,並將這些對象寫入流。
下圖演示了這種情況。調用writeObject方法將a對象寫入流,但該對象包含了對對象b和c的引用,而b包含對d和e的引用。在將a寫入輸出流時,會同時寫入其他四個對象。當讀回a時,也會讀回其他四個對象,並保留原有的引用關係。
您可能想知道如果將一個對象向同一個流中寫入兩次會發生什麼。當它們被讀回時,還會引用同一個對象嗎?答案是肯定的。無論寫入多少次,流只會包含一個對象的副本。因此,如果明確地將對象寫入流兩次,那麼實際上只寫入了兩次引用。例如,下面的代碼將對象ob寫入兩次:
Object ob = new Object();
out.writeObject(ob);
out.writeObject(ob);
每次調用readObject方法都會讀回一個ob對象:
Object ob1 = in.readObject();
Object ob2 = in.readObject();
System.out.println(ob1 == ob2);
上面的程序會輸出true,因爲ob1和ob2引用了同一個對象。但是,如果將一個對象寫入兩個不同的流,那麼讀回的將是兩個不同的對象。
這裏還要介紹一個關鍵字transient。若類的某個域被transient修飾,則在將該類的實例序列化時將不會對該字段進行序列化。當從輸入流中讀入對象時,該域將會使用默認值填充。
二.文件操作
上文中提到的文件輸入和輸出流只能用於獲取和寫入文件內容,不能對這個文件進行移動、複製、刪除等操作,且寫入文件時只能覆蓋或在文件尾部追加,而不能修改文件內容。這一節將會討論有關文件的操作以及如何對文件內容進行隨機讀寫。
1.File類
File類是java.io包中代表與平臺無關的文件和目錄。注意,File對象不僅可以表示文件,還可以用來表示目錄。如果希望在程序中操作文件和目錄,都可以通過File類來完成。File能對文件或目錄進行新建、刪除、重命名等操作,但它不能訪問文件內容本身。
下面是File類的四個構造方法:
方法 | 描述 |
---|---|
File(String pathname) | 根據指定的路徑名稱創建一個File實例 |
File(File parent, String child) | 根據父路徑和子路徑名稱創建一個File實例 |
File(String parent, String child) | 根據父路徑名稱和子路徑名稱創建一個File實例 |
File(URI uri) | 根據指定的URI創建一個File實例 |
創建File實例之後,並不是真的創建了一個文件或目錄,而是對指定路徑的一個抽象,通過File對象可以判斷當前路徑對應的文件或目錄是否存在,或者對其進行復制、刪除等操作。下面是一些常用的操作:
- boolean createNewFile()
根據當前File對象對應的路徑創建文件,若文件不存在且創建成功則返回true,若文件不存在則返回false。 - boolean delete()
刪除當前文件或目錄,刪除成功返回true,若目錄非空或因其他原因刪除失敗則返回false。 - boolean exists()
判斷當前文件或目錄是否存在。 - public boolean mkdir()
根據當前路徑創建目錄。若父目錄不存在或當前目錄創建失敗則返回false。 - public boolean mkdirs()
根據當前路徑創建目錄。若父目錄不存在則創建所有不存在的父目錄。
File類中還提供了許多其他操作文件和查看文件屬性的方法,這裏不再一一列舉,必要時可查看官方API文檔。
2.RandomAccessFile
RandomAccessFile是一個功能豐富的文件內容訪問類,它提供了衆多的方法來訪問文件內容,它既可以讀取文件內容,也可以向文件中寫入數據。與I/O流不同的是,它支持“隨機訪問”的方式,即程序可以直接跳轉到文件的任意位置來讀寫數據。
RandomAccessFile對象包含了一個指針,用以表示當前讀寫處的位置。當創建一個RandomAccessFile對象時,該指針位於文件頭(也就是0處),當讀寫了n個字節後,指針會向後移動n個字節。此外,還可以手動移動該指針,既可以向前,也可以向後。
RandomAccessFile類中定義了以下兩個方法來操作指針:
- long getFilePointer()
返回指針當前所在位置。 - void seek(long pos)
移動指針至指定位置。
RandomAccessFile類有兩個構造器,一個使用File對象來指定文件,另一個使用String參數來指定文件路徑。此外,這兩個構造器還需要制定另外一個mode參數,該參數用於指定對指定文件的訪問模式,該參數有以下4個值:
- “r”:以只讀方式打開文件。
- “rw”:以可讀可寫方式打開文件。
- “rwd”:以可讀可寫方式打開文件。相對於“rw”模式,還要求對文件內容的每個更新都寫入到底層存儲設備。
- “rws”:以可讀可寫方式打開文件。相對於“rw”模式,還要求對文件的元數據和文件內容的每個更新都寫入到底層存儲設備。
這裏解釋一下元數據的概念。元數據,即metadata,可以理解爲關於數據的數據。我們將數據存儲在文件中,可以將文件看作數據,那麼關於文件本身的數據就可以稱爲元數據,例如修改時間、擁有者、訪問權限等。
當使用“rw”模式讀寫文件時,只有在關閉文件的時候纔會將修改同步到存儲設備上;使用“rwd”模式時,每次對於文件內容的修改都會同步到存儲設備上,但只有在關閉文件時纔會同步文件的元數據;使用“rws”模式時,每次對文件的元數據和文件內容的修改都會同步到存儲設備上。
RandomAccessFile類提供了一系列readXX和writeXX方法來支持各種類型的數據的讀取和寫入,這裏不再一一列出。需要注意的是,每次操作完文件後要記得調用close方法來關閉文件。
下面是對RandomAccessFile類的讀取、寫入、移動指針等操作的一個演示程序。這個程序將指定文件中的大寫字母轉換爲小寫:
import java.io.IOException;
import java.io.RandomAccessFile;
public class RandomAccessFileDemo {
public static void main(String[] args) {
try {
RandomAccessFile raf = new RandomAccessFile("a.txt", "rw");
for (int i = 0, b; (b = raf.read()) != -1; i++) {
if (b >= 65 && b < 90) {
raf.seek(i);
raf.write(b + 32);
}
}
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}