前言
從本篇文章開始,我們將開啟對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.txtDesktop
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;}以下是一些有關目錄的相關操作,實現比較簡單,此處簡單羅列了:
這裡我們需要對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」位於當前目錄的上級目錄中。這就是兩者最大的不同之處,適合不同的情境。
文件的屬性信息獲取
這部分的文件操作其實很簡單,無非是一些文件權限的問題,是否可讀,是否可寫,是否為隱藏文件等。下面我們具體看看這些方法:
需要說明一點的是,length 方法對於純文件來說,可以正確返回該文件的字節總數,但是對於一個目錄而言,返回值將會是一個「unspecified」的數值,既不是目錄下所有文件的總字節數,也不是零,只是一個未被說明的數值,沒有意義。
文件的操作
文件的操作無外乎「增刪改查」,下面我們一起來看看。
當然,處理上述兩個簡單的新建和刪除操作,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 文件夾下文件情況如下:
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 方法,例如:
它們不再返回目標目錄下的「純文件」和「目錄」的簡單名稱,而返回它們所對應的File 對象,其實也沒什麼,目標目錄+ 簡單名稱即可構建出這些File 實例了。
所以,本質上說,list 方法並不會遍歷出目標目錄下的所有文件,即目標目錄的子目錄中的文件並不會被訪問遍歷。
所以你應當思考如何完成目標目錄下所有文件的遍歷,包含一級子目錄下的深層次文件的遍歷。文末將給出答案。
接下來的兩個方法和文件夾的創建有關:
兩者都是依據的當前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)
大家也可以選擇通過本地下載。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對武林網的支持。