## IO
大多數應用程序都需要實現與設備之間的數據傳輸,例如鍵盤可以輸入數據,顯示器可以顯示程序的運行結果等。在Java中,將這種通過不同輸入輸出設備(鍵盤,內存,顯示器,網絡等)之間的數據傳輸抽象表述為“流”,程序允許通過流的方式與輸入輸出設備進行數據傳輸。Java中的“流”都位于java.io包中,稱為IO(輸入輸出)流。
IO流有很多種,按照操作數據的不同,可以分為字節流和字符流,按照數據傳輸方向的不同又可分為輸入流和輸出流,程序從輸入流中讀取數據,向輸出流中寫入數據。在IO包中,字節流的輸入輸出流分別用java.io.InputStream 和java.io.OutputStream 表示,字符流的輸入輸出流分別用java.io.Reader和java.io.Writer表示,具體分類如圖所示。

## 字節流
#### 字節流概念
在計算機中,無論是文本、圖片、音頻還是視頻,所有的文件都是以二進制(字節)形式存在,IO流中針對字節的輸入輸出提供了一系列的流,統稱為字節流。字節流是程序中最常用的流,根據數據的傳輸方向可將其分為字節輸入流和字節輸出流。在JDK 中,提供了兩個抽象類InputStream 和OutputStream,它們是字節流的頂級父類,所有的字節輸入流都繼承自InputStream,所有的字節輸出流都繼承自OutputStream。為了方便理解,可以把InputStream 和OutputStream 比作兩根“水管”,如圖所示。

圖中,InputStream 被看成一個輸入管道,OutputStream 被看成一個輸出管道,數據通過InputStream 從源設備輸入到程序,通過OutputStream 從程序輸出到目標設備,從而實現數據的傳輸。由此可見,IO流中的輸入輸出都是相對于程序而言的。
在JDK中,InputStream 和OutputStream 提供了一系列與讀寫數據相關的方法,接下來先來了解一下InputStream 的常用方法,如表所示。

表中列舉了InputStream 的四個常用方法。前三個read()方法都是用來讀數據的,其中,第一個read()方法是從輸入流中逐個讀入字節,而第二個和第三個read()方法則將若干字節以字節數組的形式一次性讀入,從而提高讀數據的效率。在進行IO 流操作時,當前IO流會占用一定的內存,由于系統資源寶貴,因此,在IO 操作結束后,應該調用close()方法關閉流,從而釋放當前IO流所占的系統資源。
與InputStream 對應的是OutputStream。OutputStream 是用于寫數據的,因此OutputStream 提供了一些與寫數據有關的方法,如表所示。

表中,列舉了OutputStream 類的五個常用方法。前三個是重載的write()方法,都是用于向輸出流寫入字節,其中,第一個方法逐個寫入字節,后兩個方法是將若干個字節以字節數組的形式一次性寫入,從而提高寫數據的效率。flush()方法用來將當前輸出流緩沖區(通常是字節數組)中的數據強制寫入目標設備,此過程稱為刷新。close()方法是用來關閉流并釋放與當前IO流相關的系統資源。
InputStream 和OutputStream 這兩個類雖然提供了一系列和讀寫數據有關的方法,但是這兩個類是抽象類,不能被實例化,因此,針對不同的功能,InputStream 和OutputStream 提供了不同的子類,這些子類形成了一個體系結構,如圖所示。


從圖中可以看出,InputStream 和OutputStream 的子類有很多是大致對應的,比如ByteArrayInputStream 和ByteArrayOutputStream,FileInputStream 和FileOutputStream 等。圖中所列出的IO 流都是程序中很常見的,接下來將逐步為大家講解這些流的具體用法。
#### 字節流讀寫文件
由于計算機中的數據基本都保存在硬盤的文件中,因此操作文件中的數據是一種很常見的操作。在操作文件時,最常見的就是從文件中讀取數據并將數據寫入文件,即文件的讀寫。針對文件的讀寫,JDK 專門提供了兩個類,分別是FileInputStream 和FileOutputStream。
FileInputStream 是InputStream 的子類,它是操作文件的字節輸入流,專門用于讀取文件中的數據。由于從文件讀取數據是重復的操作,因此需要通過循環語句來實現數據的持續讀取。接下來通過一個案例來實現字節流對文件數據的讀取,首先在D盤目錄下創建一個文本文件IO.txt,在文件中輸入內容“小海綿”,具體代碼如例所示。
```java
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 的子類,它是操作文件的字節輸出流,專門用于把數據寫入文件。接下來通過一個案例來演示如何將數據寫入文件,如例所示。
```java
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。接下來通過一個案例來演示如何將數據追加到文件末尾,如例所示。
```java
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”文件,拷貝文件的代碼如例所示。
```java
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,表示讀取到了文件的末尾,結束循環,完成文件的拷貝。程序運行結束后,會在命令行窗口打印拷貝文件所消耗的時間。
#### 字節流的緩沖區
雖然上一個案例實現了文件的拷貝,但是一個字節一個字節的讀寫,需要頻繁的操作文件,效率非常低,這就好比從北京運送烤鴨到上海,如果有一萬只烤鴨,每次運送一只,就必須運輸一萬次,這樣的效率顯然非常低。為了減少運輸次數,可以先把一批烤鴨裝在車廂中,這樣就可以成批的運送烤鴨,這時的車廂就相當于一個臨時緩沖區。當通過流的方式拷貝文件時,為了提高效率也可以定義一個字節數組作為緩沖區。在拷貝文件時,可以一次性讀取多個字節的數據,并保存在字節數組中,然后將字節數組中的數據一次性寫入文件。接下來通過一個案例來學習如何使用緩沖區拷貝文件,如例所示。
```java
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時,說明已經讀到了文件的末尾,循環會結束,整個拷貝過程也就結束了,最終程序將整個拷貝過程所消耗的時間打印了出來。
通過兩種拷貝方式的對比,可以看出拷貝文件所消耗的時間明顯減少了,從而說明緩沖區讀寫文件可以有效的提高程序的效率。這是因為程序中的緩沖區就是一塊內存,用于存放暫時輸入輸出的數據,使用緩沖區減少了對文件的操作次數,所以可以提高讀寫數據的效率。
#### 裝飾設計模式
俗話說“人靠衣裝馬靠鞍”,漂亮得體的裝扮不僅能提升形象,還能提高競爭力。在程序設計中,同樣可以通過“裝飾”一個類,增強它的功能。裝飾設計模式就是通過包裝一個類,動態地為它增加功能的一種設計模式。
裝飾設計模式在現實生活中隨處可見,比如買了一輛車,想為新車裝一個倒車雷達,這就相當于為這輛汽車增加新的功能。接下來通過一個案例來實現上述過程,如例所示。
```java
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 類型的參數作為被包裝對象,在讀寫數據時提供緩沖功能。應用程序、緩沖流和底層字節流之間的關系如圖所示。

