表示一個檔案的 File 型別

YangAM發表於2018-05-10

從本篇文章開始,我們將開啟對 Java IO 系統的學習,本質上就是對檔案的讀寫操作,聽上去簡單,其實並不容易。Java 的 IO 系統一直在完善和改進,設計了大量的類,也只有理解了這些型別被設計出來的意義以及各自的應用場景,才能提升檔案 IO 的理解。

那麼,第一步就是要解決如何表示一個檔案的問題,Java 世界中「萬物皆物件」,如何將一個實際磁碟檔案或目錄對應到一個 Java 物件則是我們首要的問題。

Java 中使用 File 來抽象一個檔案,無論是普通檔案或是目錄,都可對應於一個 File 物件。我覺得大家對於 File 這個型別的定位一定要準確:它只是抽象的代表了磁碟上的某個檔案或目錄,內部實際上是依賴一個平臺無關的本地檔案系統類,並且 File 無法對其所表示檔案內容進行任何讀寫操作(那是流做的事情)

構建一個 File 例項

在實際介紹 File 例項構造方法之前,我們得先看看它的幾種重要的屬性成員。

private static final FileSystem fs = DefaultFileSystem.getFileSystem();
複製程式碼

這是 File 類中最核心的成員,它表示為當前系統的檔案系統 API,所有向磁碟發出的操作都是基於這個屬性的。

private final String path;
複製程式碼

path 代表了當前例項的完整路徑名稱,如果當前的 File 例項表示的是目錄的話,那麼 path 的值就是這個完整的目錄名稱,如果表示的是純檔案的話,那麼這個 path 的值等於該檔案的完整路徑 + 檔名稱。

public static final char separatorChar = fs.getSeparator();
public static final char pathSeparatorChar = fs.getPathSeparator();
複製程式碼

separatorChar 表示的是目錄間的分隔符,pathSeparatorChar 表示的是不同路徑下的分隔符,這兩個值在不同的系統平臺下不盡相同。例如 Windows 下這兩者的值分別為:「\」 和 「;」,其中封號用於分隔多個不同路徑。

File 類提供了四種不同的構造器用於例項化一個 File 物件,但較為常用的只有三個,我們也著重學習前三個構造器。

public File(String pathname)
複製程式碼

這是最普遍的例項化一個 File 物件的方法,pathname 的值可以是一個目錄,也可以是一個純檔案的名稱。例如:

File file = new File("C:\\Users\\yanga\\Desktop");

File file1 = new File("C:\\Users\\yanga\\Desktop\\a.txt");

File file2 = new File("a.txt");
複製程式碼

當然也可以顯式指定一個父路徑:

public File(String parent, String child)
複製程式碼

在構造器的內部,程式會為我們拼接出一個完整的檔案路徑,例如:

File file = new File("C:\\Users\\yanga\\Desktop","a.txt");

File file1 = new File("C:\\Users\\yanga\\Desktop","java");
複製程式碼

第三種構造器其實本質上和第二種是一樣的,只不過增加了一個父類 File 例項的封裝過程:

public File(File parent, String child)
複製程式碼

類似的情況,不再舉例說明了。我們這裡並沒有深究這些構造器的內部具體實現情況,並不是說它簡單,而是 File 過度依賴本地檔案系統,很多方法的實現情況都不得直接看到,所以對於 File 的學習,定位為熟練掌握即可,具體實現暫時沒法深入學習。

檔名稱或路徑相關資訊獲取

getName 方法可以用於獲取檔名稱:

public String getName() {
    int index = path.lastIndexOf(separatorChar);
    if (index < prefixLength) return path.substring(prefixLength);
    return path.substring(index + 1);
}
複製程式碼

還記得我們的 separatorChar 表示的是什麼了嗎?

它表示為路徑分隔符,Windows 中為符號「\」,path 屬性儲存的當前 File 例項的完整路徑名稱,所以最後一次出現的位置後面所有的字元必然是我們的檔名稱。

當然你一定發現了,對於純檔案來說,該方法能夠返回檔案的簡單名稱,而對於一個目錄而言,返回值將會是最近的目錄名。例如:

File file = new File("C:\\Users\\yanga\\Desktop\\a.txt");
System.out.println(file.getName());

File file1 = new File("C:\\Users\\yanga\\Desktop");
System.out.println(file1.getName());
複製程式碼

