文章目錄
- IO 流
- 什麼是 IO
- 位元組流
- 位元組流概念
- 位元組流讀寫檔案
- 檔案的拷貝
- 位元組流的緩沖區
- 裝飾設計模式
- 位元組緩沖流
- 字元流
- 字元流定義及基本用法
- 字元流操作檔案
- 轉換流
- File 類
- File 類的常用方法
- 周遊目錄下的檔案
- 删除檔案及目錄
- 字元編碼
- 常用字元集
- 微信公衆号
IO 流
什麼是 IO
大多數應用程式都需要實作與裝置之間的資料傳輸,例如鍵盤可以輸入資料,顯示器可以顯示程式的運作結果等。在 Java 中,将這種通過不同輸入輸出裝置(鍵盤,記憶體,顯示器,網絡等)之間的資料傳輸抽象表述為“流”,程式允許通過流的方式與輸入輸出裝置進行資料傳輸。Java 中的“流”都位于 java.io 包中,稱為 IO(輸入輸出)流。
IO 流有很多種,按照操作資料的不同,可以分為位元組流和字元流,按照資料傳輸方向的不同又可分為輸入流和輸出流,程式從輸入流中讀取資料,向輸出流中寫入資料。在 IO 包中,位元組流的輸入輸出流分别用 java.io.InputStream 和 java.io.OutputStream 表示,字元流的輸入輸出流分别用 java.io.Reader 和 java.io.Writer 表示,具體分類如圖所示。
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-6cXJDz5g-1642058941599)(http://47.107.171.232/easily-j/images/20190111/9766628d-b101-45b0-b592-9af06cde7531.png)]
位元組流
位元組流概念
在計算機中,無論是文本、圖檔、音頻還是視訊,所有的檔案都是以二進制(位元組)形式存在,IO 流中針對位元組的輸入輸出提供了一系列的流,統稱為位元組流。位元組流是程式中最常用的流,根據資料的傳輸方向可将其分為位元組輸入流和位元組輸出流。在 JDK 中,提供了兩個抽象類 InputStream 和 OutputStream,它們是位元組流的頂級父類,所有的位元組輸入流都繼承自 InputStream,所有的位元組輸出流都繼承自 OutputStream。為了友善了解,可以把 InputStream 和 OutputStream 比作兩根“水管”,如圖所示。
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-eZ4OSMv2-1642058941601)(http://47.107.171.232/easily-j/images/20190111/bcfbd193-8f57-444c-a6f4-d8e0d7ac28ea.png)]
圖中,InputStream 被看成一個輸入管道,OutputStream 被看成一個輸出管道,資料通過 InputStream 從源裝置輸入到程式,通過 OutputStream 從程式輸出到目标裝置,進而實作資料的傳輸。由此可見,IO 流中的輸入輸出都是相對于程式而言的。
在 JDK 中,InputStream 和 OutputStream 提供了一系列與讀寫資料相關的方法,接下來先來了解一下 InputStream 的常用方法,如表所示。
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-FwInIid8-1642058941603)(http://47.107.171.232/easily-j/images/20190111/961a30d6-deb2-4a38-bea3-2c417c2b222f.png)]
表中列舉了 InputStream 的四個常用方法。前三個 read()方法都是用來讀資料的,其中,第一個 read()方法是從輸入流中逐個讀入位元組,而第二個和第三個 read()方法則将若幹位元組以位元組數組的形式一次性讀入,進而提高讀資料的效率。在進行 IO 流操作時,目前 IO 流會占用一定的記憶體,由于系統資源寶貴,是以,在 IO 操作結束後,應該調用 close()方法關閉流,進而釋放目前 IO 流所占的系統資源。
與 InputStream 對應的是 OutputStream。OutputStream 是用于寫資料的,是以 OutputStream 提供了一些與寫資料有關的方法,如表所示。
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-Qyl5faff-1642058941604)(http://47.107.171.232/easily-j/images/20190111/d2835b5e-42af-4e74-8869-ae9775214172.png)]
表中,列舉了 OutputStream 類的五個常用方法。前三個是重載的 write()方法,都是用于向輸出流寫入位元組,其中,第一個方法逐個寫入位元組,後兩個方法是将若幹個位元組以位元組數組的形式一次性寫入,進而提高寫資料的效率。flush()方法用來将目前輸出流緩沖區(通常是位元組數組)中的資料強制寫入目标裝置,此過程稱為重新整理。close()方法是用來關閉流并釋放與目前 IO 流相關的系統資源。
InputStream 和 OutputStream 這兩個類雖然提供了一系列和讀寫資料有關的方法,但是這兩個類是抽象類,不能被執行個體化,是以,針對不同的功能,InputStream 和 OutputStream 提供了不同的子類,這些子類形成了一個體系結構,如圖所示。
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-mnDjt0W2-1642058941606)(http://47.107.171.232/easily-j/images/20190111/dd8d76df-53c9-480b-a7f2-b75ee2db7c7c.png)]
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-UJtxL5o4-1642058941607)(http://47.107.171.232/easily-j/images/20190111/baea8226-fb97-432b-8b3f-d970844abd75.png)]
從圖中可以看出,InputStream 和 OutputStream 的子類有很多是大緻對應的,比如 ByteArrayInputStream 和 ByteArrayOutputStream,FileInputStream 和 FileOutputStream 等。圖中所列出的 IO 流都是程式中很常見的,接下來将逐漸為大家講解這些流的具體用法。
位元組流讀寫檔案
由于計算機中的資料基本都儲存在硬碟的檔案中,是以操作檔案中的資料是一種很常見的操作。在操作檔案時,最常見的就是從檔案中讀取資料并将資料寫入檔案,即檔案的讀寫。針對檔案的讀寫,JDK 專門提供了兩個類,分别是 FileInputStream 和 FileOutputStream。
FileInputStream 是 InputStream 的子類,它是操作檔案的位元組輸入流,專門用于讀取檔案中的資料。由于從檔案讀取資料是重複的操作,是以需要通過循環語句來實作資料的持續讀取。接下來通過一個案例來實作位元組流對檔案資料的讀取,首先在 D 盤目錄下建立一個文本檔案 IO.txt,在檔案中輸入内容“小海綿”,具體代碼如例所示。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class IOTest {
public static void main(String[] args) {
try {
// 建立一個檔案位元組輸入流
FileInputStream in = new FileInputStream("D:/IO.txt");
int b = 0; // 定義一個int 類型的變量b,記住每次讀取的一個位元組
while (true) {
b = in.read(); // 變量b 記住讀取的一個位元組
if (b == -1) { // 如果讀取的位元組為-1,跳出while 循環
break;
}
System.out.println(b); // 否則将b 寫出
}
in.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
運作結果:
208
161
186
163
195
224
例中,建立的位元組流 FileInputStream 通過 read()方法将目前目錄檔案“D://IO.txt”中的資料讀取并列印。通常情況下讀取檔案應該輸出字元,之是以輸出數字是因為硬碟上的檔案是以位元組的形式存在的,在“IO.txt”檔案中,字元’小’,‘海’,'綿’各占 2 個位元組,是以,最終結果顯示的就是檔案中的六個位元組所對應的十進制數。
與 FileInputStream 對應的是 FileOutputStream。FileOutputStream 是 OutputStream 的子類,它是操作檔案的位元組輸出流,專門用于把資料寫入檔案。接下來通過一個案例來示範如何将資料寫入檔案,如例所示。
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class IOTest {
public static void main(String[] args) {
try {
// 建立一個檔案位元組輸出流
FileOutputStream out = new FileOutputStream("D:/example.txt");
String str = "小海綿";
byte[] b = str.getBytes();
for (int i = 0; i < b.length; i++) {
out.write(b[i]);
}
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
程式運作後,會在 D 盤目錄下生成一個新的文本檔案 example.txt,打開此檔案,檔案内容為小海綿。
通過運作結果可以看出,通過 FileOutputStream 寫資料時,自動建立了檔案 example.txt,并将資料寫入檔案。需要注意的是,如果是通過 FileOutputStream 向一個已經存在的檔案中寫入資料,那麼該檔案中的資料首先會被清空,再寫入新的資料。若希望在已存在的檔案内容之後追加新内容,則可使用 FileOutputStream 的構造函數 FileOutputStream(StringfileName,booleanappend)來建立檔案輸出流對象,并把 append 參數的值設定為 true。接下來通過一個案例來示範如何将資料追加到檔案末尾,如例所示。
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class IOTest {
public static void main(String[] args) {
try {
OutputStream out = new FileOutputStream("D:/example.txt", true);
String str = "炒雞帥";
byte[] b = str.getBytes();
for (int i = 0; i < b.length; i++) {
out.write(b[i]);
}
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
檢視 D://example.txt 檔案,檔案中的内容為 小海綿炒雞帥。
檔案的拷貝
在應用程式中,IO 流通常都是成對出現的,即輸入流和輸出流一起使用。例如檔案的拷貝就需要通過輸入流來讀取檔案中的資料,通過輸出流将資料寫入檔案。接下來通過一個案例來示範如何進行檔案内容的拷貝,首先在 D 盤建立檔案夾 one,和 two,然後在 one 檔案夾中存放一個“example.txt”檔案,拷貝檔案的代碼如例所示。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class IOTest {
public static void main(String[] args) {
try {
// 建立一個位元組輸入流,用于讀取目前目錄下source 檔案夾中的mp3 檔案
InputStream in = new FileInputStream("D:/one/example.txt");
// 建立一個檔案位元組輸出流,用于将讀取的資料寫入target 目錄下的檔案中
OutputStream out = new FileOutputStream("D:/two/example.txt");
int len; // 定義一個int 類型的變量len,記住每次讀取的一個位元組
long begintime = System.currentTimeMillis(); // 擷取拷貝檔案前的系統時間
while ((len = in.read()) != -1) { // 讀取一個位元組并判斷是否讀到檔案末尾
out.write(len); // 将讀到的位元組寫入檔案
}
long endtime = System.currentTimeMillis(); // 擷取檔案拷貝結束時的系統時間
System.out.println("拷貝檔案所消耗的時間是: " + (endtime - begintime) + "毫秒");
in.close();
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
運作結果:
拷貝檔案所消耗的時間是: 2毫秒
在拷貝過程中,通過 while 循環将位元組逐個進行拷貝。每循環一次,就通過 FileInputStream 的 read()方法讀取一個位元組,并通過 FileOutputStream 的 write()方法将該位元組寫入指定檔案,循環往複,直到 len 的值為-1,表示讀取到了檔案的末尾,結束循環,完成檔案的拷貝。程式運作結束後,會在指令行視窗列印拷貝檔案所消耗的時間。
位元組流的緩沖區
雖然上一個案例實作了檔案的拷貝,但是一個位元組一個位元組的讀寫,需要頻繁的操作檔案,效率非常低,這就好比從北京運送烤鴨到上海,如果有一萬隻烤鴨,每次運送一隻,就必須運輸一萬次,這樣的效率顯然非常低。為了減少運輸次數,可以先把一批烤鴨裝在車廂中,這樣就可以成批的運送烤鴨,這時的車廂就相當于一個臨時緩沖區。當通過流的方式拷貝檔案時,為了提高效率也可以定義一個位元組數組作為緩沖區。在拷貝檔案時,可以一次性讀取多個位元組的資料,并儲存在位元組數組中,然後将位元組數組中的資料一次性寫入檔案。接下來通過一個案例來學習如何使用緩沖區拷貝檔案,如例所示。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class IOTest {
public static void main(String[] args) {
try {
// 建立一個位元組輸入流,用于讀取目前目錄下source 檔案夾中的mp3 檔案
InputStream in = new FileInputStream("D:/one/example.txt");
// 建立一個檔案位元組輸出流,用于将讀取的資料寫入目前目錄的target 檔案中
OutputStream out = new FileOutputStream("D:/one/example.txt");
// 以下是用緩沖區讀寫檔案
byte[] buff = new byte[1024]; // 定義一個位元組數組,作為緩沖區
// 定義一個int 類型的變量len 記住讀取讀入緩沖區的位元組數
int len;
long begintime = System.currentTimeMillis();
while ((len = in.read(buff)) != -1) { // 判斷是否讀到檔案末尾
out.write(buff, 0, len); // 從第一個位元組開始,向檔案寫入len 個位元組
}
long endtime = System.currentTimeMillis();
System.out.println("拷貝檔案所消耗的時間是: " + (endtime - begintime) + "毫秒");
in.close();
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
運作結果:
拷貝檔案所消耗的時間是: 0毫秒
在拷貝過程中,使用 while 循環語句逐漸實作位元組檔案的拷貝,每循環一次,就從檔案讀取若幹位元組填充位元組數組,并通過變量 len 記住讀入數組的位元組數,然後從數組的第一個位元組開始,将 len 個位元組依次寫入檔案。循環往複,當 len 值為-1 時,說明已經讀到了檔案的末尾,循環會結束,整個拷貝過程也就結束了,最終程式将整個拷貝過程所消耗的時間列印了出來。
通過兩種拷貝方式的對比,可以看出拷貝檔案所消耗的時間明顯減少了,進而說明緩沖區讀寫檔案可以有效的提高程式的效率。這是因為程式中的緩沖區就是一塊記憶體,用于存放暫時輸入輸出的資料,使用緩沖區減少了對檔案的操作次數,是以可以提高讀寫資料的效率。
裝飾設計模式
俗話說“人靠衣裝馬靠鞍”,漂亮得體的裝扮不僅能提升形象,還能提高競争力。在程式設計中,同樣可以通過“裝飾”一個類,增強它的功能。裝飾設計模式就是通過包裝一個類,動态地為它增加功能的一種設計模式。
裝飾設計模式在現實生活中随處可見,比如買了一輛車,想為新車裝一個倒車雷達,這就相當于為這輛汽車增加新的功能。接下來通過一個案例來實作上述過程,如例所示。
class Car {
private String carName; // 定義一個屬性,代表車名
public Car(String carName) {
this.carName = carName;
}
public void show() { // 實作Car 的show()方法
System.out.println("我是" + carName + ",具有基本功能");
}
}
// 定義一個類RadarCar
class RadarCar {
public Car myCar;
public RadarCar(Car myCar) { // 通過構造方法接收被包裝的對象
this.myCar = myCar;
}
public void show() {
myCar.show();
System.out.println("具有倒車雷達功能"); // 實作功能的增強
}
}
public class IOTest {
public static void main(String[] args) {
Car benz = new Car("Benz"); // 建立一個NewCar 對象
System.out.println("--------------包裝前--------------");
benz.show();
RadarCar decoratedCar_benz = new RadarCar(benz); // 建立一個RadarCar 對象
System.out.println("--------------包裝後--------------");
decoratedCar_benz.show();
}
}
運作結果:
--------------包裝前--------------
我是Benz,具有基本功能
--------------包裝後--------------
我是Benz,具有基本功能
具有倒車雷達功能
例實作了 RadarCar 類對 Car 類的包裝。包裝類 RadarCar 的構造方法中接收一個 Car 類型的執行個體對象。通過運作結果可以看出,當 RadarCar 對象調用 show()方法時,被 RadarCar 包裝後的對象 benz 不僅具有車的基本功能,而且具有了倒車雷達的功能。
位元組緩沖流
在 IO 包中提供兩個帶緩沖的位元組流,分别是 BufferedInputStream 和 BufferdOutputStream,這兩個流都使用了裝飾設計模式。它們的構造方法中分别接收 InputStream 和 OutputStream 類型的參數作為被包裝對象,在讀寫資料時提供緩沖功能。應用程式、緩沖流和底層位元組流之間的關系如圖所示。
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-buSIMf4w-1642058941609)(http://47.107.171.232/easily-j/images/20190111/dfccccb9-741b-419b-8e47-5ca52e4540f6.png)]
從圖中可以看出應用程式是通過緩沖流來完成資料讀寫的,而緩沖流又是通過底層被包裝的位元組流與裝置進行關聯的。接下來通過一個案例來學習 BufferedInputStream 和 BufferedOutputStream 這兩個流的用法,如例所示。
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class IOTest {
public static void main(String[] args) {
try {
// 建立一個帶緩沖區的輸入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:/example.txt"));
// 建立一個帶緩沖區的輸出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:/example.txt"));
int len;
while ((len = bis.read()) != -1) {
bos.write(len);
}
bis.close();
bos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
例中,建立了 BufferedInputStream 和 BufferedOutputStream 兩個緩沖流對象,這兩個流内部都定義了一個大小為 8192 的位元組數組,當調用 read()或者 write()方法讀寫資料時,首先将讀寫的資料存入定義好的位元組數組,然後将位元組數組的資料一次性讀寫到檔案中,這種方式與位元組流的緩沖區類似,都對資料進行了緩沖,進而有效的提高了資料的讀寫效率。
字元流
字元流定義及基本用法
前面我們講過 InputStream 類和 OutputStream 類在讀寫檔案時操作的都是位元組,如果希望在程式中操作字元,使用這兩個類就不太友善,為此 JDK 提供了字元流。同位元組流一樣,字元流也有兩個抽象的頂級父類,分别是 Reader 和 Writer。其中 Reader 是字元輸入流,用于從某個源裝置讀取字元,Writer 是字元輸出流,用于向某個目标裝置寫入字元。Reader 和 Writer 作為字元流的頂級父類,也有許多子類,接下來通過繼承關系圖來列出 Reader 和 Writer 的一些常用子類,如圖所示。
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-rSGU0iRn-1642058941610)(http://47.107.171.232/easily-j/images/20190111/c2d5f0fd-0f90-4bb8-922a-247e2f96539e.png)]
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-EUDqEAoO-1642058941611)(http://47.107.171.232/easily-j/images/20190111/eb650e26-dc33-4eb3-8ad9-6860954b7e1e.png)]
從圖可以看到,字元流的繼承關系與位元組流的繼承關系有些類似,很多子類都是成對(輸入流和輸出流)出現,其中 FileReader 和 FileWriter 用于讀寫檔案,BufferedReader 和 BufferedWriter 是具有緩沖功能的流,它們可以提高讀寫效率。
字元流操作檔案
在程式開發中,經常需要對文本檔案的内容進行讀取,如果想從檔案中直接讀取字元便可以使用字元輸入流 FileReader,通過此流可以從關聯的檔案中讀取一個或一組字元。接下來首先在 D 盤目錄下建立檔案“example.txt”并在其中輸入字元“小海綿”,然後通過一個案例來學習如何使用 FileReader 讀取檔案中的字元,如例所示。
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class IOTest {
public static void main(String[] args) {
try {
// 建立一個FileReader 對象用來讀取檔案中的字元
FileReader reader = new FileReader("D:/example.txt");
int ch; // 定義一個變量用于記錄讀取的字元
while ((ch = reader.read()) != -1) { // 循環判斷是否讀取到檔案的末尾
System.out.println((char) ch); // 不是字元流末尾就轉為字元列印
}
reader.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
運作結果:
小
海
綿
例實作了讀取檔案字元的功能。首先建立一個 FileReader 對象與檔案關聯,然後通過 while 循環每次從檔案中讀取一個字元并列印,這樣便實作了 FileReader 讀檔案字元的操作。需要注意的是,字元輸入流的 read()方法傳回的是 int 類型的值,如果想獲得字元就需要進行強制類型轉換。
例講解了如何使用 FileReader 讀取檔案中的字元,如果要向檔案中寫入字元就需要使用 FileWriter 類。FileWriter 是 Writer 的一個子類,接下來通過一個案例來學習如何使用 FileWriter 将字元寫入檔案,如例所示。
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
public class IOTest {
public static void main(String[] args) {
try {
// 建立一個FileWriter 對象用于向檔案中寫入資料
FileWriter writer = new FileWriter("D:/example.txt", true);
String str = "小哥哥";
writer.write(str); // 将字元資料寫入到文本檔案中
writer.write("\r\n"); // 将輸出語句換行
writer.close(); // 關閉寫入流,釋放資源
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
再次運作程式就可以實作在檔案中追加内容的效果。
接下來通過一個案例來學習如何使用 BufferedReader 和 BufferedWriter 實作檔案的拷貝,如例所示。
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class IOTest {
public static void main(String[] args) {
try {
FileReader reader = new FileReader("D:/one/example.txt");
// 建立一個BufferedReader 緩沖對象
BufferedReader br = new BufferedReader(reader);
FileWriter writer = new FileWriter("D:/one/example.txt");
// 建立一個BufferdWriter 緩沖對象
BufferedWriter bw = new BufferedWriter(writer);
String str;
while ((str = br.readLine()) != null) { // 每次讀取一行文本,判斷是否到檔案末尾
bw.write(str);
bw.newLine(); // 寫入一個換行符,該方法會根據不同的作業系統生成相應的換行符
}
br.close();
bw.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在例中,首先對輸入輸出流進行了包裝,并通過一個 while 循環實作了文本檔案的拷貝。在拷貝過程中,每次循環都使用 readLine()方法讀取檔案的一行,然後通過 write()方法寫入目标檔案。其中 readLine()方法會逐個讀取字元,當讀到回車’r’或換行’ n’時會将讀到的字元作為一行的内容傳回。
需要注意的是,由于包裝流内部使用了緩沖區,在循環中調用 BufferedWriter 的 write()方法寫字元時,這些字元首先會被寫入緩沖區,當緩沖區寫滿時或調用 close()方法時,緩沖區中的字元才會被寫入目标檔案。是以在循環結束時一定要調用 close()方法,否則極有可能會導緻部分存在緩沖區中的資料沒有被寫入目标檔案。
轉換流
前面提到 IO 流可分為位元組流和字元流,有時位元組流和字元流之間也需要進行轉換。在 JDK 中提供了兩個類可以将位元組流轉換為字元流,它們分别是 InputStreamReader 和 OutputStreamWriter。
轉換流也是一種包裝流,其中 OutputStreamWriter 是 Writer 的子類,它可以将一個位元組輸出流包裝成字元輸出流,友善直接寫入字元,而 InputStreamReader 是 Reader 的子類,它可以将一個位元組輸入流包裝成字元輸入流,友善直接讀取字元。通過轉換流進行資料讀寫的過程如圖所示。
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-NZ3r10TG-1642058941613)(http://47.107.171.232/easily-j/images/20190111/e7663ad2-4ad5-4632-a1a5-48b0d2b3b90c.png)]
接下來通過一個案例來學習如何将位元組流轉為字元流,為了提高讀寫效率,可以通過 BufferedReader 和 BufferedWriter 對轉換流進行包裝,具體代碼如例所示。
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class IOTest {
public static void main(String[] args) {
try {
FileInputStream in = new FileInputStream("D:/example.txt"); // 建立位元組輸入流
InputStreamReader isr = new InputStreamReader(in);
// 将位元組流輸入轉換成字元輸入流
BufferedReader br = new BufferedReader(isr); // 對字元流對象進行包裝
FileOutputStream out = new FileOutputStream("D:/example.txt");
// 将位元組輸出流轉換成字元輸出流
OutputStreamWriter osw = new OutputStreamWriter(out);
BufferedWriter bw = new BufferedWriter(osw); // 對字元輸出流對象進行包裝
String line;
while ((line = br.readLine()) != null) { // 判斷是否讀到檔案末尾
bw.write(line); // 輸出讀取到的檔案
}
br.close();
bw.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
例實作了位元組流和字元流之間的轉換,将位元組流轉換為字元流,進而實作直接對字元的讀寫。需要注意的是,在使用轉換流時,隻能針對操作文本檔案的位元組流進行轉換,如果位元組流操作的是一張圖檔,此時轉換為字元流就會造成資料丢失。
File 類
本章前面講解的 IO 流可以對檔案的内容進行讀寫操作,在應用程式中還會經常對檔案本身進行一些正常操作,例如建立一個檔案,删除或者重命名某個檔案,判斷硬碟上某個檔案是否存在,查詢檔案最後修改時間等。針對檔案的這類操作,JDK 中提供了一個 File 類,該類封裝了一個路徑,并提供了一系列的方法用于操作該路徑所指向的檔案,接下來圍繞 File 類展開詳細講解。
File 類的常用方法
File 類用于封裝一個路徑,這個路徑可以是從系統盤符開始的絕對路徑,如 D:/example.txt,也可以是相對于目前目錄而言的相對路徑,如 src/example.txt。File 類内部封裝的路徑可以指向一個檔案,也可以指向一個目錄,在 File 類中提供了針對這些檔案或目錄的一些正常操作。接下來首先介紹一下 File 類常用的構造方法,如表所示。
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-cDwqJsfq-1642058941614)(http://47.107.171.232/easily-j/images/20190111/6c73686e-16fc-4ed6-b676-390bb5a68b14.png)]
表中列出了 File 類的三個構造方法。通常來講,如果程式隻處理一個目錄或檔案,并且知道該目錄或檔案的路徑,使用第一個構造方法較友善。如果程式處理的是一個公共目錄中的若幹子目錄或檔案,那麼使用第二個或者第三個構造方法會更友善。
File 類中提供了一系列方法,用于操作其内部封裝的路徑指向的檔案或者目錄,例如判斷檔案/目錄是否存在、建立、删除檔案/目錄等。接下來介紹一下 File 類中的常用方法,如表所示。
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-ZdzxvURF-1642058941615)(http://47.107.171.232/easily-j/images/20190111/62735cf7-6e4a-41f1-a4cf-6c27f56dab7e.png)]
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-gAGgFrG1-1642058941617)(http://47.107.171.232/easily-j/images/20190111/7e92b847-0b5a-4b18-a032-cac4fb58bff5.png)]
表中,列出了 File 類的一系列常用方法,此表僅僅通過文字對 File 類的方法進行介紹,對于初學者來說很難弄清它們之間的差別,接下來,首先在 D 盤目錄下建立一個檔案“example.txt”并輸入内容“小海綿”,然後通過一個案例來示範 File 類的常用方法,如例所示。
import java.io.File;
public class IOTest {
public static void main(String[] args) {
File file = new File("D:/example.txt"); // 建立File 檔案對象,表示一個檔案
// 擷取檔案名稱
System.out.println("檔案名稱:" + file.getName());
// 擷取檔案的相對路徑
System.out.println("檔案的相對路徑:" + file.getPath());
// 擷取檔案的絕對路徑
System.out.println("檔案的絕對路徑:" + file.getAbsolutePath());
// 擷取檔案的父路徑
System.out.println("檔案的父路徑:" + file.getParent());
// 判斷檔案是否可讀
System.out.println(file.canRead() ? "檔案可讀" : "檔案不可讀");
// 判斷檔案是否可寫
System.out.println(file.canWrite() ? "檔案可寫" : "檔案不可寫");
// 判斷是否是一個檔案
System.out.println(file.isFile() ? "是一個檔案" : "不是一個檔案");
// 判斷是否是一個目錄
System.out.println(file.isDirectory() ? "是一個目錄" : "不是一個目錄");
// 判斷是否是一個絕對路徑
System.out.println(file.isAbsolute() ? "是絕對路徑" : "不是絕對路徑");
// 得到檔案最後修改時間
System.out.println("最後修改時間為:" + file.lastModified());
// 得到檔案的大小
System.out.println("檔案大小為:" + file.length() + " bytes");
// 是否成功删除檔案
System.out.println("是否成功删除檔案" + file.delete());
}
}
運作結果:
檔案名稱:example.txt
檔案的相對路徑:D:\example.txt
檔案的絕對路徑:D:\example.txt
檔案的父路徑:D:\
檔案可讀
檔案可寫
是一個檔案
不是一個目錄
是絕對路徑
最後修改時間為:1547187548383
檔案大小為:6 bytes
是否成功删除檔案true
周遊目錄下的檔案
在表中列舉的方法中有一個 list()方法,該方法用于周遊某個指定目錄下的所有檔案的名稱,例 8-25 中沒有示範該方法的使用,接下來通過一個案例來示範 list()方法的用法,如例所示。
import java.io.File;
public class IOTest {
public static void main(String[] args) {
File file = new File("D:/"); // 建立File 對象
if (file.isDirectory()) { // 判斷File 對象對應的目錄是否存在
String[] names = file.list(); // 獲得目錄下的所有檔案的檔案名
for (String name : names) {
System.out.println(name); // 輸出檔案名
}
}
}
}
例中,建立了一個 File 對象,封裝了一個路徑,通過調用 File 的 isDirectory()方法判斷路徑指向的是否為存在的目錄,如果存在就調用 list()方法,獲得一個 String 類型的數組 names,數組中包含這個目錄下所有檔案的檔案名。接着通過循環周遊數組 names,依次列印出每個檔案的檔案名。
例實作了周遊一個目錄下所有的檔案,有時程式隻是需要得到指定類型的檔案,如擷取指定目錄下所有的“.java”檔案。針對這種需求,File 類中提供了一個重載的 list(FilenameFilterfilter)方法,該方法接收一個 FilenameFilter 類型的參數。FilenameFilter 是一個接口,被稱作檔案過濾器,當中定義了一個抽象方法 accept(File dir,String name),在調用 list()方法時,需要實作檔案過濾器,在 accept()方法中做出判斷,進而獲得指定類型的檔案。
為了讓初學者更好地了解檔案過濾的原理,接下來分步驟分析 list(FilenameFilter filter)方法的工作原理。
- 調用 list()方法傳入 FilenameFilter 檔案過濾器對象。
- 取出目前 File 對象所代表目錄下的所有子目錄和檔案。
- 對于每一個子目錄或檔案,都會調用檔案過濾器對象的 accept(File dir,String name)方法,并把代表目前目錄的 File 對象以及這個子目錄或檔案的名字作為參數 dir 和 name 傳遞給方法。
- 如果 accept()方法傳回 true,就将目前周遊的這個子目錄或檔案添加到數組中,如果傳回 false,則不添加。
接下來通過一個案例來示範如何周遊指定目錄下所有擴充名為.java 的檔案,如例所示。
import java.io.File;
import java.io.FilenameFilter;
public class IOTest {
public static void main(String[] args) {
// 建立File 對象
File file = new File("D:/test");
// 建立過濾器對象
FilenameFilter filter = new FilenameFilter() {
// 實作accept()方法
public boolean accept(File dir, String name) {
File currFile = new File(dir, name);
// 如果檔案名以.java 結尾傳回true,否則傳回false
if (currFile.isFile() && name.endsWith(".java")) {
return true;
} else {
return false;
}
}
};
if (file.exists()) { // 判斷File 對象對應的目錄是否存在
String[] lists = file.list(filter); // 獲得過濾後的所有檔案名數組
for (String name : lists) {
System.out.println(name);
}
}
}
}
例的 main()方法中,定義了 FilenameFilter 檔案過濾器對象 filter,并且實作了 accept()方法,在 accept()方法中對目前正在周遊的 currFile 對象進行判斷,隻有當 currFile 對象代表檔案,并且擴充名“.java”時,才傳回 true。在調用 File 對象的 list()方法時将 filter 過濾器對象傳入,就得到包含所有“.java”檔案名字的字元串數組。
前面的兩個例子示範的都是周遊目錄下檔案的檔案名,有時候在一個目錄下,除了檔案,還有子目錄,如果想得到所有子目錄下的 File 類型對象,list()方法顯然不能滿足要求,這時需要使用 File 類提供的另一個方法 listFiles()。listFiles()方法傳回一個 File 對象數組,當對數組中的元素進行周遊時,如果元素中還有子目錄需要周遊,則需要使用遞歸。接下來通過一個案例來實作周遊指定目錄下的檔案,如例所示。
import java.io.File;
public class IOTest {
public static void main(String[] args) {
File file = new File("D:/test"); // 建立一個代表目錄的File 對象
fileDir(file);
}
public static void fileDir(File dir) {
File[] files = dir.listFiles(); // 獲得表示目錄下所有檔案的數組
for (File file : files) { // 周遊所有的子目錄和檔案
if (file.isDirectory()) {
fileDir(file); // 如果是目錄,遞歸調用FileDir()
}
System.out.println(file.getAbsolutePath()); // 輸出檔案的絕對路徑
}
}
}
運作結果:
D:\test\one
D:\test\test.txt
D:\test\two
例中,定義了一個靜态方法 fileDir(),方法接收一個表示目錄的 File 對象。在方法中,首先通過調用 listFiles()方法把該目錄下所有的子目錄和檔案存到一個 File 類型的數組 files 中,接着周遊數組 files,對目前周遊的 File 對象進行判斷,如果是目錄就重新調用 fileDir()方法進行遞歸,如果是檔案就直接列印輸出檔案的路徑,這樣該目錄下的所有檔案就被成功周遊出來了。
删除檔案及目錄
在操作檔案時,經常需要删除一個目錄下的某個檔案或者删除整個目錄,這時大家首先會想到 File 類的 delete()方法,接下來通過一個案例來示範使用 delete()方法删除檔案,如例所示。
import java.io.File;
public class IOTest {
public static void main(String[] args) {
File file = new File("D:/test"); // 這是一個代表目錄的File 對象
if (file.exists()) {
System.out.println(file.delete());
}
}
}
運作結果:
false
圖的運作結果中輸出了 false,這說明删除檔案失敗了。大家可能會疑惑,為什麼會失敗呢? 那是因為 File 類的 delete()方法隻是删除一個指定的檔案,假如 File 對象代表目錄,并且目錄下包含子目錄或檔案,則 File 類的 delete()方法不允許對這個目錄直接删除。在這種情況下,需要通過遞歸的方式将整個目錄以及其中的檔案全部删除,接下來通過一個案例來示範,如例所示。
import java.io.File;
public class IOTest {
public static void main(String[] args) {
File file = new File("D:/test"); // 建立一個代表目錄的File 對象
deleteDir(file); // 調用deleteDir 删除方法
}
public static void deleteDir(File dir) {
if (dir.exists()) { // 判斷傳入的File 對象是否存在
File[] files = dir.listFiles(); // 得到File 數組
for (File file : files) { // 周遊所有的子目錄和檔案
if (file.isDirectory()) {
deleteDir(file); // 如果是目錄,遞歸調用deleteDir()
} else {
// 如果是檔案,直接删除
file.delete();
}
}
// 删除完一個目錄裡的所有檔案後,就删除這個目錄
dir.delete();
}
}
}
例中,定義了一個删除目錄的靜态方法 deleteDir(),接收一個 File 類型的參數。在這個方法中,調用 listFiles()方法把這個目錄下所有的子目錄和檔案儲存到一個 File 類型的數組 files 中,然後周遊 files,如果是目錄就重新調用 deleteDir()方法進行遞歸,如果是檔案就直接調用 File 的 delete()方法删除。當删除完一個目錄下的所有檔案後,再删除目前這個目錄,這樣便從裡層到外層遞歸地删除了整個目錄。
需要注意的是,在 Java 中删除目錄是從虛拟機直接删除而不走資源回收筒,檔案将無法恢複,是以在進行删除操作的時候需要格外小心。
字元編碼
常用字元集
看戰争片時,經常會看到劇中出現收發電報的情況,發報員拿着密碼本将文字翻譯成某種碼文發出,收報員使用同樣的密碼本将收到的碼文再翻譯成文字。這個密碼本其實是發送方和接收方約定的一套電碼表,電碼表中規定了文字和電碼之間的一一對應關系。
在計算機之間,同樣無法直接傳輸一個一個的字元,而隻能傳輸二進制資料。為了使發送的字元資訊能以二進制資料的形式進行傳輸,同樣需要使用一種“密碼本”,它叫做字元碼表。字元碼表是一種可以友善計算機識别的特定字元集,它是将每一個字元和一個唯一的數字對應而形成的一張表。針對不同的文字,每個國家都制定了自己的碼表,下面就來介紹幾種最常用的字元碼表,如表所示。
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-esDhzKLt-1642058941619)(http://47.107.171.232/easily-j/images/20190111/86838adf-7d69-4ac1-b642-54cd02c634d5.png)]
表中列舉了最常用的幾種碼表,通過選擇合适的碼表就能完成字元和二進制資料之間的轉換,進而實作資料的傳輸。