前言
從本篇文章開始,我們將開啟對 java io 系統的學習,本質上就是對文件的讀寫操作,聽上去簡單,其實并不容易。java 的 io 系統一直在完善和改進,設計了大量的類,也只有理解了這些類型被設計出來的意義以及各自的應用場景,才能提升文件 io 的理解。
那么,第一步就是要解決如何表示一個文件的問題,java 世界中「萬物皆對象」,如何將一個實際磁盤文件或目錄對應到一個 java 對象則是我們首要的問題。
java 中使用 file 來抽象一個文件,無論是普通文件或是目錄,都可對應于一個 file 對象。我覺得大家對于 file 這個類型的定位一定要準確:它只是抽象的代表了磁盤上的某個文件或目錄,內部實際上是依賴一個平臺無關的本地文件系統類,并且 file 無法對其所表示文件內容進行任何讀寫操作(那是流做的事情)。
構建一個 file 實例
在實際介紹 file 實例構造方法之前,我們得先看看它的幾種重要的屬性成員。
1
|
private static final filesystem fs = defaultfilesystem.getfilesystem(); |
這是 file 類中最核心的成員,它表示為當前系統的文件系統 api,所有向磁盤發出的操作都是基于這個屬性的。
1
|
private final string path; |
path 代表了當前實例的完整路徑名稱,如果當前的 file 實例表示的是目錄的話,那么 path 的值就是這個完整的目錄名稱,如果表示的是純文件的話,那么這個 path 的值等于該文件的完整路徑 + 文件名稱。
1
2
|
public static final char separatorchar = fs.getseparator(); public static final char pathseparatorchar = fs.getpathseparator(); |
separatorchar 表示的是目錄間的分隔符,pathseparatorchar 表示的是不同路徑下的分隔符,這兩個值在不同的系統平臺下不盡相同。例如 windows 下這兩者的值分別為:「」 和 「;」,其中封號用于分隔多個不同路徑。
file 類提供了四種不同的構造器用于實例化一個 file 對象,但較為常用的只有三個,我們也著重學習前三個構造器。
1
|
public file(string pathname) |
這是最普遍的實例化一個 file 對象的方法,pathname 的值可以是一個目錄,也可以是一個純文件的名稱。例如:
1
2
3
4
5
|
file file = new file( "c:\\users\\yanga\\desktop" ); file file1 = new file( "c:\\users\\yanga\\desktop\\a.txt" ); file file2 = new file( "a.txt" ); |
當然也可以顯式指定一個父路徑:
1
|
public file(string parent, string child) |
在構造器的內部,程序會為我們拼接出一個完整的文件路徑,例如:
1
2
3
|
file file = new file( "c:\\users\\yanga\\desktop" , "a.txt" ); file file1 = new file( "c:\\users\\yanga\\desktop" , "java" ); |
第三種構造器其實本質上和第二種是一樣的,只不過增加了一個父類 file 實例的封裝過程:
1
|
public file(file parent, string child) |
類似的情況,不再舉例說明了。我們這里并沒有深究這些構造器的內部具體實現情況,并不是說它簡單,而是 file 過度依賴本地文件系統,很多方法的實現情況都不得直接看到,所以對于 file 的學習,定位為熟練掌握即可,具體實現暫時沒法深入學習。
文件名稱或路徑相關信息獲取
getname 方法可以用于獲取文件名稱:
1
2
3
4
5
|
public string getname() { int index = path.lastindexof(separatorchar); if (index < prefixlength) return path.substring(prefixlength); return path.substring(index + 1 ); } |
還記得我們的 separatorchar 表示的是什么了嗎?
它表示為路徑分隔符,windows 中為符號「」,path 屬性存儲的當前 file 實例的完整路徑名稱,所以最后一次出現的位置后面所有的字符必然是我們的文件名稱。
當然你一定發現了,對于純文件來說,該方法能夠返回文件的簡單名稱,而對于一個目錄而言,返回值將會是最近的目錄名。例如:
1
2
3
4
5
|
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()); |
輸出結果不會出乎你的意料:
1
2
|
a.txt desktop |
getparent 方法用于返回當前文件的父級目錄,無論你是純文件或是目錄,你終有你的父目錄(當然,虛擬機生成的臨時文件自然不是)。
1
2
3
4
5
6
7
8
9
|
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 實例的完整文件名稱:
1
2
3
|
public string getpath() { return path; } |
以下是一些有關目錄的相關操作,實現比較簡單,此處簡單羅列了:
- public boolean isabsolute():是否為絕對路徑
- public string getabsolutepath():獲取當前 file 實例的絕對路徑
- public string getcanonicalpath():返回當前 file 實例的標準路徑
這里我們需要對 getcanonicalpath 做一點解釋,什么叫標準路徑,和絕對路徑有區別嗎?
一般而言,「../」表示源文件所在目錄的上一級目錄,「../../」表示源文件所在目錄的上上級目錄,并以此類推。getabsolutepath 方法不會做這種轉換的操作,而 getcanonicalpath 方法則會將這些特殊字符進行識別并取合適的語義。
例如:
1
2
3
|
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 類還提供了所謂「查詢」操作,這個我們要好好學習一下。例如:
1
2
3
4
5
6
7
8
9
10
|
public string[] list() { securitymanager security = system.getsecuritymanager(); if (security != null ) { security.checkread(path); } if (isinvalid()) { return null ; } return fs.list( this ); } |
這個方法會檢索出當前實例所代表的目錄下所有的「純文件」和「目錄」簡單名稱集合。例如:
1
2
3
4
5
|
file file = new file( "c:\\users\\yanga\\desktop" ); string[] list = file.list(); for (string str : list){ system.out.println(str); } |
程序的輸出結果會打印我電腦桌面目錄下所有的文件的簡單名稱,就不給大家看了。
需要注意一點,如果我們的 file 實例對應的不是一個目錄,而是一個純文件,那么 list 將返回 null。
接著,我們再看一個檢索目錄文件的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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 接口的定義卻是如此簡單:
1
2
3
|
public interface filenamefilter { boolean accept(file dir, string name); } |
只需要重寫這個 accept 方法即可,list 的 for 循環每獲取一個文件或目錄就會嘗試著先調用這個過濾方法,如果通過篩選,才會將當前文件的簡單名稱添加進返回集合中。
所以這個 accept 方法的重寫就決定著哪些文件能夠通過篩選,哪些則不能。我們看個例子:
我的桌面上 test 文件夾下文件情況如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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 實例創建文件夾,關于它們的不同點,我們先看一段代碼:
1
2
3
4
5
|
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 方法就是用于解決這種情境的,它會創建目標路徑上所有未創建的目錄,看代碼:
1
2
|
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)
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對服務器之家的支持。
原文鏈接:https://www.cnblogs.com/yangming1996/p/9020069.html