計算機程式的思維邏輯 (59) - 檔案和目錄操作

swiftma發表於2016-12-28

本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結

計算機程式的思維邏輯 (59) - 檔案和目錄操作

前面兩節我們介紹瞭如何通過流的方式讀寫檔案內容,本節我們介紹檔案後設資料和目錄的一些操作。

檔案和目錄操作最終是與作業系統和檔案系統相關的,不同系統的實現是不一樣的,但Java中的java.io.File類提供了統一的介面,底層它會通過本地方法呼叫作業系統和檔案系統的具體實現,本節,我們就來介紹File類。

File類中的操作大概可以分為三類:

  • 檔案後設資料
  • 檔案操作
  • 目錄操作

在介紹這些操作之前,我們先來看下File的構造方法。

構造方法

File既可以表示檔案,也可以表示目錄,它的主要構造方法有:

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

可以是一個引數pathname,表示完整路徑,該路徑可以是相對路徑,也可以是絕對路徑。還可以是兩個引數,表示父目錄的parent和表示孩子的child。

File中的路徑可以是已經存在的,也可以是不存在的。

通過new新建一個File物件,不會實際建立一個檔案,只是建立一個表示檔案或目錄的物件,new之後,File物件中的路徑是不可變的。

檔案後設資料

檔名與檔案路徑

有了File物件後,就可以獲取它的檔名和路徑資訊,相關方法有:

public String getName()
public boolean isAbsolute()
public String getPath()
public String getAbsolutePath()
public String getCanonicalPath() throws IOException
public String getParent()
public File getParentFile()
public File getAbsoluteFile()
public File getCanonicalFile() throws IOException
複製程式碼

getName()返回的就是檔案或目錄名稱,不含路徑名。isAbsolute()判斷File中的路徑是否是絕對路徑。

getPath()返回構造File物件時的完整路徑名,包括路徑和檔名稱。getAbsolutePath()返回完整的絕對路徑名。getCanonicalPath()返回標準的完整路徑名,它會去掉路徑中的冗餘名稱如".","..",跟蹤軟連線(Unix系統概念)等。這三個路徑容易混淆,我們看一個例子來說明:

File f = new File("../io/test/students.txt");
System.out.println(System.getProperty("user.dir"));
System.out.println("path: " + f.getPath());
System.out.println("absolutePath: " + f.getAbsolutePath());
System.out.println("canonicalPath: " + f.getCanonicalPath());
複製程式碼

這裡,使用相對路徑來構造File物件,..表示上一級目錄,輸出為:

/Users/majunchang/io
path: ../io/test/students.txt
absolutePath: /Users/majunchang/io/../io/test/students.txt
canonicalPath: /Users/majunchang/io/test/students.txt
複製程式碼

當前目錄為/Users/majunchang/io,getPath()返回的就是構造File物件時使用的相對路徑,而getAbsolutePath()返回的是完整路徑,但是包含冗餘路徑"../io/",而getCanonicalPath()則去除了該冗餘路徑。

getParent()返回父目錄路徑,getParentFile()返回父目錄的File物件,需要注意的是,如果File物件是相對路徑,則這些方法可能得不到父目錄,比如:

File f = new File("students.txt");
System.out.println(System.getProperty("user.dir"));
System.out.println("parent: " + f.getParent());
System.out.println("parentFile: " + f.getParentFile());
複製程式碼

輸出為:

/Users/majunchang/io
parent: null
parentFile: null
複製程式碼

即使是有父目錄的,getParent()的返回值也是null。那如何解決這個問題呢?可以先使用getAbsoluteFile()或getCanonicalFile()方法,它們都返回一個新的File物件,新的File物件分別使用getAbsolutePath()和getCanonicalPath()的返回值作為引數構造。比如,修改上面的程式碼為:

File f = new File("students.txt");
System.out.println(System.getProperty("user.dir"));
System.out.println("parent: " + f.getCanonicalFile().getParent());
System.out.println("parentFile: " + f.getCanonicalFile().getParentFile());
複製程式碼

這次,就能得到父目錄了,輸出為:

/Users/majunchang/io
parent: /Users/majunchang/io
parentFile: /Users/majunchang/io
複製程式碼