輸出結果不會出乎你的意料:

a.txt
Desktop
複製程式碼

getParent 方法用於返回當前檔案的父級目錄,無論你是純檔案或是目錄,你終有你的父目錄(當然,虛擬機器生成的臨時檔案自然不是)。

public String getParent() {
    int index = path.lastIndexOf(separatorChar);
    if (index < prefixLength) {
        if ((prefixLength > 0) && (path.length() > prefixLength))
            return path.substring(0, prefixLength);
        return null;
    }
    return path.substring(0, index);
}
複製程式碼

方法的實現很簡單,不再贅述。

getPath 方法可以返回當前 File 例項的完整檔名稱:

public String getPath() {
    return path;
}
複製程式碼

以下是一些有關目錄的相關操作,實現比較簡單,此處簡單羅列了:

  • public boolean isAbsolute():是否為絕對路徑
  • public String getAbsolutePath():獲取當前 File 例項的絕對路徑
  • public String getCanonicalPath():返回當前 File 例項的標準路徑

這裡我們需要對 getCanonicalPath 做一點解釋,什麼叫標準路徑,和絕對路徑有區別嗎?

一般而言,「../」表示原始檔所在目錄的上一級目錄,「../../」表示原始檔所在目錄的上上級目錄,並以此類推。getAbsolutePath 方法不會做這種轉換的操作,而 getCanonicalPath 方法則會將這些特殊字元進行識別並取合適的語義。

例如:

File file = new File("..\\a.txt");
System.out.println(file.getAbsolutePath());
System.out.println(file.getCanonicalPath());
複製程式碼

輸出結果:

C:\Users\yanga\Desktop\Java\workspace2017\TestFile\..\a.txt
C:\Users\yanga\Desktop\Java\workspace2017\a.txt
複製程式碼

前者會將「..\a.txt」作為檔案路徑名稱的一部分,而後者卻能夠識別「..\a.txt」表示的是「a.txt」位於當前目錄的上級目錄中。這就是兩者最大的不同之處,適合不同的情境。

檔案的屬性資訊獲取

這部分的檔案操作其實很簡單,無非是一些檔案許可權的問題,是否可讀,是否可寫,是否為隱藏檔案等。下面我們具體看看這些方法:

  • public boolean canRead():該抽象的 File 例項對應的檔案是否可讀
  • public boolean canWrite():該抽象的 File 例項對應的檔案是否可寫
  • public boolean exists():該抽象的 File 例項對應的檔案是否實際存在
  • public boolean isDirectory():該抽象的 File 例項對應的檔案是否是一個目錄
  • public boolean isFile():該抽象的 File 例項對應的檔案是否是一個純檔案
  • public boolean isHidden():該抽象的 File 例項對應的檔案是否是一個隱藏檔案
  • public long length():檔案內容所佔的位元組數

需要說明一點的是,length 方法對於純檔案來說,可以正確返回該檔案的位元組總數,但是對於一個目錄而言,返回值將會是一個「unspecified」的數值,既不是目錄下所有檔案的總位元組數,也不是零,只是一個未被說明的數值,沒有意義。

檔案的操作

檔案的操作無外乎「增刪改查」,下面我們一起來看看。

  • public boolean createNewFile():根據抽象的 File 物件建立一個實際存在的磁碟檔案
  • public boolean delete():刪除該 File 物件對應的磁碟檔案,刪除失敗會返回 false

當然,處理上述兩個簡單的新建和刪除操作,File 類還提供了所謂「查詢」操作,這個我們要好好學習一下。例如:

public String[] list() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(path);
    }
    if (isInvalid()) {
        return null;
    }
    return fs.list(this);
}
複製程式碼

這個方法會檢索出當前例項所代表的目錄下所有的「純檔案」和「目錄」簡單名稱集合。例如:

File file = new File("C:\\Users\\yanga\\Desktop");
String[] list = file.list();
for (String str : list){
    System.out.println(str);
}
複製程式碼

程式的輸出結果會列印我電腦桌面目錄下所有的檔案的簡單名稱,就不給大家看了。

需要注意一點,如果我們的 File 例項對應的不是一個目錄,而是一個純檔案,那麼 list 將返回 null。

