天天看點

【Java】9、Java IO 流

文章目錄

  • ​​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)]

表中列舉了最常用的幾種碼表,通過選擇合适的碼表就能完成字元和二進制資料之間的轉換,進而實作資料的傳輸。

微信公衆号