File類中有四個靜態變數,表示路徑分隔符,它們是:

public static final String separator
public static final char separatorChar
public static final String pathSeparator
public static final char pathSeparatorChar
複製程式碼

separator和separatorChar表示檔案路徑分隔符,在Windows系統中,一般為"",Linux系統中一般為"/"。

pathSeparator和pathSeparatorChar表示多個檔案路徑中的分隔符,比如環境變數PATH中的分隔符,Java類路徑變數classpath中的分隔符,在執行命令時,作業系統會從PATH指定的目錄中尋找命令,Java執行時載入class檔案時,會從classpath指定的路徑中尋找類檔案。在Windows系統中,這個分隔符一般為';',在Linux系統中,這個分隔符一般為':'。

檔案基本資訊

除了檔名和路徑,File物件還有如下方法,以獲取檔案或目錄的基本資訊:

//檔案或目錄是否存在
public boolean exists()
//是否為目錄
public boolean isDirectory()
//是否為檔案
public boolean isFile()
//檔案長度,位元組數
public long length()
//最後修改時間,從紀元時開始的毫秒數
public long lastModified()
//設定最後修改時間,設定成功返回true,否則返回false
public boolean setLastModified(long time)
複製程式碼

對於目錄,length()方法的返回值是沒有意義的。

需要說明的是,File物件沒有返回建立時間的方法,因為建立時間不是一個公共概念,Linux/Unix就沒有建立時間的概念。

安全和許可權資訊

File類中與安全和許可權相關的方法有:

//是否為隱藏檔案
public boolean isHidden()
//是否可執行
public boolean canExecute()
//是否可讀
public boolean canRead()
//是否可寫
public boolean canWrite()
//設定檔案為只讀檔案
public boolean setReadOnly()
//修改檔案讀許可權
public boolean setReadable(boolean readable, boolean ownerOnly)
public boolean setReadable(boolean readable)
//修改檔案寫許可權
public boolean setWritable(boolean writable, boolean ownerOnly)
public boolean setWritable(boolean writable)
//修改檔案可執行許可權
public boolean setExecutable(boolean executable, boolean ownerOnly)
public boolean setExecutable(boolean executable)
複製程式碼

在修改方法中,如果修改成功,返回true,否則返回false。在設定許可權方法中,ownerOnly為true表示只針對owner,為false表示針對所有使用者,沒有指定ownerOnly的方法中,ownerOnly相當於是true。

檔案操作

檔案操作主要有建立、刪除、重新命名。

建立

新建一個File物件不會實際建立檔案,但如下方法可以:

public boolean createNewFile() throws IOException 
複製程式碼

建立成功返回true,否則返回false,新建立的檔案內容為空。如果檔案已存在,不會建立。

File物件還有兩個靜態方法,可以建立臨時檔案:

public static File createTempFile(String prefix, String suffix) throws IOException
public static File createTempFile(String prefix, String suffix, File directory) throws IOException
複製程式碼

臨時檔案的完整路徑名是系統指定的、唯一的,但可以通過引數指定字首(prefix)、字尾(suffix)和目錄(directory),prefix是必須的,且至少要三個字元,suffix如果為null,則預設為".tmp", directory如果不指定或指定為null,則使用系統預設目錄。我們看個例子:

File file = File.createTempFile("upload_", ".jpg");
System.out.println(file.getAbsolutePath());
複製程式碼

在我的電腦上的一些執行的輸出為:

/var/folders/fs/8s4jdbj51jvcm7vc6lm_144r0000gn/T/upload_8850973909847443784.jpg
複製程式碼

刪除

File類如下刪除方法:

public boolean delete()
public void deleteOnExit()
複製程式碼

delete刪除檔案或目錄,刪除成功返回true,否則返回false。如果File是目錄且不為空,則delete不會成功,返回false,換句話說,要刪除目錄,先要刪除目錄下的所有子目錄和檔案。

deleteOnExit將File物件加入到待刪列表,在Java虛擬機器正常退出的時候進行實際刪除。

重新命名

方法為:

public boolean renameTo(File dest) 
複製程式碼

引數dest代表重新命名後的檔案,重新命名能否成功與系統有關,如果成功返回true,否則返回false。

目錄操作