接著,我們再看一個檢索目錄檔案的方法:

 public String[] list(FilenameFilter filter) {
    String names[] = list();
    if ((names == null) || (filter == null)) {
        return names;
    }
    List<String> v = new ArrayList<>();
    for (int i = 0 ; i < names.length ; i++) {
        if (filter.accept(this, names[i])) {
            v.add(names[i]);
        }
    }
    return v.toArray(new String[v.size()]);
}
複製程式碼

這個方法其實是 list 的過載版本,它允許傳入一個過濾器用於檢索目錄時只篩選我們需要的檔案及目錄。

而這個 FilenameFilter 介面的定義卻是如此簡單:

public interface FilenameFilter {
    boolean accept(File dir, String name);
}
複製程式碼

只需要重寫這個 accept 方法即可,list 的 for 迴圈每獲取一個檔案或目錄就會嘗試著先呼叫這個過濾方法,如果通過篩選,才會將當前檔案的簡單名稱新增進返回集合中。

所以這個 accept 方法的重寫就決定著哪些檔案能夠通過篩選,哪些則不能。我們看個例子:

我的桌面上 test 資料夾下檔案情況如下:

image

File file = new File("C:\\Users\\yanga\\Desktop\\test");
    String[] list = file.list(
        new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                // dir 代表的當前 File 物件
                //name 是當前遍歷的檔案項的簡單名稱
                if (!name.endsWith(".txt"))
                    return false;
                else
                    return true;
            }
        }
    );
    for (String str : list){
        System.out.println(str);
    }
複製程式碼

這裡呢,我們使用匿名內部類建立一個 FilenameFilter 的子類例項,然後實現了它的 accept 方法,具體的實現很簡單,過濾掉所有的目錄並取出所有純檔案的簡單名稱。

最後輸出結果如下:

3.txt
4.txt
複製程式碼

當然,File 類中還提供了兩個「變種」list 方法,例如:

  • public File[] listFiles()
  • public File[] listFiles(FilenameFilter filter)

它們不再返回目標目錄下的「純檔案」和「目錄」的簡單名稱,而返回它們所對應的 File 物件,其實也沒什麼,目標目錄 + 簡單名稱 即可構建出這些 File 例項了。

所以,本質上說,list 方法並不會遍歷出目標目錄下的所有檔案,即目標目錄的子目錄中的檔案並不會被訪問遍歷。

所以你應當思考如何完成目標目錄下所有檔案的遍歷,包含一級子目錄下的深層次檔案的遍歷。文末將給出答案。

接下來的兩個方法和資料夾的建立有關:

  • public boolean mkdir()
  • public boolean mkdirs()

兩者都是依據的當前 File 例項建立資料夾,關於它們的不同點,我們先看一段程式碼:

File file = new File("C:\\Users\\yanga\\Desktop\\test2");
System.out.println(file.mkdir());

File file2 = new File("C:\\Users\\yanga\\Desktop\\test3\\hello");
System.out.println(file2.mkdir());
複製程式碼

其中,test2 和 test3 在程式執行之前都不存在。

輸出結果如下:

true
false
複製程式碼

為什麼後者建立失敗了?

這源於 mkdir 方法一次只能建立一個資料夾,倘若給定的目錄的父級或更上層目錄存在未被建立的目錄,那麼將導致建立失敗。

而 mkdirs 方法就是用於解決這種情境的,它會建立目標路徑上所有未建立的目錄,看程式碼:

File file3 = new File("C:\\Users\\yanga\\Desktop\\test3\\hello\\231");
System.out.println(file3.mkdirs());
複製程式碼

即便我們 test3 資料夾就不存在,程式執行之後,test3、hello、231 這三個資料夾都會被建立出來。

除此之外,File 還有一類建立臨時檔案的方法,所謂臨時檔案即:執行期存在,虛擬機器關閉時銷燬。大家可以自行研究,使用上還是比較簡單的,這裡不再贅述了。

至此,有關 File 這個檔案型別,我們大致學習了一下,想必大家都會或多或少的感覺到將純檔案和目錄使用同一個型別進行表示的設計似乎有些混亂不合理。知道 jdk1.7 sun 推出了 Files 和 Path 分離了檔案和目錄,我們後續文章會詳細學習一下。


文章中的所有程式碼、圖片、檔案都雲端儲存在我的 GitHub 上:

(https://github.com/SingleYam/overview_java)

歡迎關注微信公眾號:撲在程式碼上的高爾基,所有文章都將同步在公眾號上。

image

相關文章