從圖中可以看出應用程序是通過緩沖流來完成數據讀寫的,而緩沖流又是通過底層被包裝的字節流與設備進行關聯的。接下來通過一個案例來學習BufferedInputStream和BufferedOutputStream 這兩個流的用法,如例所示。
```java
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的一些常用子類,如圖所示。


從圖可以看到,字符流的繼承關系與字節流的繼承關系有些類似,很多子類都是成對(輸入流和輸出流)出現,其中FileReader和FileWriter用于讀寫文件,BufferedReader和BufferedWriter是具有緩沖功能的流,它們可以提高讀寫效率。
#### 字符流操作文件
在程序開發中,經常需要對文本文件的內容進行讀取,如果想從文件中直接讀取字符便可以使用字符輸入流FileReader,通過此流可以從關聯的文件中讀取一個或一組字符。接下來首先在D盤目錄下新建文件“example.txt”并在其中輸入字符“小海綿”,然后通過一個案例來學習如何使用FileReader讀取文件中的字符,如例所示。
```java
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將字符寫入文件,如例所示。
```java
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實現文件的拷貝,如例所示。
```java
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的子類,它可以將一個字節輸入流包裝成字符輸入流,方便直接讀取字符。通過轉換流進行數據讀寫的過程如圖所示。

接下來通過一個案例來學習如何將字節流轉為字符流,為了提高讀寫效率,可以通過BufferedReader和BufferedWriter對轉換流進行包裝,具體代碼如例所示。
```java
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類常用的構造方法,如表所示。

表中列出了File類的三個構造方法。通常來講,如果程序只處理一個目錄或文件,并且知道該目錄或文件的路徑,使用第一個構造方法較方便。如果程序處理的是一個公共目錄中的若干子目錄或文件,那么使用第二個或者第三個構造方法會更方便。
File類中提供了一系列方法,用于操作其內部封裝的路徑指向的文件或者目錄,例如判斷文件/目錄是否存在、創建、刪除文件/目錄等。接下來介紹一下File類中的常用方法,如表所示。


表中,列出了File類的一系列常用方法,此表僅僅通過文字對File類的方法進行介紹,對于初學者來說很難弄清它們之間的區別,接下來,首先在D盤目錄下創建一個文件“example.txt”并輸入內容“小海綿”,然后通過一個案例來演示File類的常用方法,如例所示。
```java
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()方法的用法,如例所示。
```java
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的文件,如例所示。
```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對象數組,當對數組中的元素進行遍歷時,如果元素中還有子目錄需要遍歷,則需要使用遞歸。接下來通過一個案例來實現遍歷指定目錄下的文件,如例所示。
```java
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()方法刪除文件,如例所示。
```java
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()方法不允許對這個目錄直接刪除。在這種情況下,需要通過遞歸的方式將整個目錄以及其中的文件全部刪除,接下來通過一個案例來演示,如例所示。
```java
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中刪除目錄是從虛擬機直接刪除而不走回收站,文件將無法恢復,因此在進行刪除操作的時候需要格外小心。
## 字符編碼
#### 常用字符集
看戰爭片時,經常會看到劇中出現收發電報的情況,發報員拿著密碼本將文字翻譯成某種碼文發出,收報員使用同樣的密碼本將收到的碼文再翻譯成文字。這個密碼本其實是發送方和接收方約定的一套電碼表,電碼表中規定了文字和電碼之間的一一對應關系。
在計算機之間,同樣無法直接傳輸一個一個的字符,而只能傳輸二進制數據。為了使發送的字符信息能以二進制數據的形式進行傳輸,同樣需要使用一種“密碼本”,它叫做字符碼表。字符碼表是一種可以方便計算機識別的特定字符集,它是將每一個字符和一個唯一的數字對應而形成的一張表。針對不同的文字,每個國家都制定了自己的碼表,下面就來介紹幾種最常用的字符碼表,如表所示。

表中列舉了最常用的幾種碼表,通過選擇合適的碼表就能完成字符和二進制數據之間的轉換,從而實現數據的傳輸。