當File物件代表目錄時,可以執行目錄相關的操作,如建立、遍歷。

建立

有兩個方法用於建立目錄:

public boolean mkdir()
public boolean mkdirs()
複製程式碼

它們都是建立目錄,建立成功返回true,失敗返回false。需要注意的是,如果目錄已存在,返回值是false。這兩個方法的區別在於,如果某一箇中間父目錄不存在,則mkdir會失敗,返回false,而mkdirs則會建立必需的中間父目錄。

遍歷

有如下方法訪問一個目錄下的子目錄和檔案:

public String[] list()
public String[] list(FilenameFilter filter)
public File[] listFiles()
public File[] listFiles(FileFilter filter)
public File[] listFiles(FilenameFilter filter)
複製程式碼

它們返回的都是直接子目錄或檔案,不會返回子目錄下的檔案。list返回的是檔名陣列,而listFiles返回的是File物件陣列。FilenameFilter和FileFilter都是介面,用於過濾,FileFilter的定義為:

public interface FileFilter {
    boolean accept(File pathname);
}
複製程式碼

FilenameFilter的定義為:

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

在遍歷子目錄和檔案時,針對每個檔案,會呼叫FilenameFilter或FileFilter的accept方法,只有accept方法返回true時,才將該子目錄或檔案包含到返回結果中。

FilenameFilter和FileFilter的區別在於,FileFilter的accept方法引數只有一個File物件,而FilenameFilter的accept方法引數有兩個,dir表示父目錄,name表示子目錄或檔名。

我們來看個例子,列出當前目錄下的所有字尾為.txt的檔案,程式碼可以為:

File f = new File(".");
File[] files = f.listFiles(new FilenameFilter(){
    @Override
    public boolean accept(File dir, String name) {
        if(name.endsWith(".txt")){
            return true;
        }
        return false;
    }
});
for(File file : files){
    System.out.println(file.getCanonicalPath());
}
複製程式碼

我們建立了個FilenameFilter的匿名內部類物件並傳遞給了listFiles。

使用遍歷方法,我們可以方便的進行遞迴遍歷,完成一些更為高階的功能。

比如,計算一個目錄下的所有檔案的大小(包括子目錄),程式碼可以為:

public static long sizeOfDirectory(final File directory) {
    long size = 0;
    if (directory.isFile()) {
        return directory.length();
    } else {
        for (File file : directory.listFiles()) {
            if (file.isFile()) {
                size += file.length();
            } else {
                size += sizeOfDirectory(file);
            }
        }
    }
    return size;
}
複製程式碼

再比如,在一個目錄下,查詢所有給定檔名的檔案,程式碼可以為:

public static Collection<File> findFile(final File directory,
        final String fileName) {
    List<File> files = new ArrayList<>();
    for (File f : directory.listFiles()) {
        if (f.isFile() && f.getName().equals(fileName)) {
            files.add(f);
        } else if (f.isDirectory()) {
            files.addAll(findFile(f, fileName));
        }
    }
    return files;
}
複製程式碼

前面介紹了File類的delete方法,我們提到,如果要刪除目錄而目錄不為空,需要先清空目錄,利用遍歷方法,我們可以寫一個刪除非空目錄的方法,程式碼可以為:

public static void deleteRecursively(final File file) throws IOException {
    if (file.isFile()) {
        if (!file.delete()) {
            throw new IOException("Failed to delete "
                    + file.getCanonicalPath());
        }
    } else if (file.isDirectory()) {
        for (File child : file.listFiles()) {
            deleteRecursively(child);
        }
        if (!file.delete()) {
            throw new IOException("Failed to delete "
                    + file.getCanonicalPath());
        }
    }
}
複製程式碼

小結

本節介紹瞭如何在Java中利用File類進行檔案和目錄操作,File類封裝了作業系統和檔案系統的差異,提供了統一的API。

理解了這些操作,我們回過頭來,再看下檔案內容的操作,前面我們介紹的都是流,除了流,還有其他操作方式,如隨機訪問和記憶體對映檔案,為什麼還需要這些方式?它們有什麼特點?適用於什麼場合?讓我們接下來繼續探索。


未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。

計算機程式的思維邏輯 (59) - 檔案和目錄操作

相關文章