在NIO.2的文件I/O中,Path
是必須掌握的關鍵類之一。Path
通常代表文件系統中的位置,比如C:workspacejava7developer(Windows文件系統中的目錄)或/usr/bin/zip(*nix文件系統中zip程序的位置)。如果你能理解如何創建和處理路徑,就能瀏覽任何類型的文件系統,包括zip歸檔文件系統。
我們通過圖2-1(基於本書中的源碼佈局)來複習一下文件系統中的幾個概念。
- 目錄樹
- 根路徑
- 絕對路徑
- 相對路徑
圖2-1 說明根目錄、絕對路徑和相對路徑概念的目錄樹
之所以要討論絕對路徑和相對路徑,是因為我們需要考慮程序運行的位置。比如說,有個程序可能在/java7developer/src/test目錄下運行,代碼要讀取位於/java7developer/src/main目錄下的文件名。為了進入/java7developer/src/main目錄,可以用相對路徑../main。
但如果程序運行在/java7developer/src/test/java/com/java7developer,用相對路徑 ../main則無法進入目標目錄,而會進到並不存在的目錄/java7developer/src/test/java/com/main中。所以必須使用絕對路徑,比如/java7developer/src/main。
反之亦然,你的程序可能一直運行在同一位置,比如圖2-1中的target目錄。但目錄樹的根目錄可能會變,比如從/java7developer變成了D:workspacej7d。這時就不能依靠絕對路徑,而要用相對路徑轉到你想到達的位置。
NIO.2中的Path
是一個抽像構造。你所創建和處理的Path
可以不馬上綁定到對應的物理位置上。儘管看起來奇怪,但有時確實需要如此。比如說,你想創建一個Path
來表示即將創建的新文件。在調用Files.createFile(Path target)
1之前,這個文件是不存在的。如果在Path
所對應的文件創建之前,你試圖讀取這個文件中的內容,就會導致IOException
。如果你指定了一個並不存在的Path
並試圖用Files.readAllBytes(Path)
之類的方法讀取,結果是一樣的。簡言之,JVM只會把Path
綁定到運行時的物理位置上。
1 你一會兒就能在2.4節見到這個方法!
警告
在編寫與具體文件系統相關的代碼時要小心。創建
Path
為C:workspacejava7developer,然後試圖讀取它的程序,這只能在有C:workspacejava7developer位置的計算機上才能工作。一定要確保程序邏輯和異常處理考慮到了各種情況,包括可能在不同的文件系統上運行,或文件系統的結構可能被改動。本書的作者之一過去就犯過這個錯誤,結果把計算機系的一組硬盤全搞壞了!2
2 我們不會告訴你是誰,不過歡迎你把他查出來!
還是得再重複一遍,NIO.2把位置(由Path
表示)的概念和物理文件系統的處理(比如複製一個文件)分得很清楚,物理文件系統的處理通常是由Files
輔助類實現的。
有關Path
類以及將在本節討論的其他類的進一步細節請見表2-1。
表2-1 學習文件I/O的關鍵基礎類
Path
Path
類中的方法可以用來獲取路徑信息,訪問該路徑中的各元素,將路徑轉換為其他形式,或提取路徑中的一部分。有的方法還可以匹配路徑字串以及移除路徑中的冗余項
Paths
工具類,提供返回一個路徑的輔助方法,比如get(String first, String... more)
和get(URI uri)
FileSystem
與文件系統交互的類,無論是默認的文件系統,還是通過其統一資源標識(URI)獲取的可選文件系統
FileSystems
工具類,提供各種方法,比如其中用於返回默認文件系統的FileSystems.getDefault
記住,Path
不一定代表真實的文件或目錄。你可以隨心所欲地操作Path
,用Files
中的功能來檢查文件是否存在,並對它進行處理。
提示
Path
並不僅限於傳統的文件系統,它也能表示zip或jar這樣的文件系統。
我們來完成幾個簡單的任務,探索一下Path
類:
- 創建一個
Path
; - 獲取
Path
的相關信息; - 移除
Path
中的冗余項; - 轉換一個
Path
; - 合併兩個
Path
,在兩個Path
中間創建一個Path
,並對這兩個Path
進行比較。
我們先從創建表示文件系統中某個位置的Path
開始。
2.2.1 創建一個Path
創建Path
沒什麼難的。調用Paths.get(String first,String...more)
是最快捷的做法,其中第二個變量一般用不到,它僅僅用來把額外的字符串合併起來形成Path
字符串。
提示 在NIO.2的API中,
Path
或Paths
中的各種方法拋出的受檢異常只有IOException
。我們認為這雖然簡單,但有時卻會掩藏潛在的問題,而且如果你想處理IOException
的某個顯式子類,則需要額外編寫異常處理代碼。
我們用Paths.get(String first)
方法為/usr/bin/目錄下的文件壓縮工具zip創建一個絕對Path
。
Path listing=Paths.get(\"/usr/bin/zip\");
調用Paths.get(\"/usr/bin/zip\")
的效果和調用下面這個長一點的代碼效果一樣:
Path listing=FileSystems.getDefault.getPath(\"/usr/bin/zip\");
提示
創建
Path
時可以用相對路徑。比如,運行在/opt目錄下的程序可以用../usr/bin/zip創建一個指向/usr/bin/zip的Path
。其含義是指進入/opt的上一層目錄(即/),然後進入/usr/bin/zip。通過調用toAbsolutePath
方法,很容易把這個相對路徑轉換成絕對路徑,如:listing.toAbsolutePath
。
你可以從Path
中獲取信息,比如其父目錄、文件名(如果有的話)等等。
2.2.2 從Path中獲取信息
Path
類中有一組方法可以返回你正在處理的路徑的相關信息。代碼清單2-1為/usr/bin目錄下的zip工具創建一個Path
,並輸出相關信息,包括它的根目錄和父目錄。如果你的操作系統中zip也放在/usr/bin目錄下,應該能見到下面這種輸出信息。
File Name [zip]
Number of Name Elements in the Path [3]
Parent Path [/usr/bin]
Root of Path [/]
Subpath from Root, 2 elements deep [usr/bin]
你在計算機上看到的結果和你所用的操作系統及程序運行的位置有關。
代碼清單2-1 從Path
中獲取信息
import java.nio.file.Path;
import java.nio.file.Paths;
public class Listing_2_1 {
public static void main(String args) {
Path listing = Paths.get(\"/usr/bin/zip\");//創建絕對路徑
System.out.println(\"File Name [\" +
listing.getFileName + \"]\");//獲取文件名
/*1 獲取名稱元素的數量*/
System.out.println(\"Number of Name Elements
in the Path [\" +
listing.getNameCount + \"]\");
/*2 獲取Path的信息*/
System.out.println(\"Parent Path [\" +
listing.getParent + \"]\");
System.out.println(\"Root of Path [\" +
listing.getRoot + \"]\");
System.out.println(\"Subpath from Root,
2 elements deep [\" +
listing.subpath(0, 2) + \"]\");
}
}
在創建了/usr/bin/zip的Path
之後,你可以看一下組成Path
的元素個數,在此例中就是目錄的數量1。相對其父目錄和根目錄,Path
的位置會更有用。也可以通過指定起始和終止的索引來挑出子路徑。在本例中是從Path
的根(0)到其第二個元素(2)之間的子路徑2。
如果這是你初次接觸NIO.2文件API,這些方法對你來說非常重要,因為你可以借助它們查看路徑處理的結果。
2.2.3 移除冗余項
在編寫工具軟件,比如屬性文件分析器時,需要處理的Path
中可能會有一個或兩個點:
- . 表示當前目錄;
- .. 表示父目錄。
假設你的程序在/java7developer/src/main/java/com/java7developer/chapter2/目錄下運行(見圖2-1)。你所在的目錄和Listing_2_1.java
一樣,如果傳給你的Path
是./Listing_2.1.java
,其中的./
部分,即程序正在運行的目錄,實際上並沒什麼用。在這裡用短一點兒的Path
——Listing_2_1.java
就夠了。
Path
中可能還有其他冗余項,比如符號鏈接(見2.4.3節)。假設在*nix系統中/usr/logs目錄下,你想尋找日誌文件log1.txt,但其實/usr/logs只是一個指向/application/logs的符號鏈接,那裡才是存放日誌文件的真正位置。要得到這個位置,就需要去掉冗余的符號信息。
所有這些冗余項都會導致 Path
指向的不是你認為它應該指向的位置。
在Java 7中,有兩個輔助方法可以用來弄清Path
的真實位置。首先可以用normalize
方法去掉Path
中的冗余信息。下面的代碼會返回Listing_2_1.java的Path
,去掉了表明它在當前目錄(./部分)中的冗余符號。
Path normalizedPath = Paths.get(\"./Listing_2_1.java\").normalize;
此外,toRealPath
方法也很有效,它融合了toAbsolutePath
和normalize
兩個方法的功能,還能檢測並跟隨符號連接。
還是回到*nix系統中的日誌文件那個例子,在/usr/logs目錄下有個日誌文件log1.txt,而這個目錄實際上是指向/application/logs的符號鏈接。通過調用toRealPath
,你能得到表示/application/logs/log1.txt的真正Path
。
Path realPath = Paths.get(\"/usr/logs/log1.txt\").toRealPath;
我們要討論的最後一個Path API特性是比較多個Path
,並找出它們之間的Path
。
2.2.4 轉換Path
轉換路徑最多的是工具軟件。比如你可能需要比較文件之間的關係,以便瞭解源碼目錄樹的結構是否符合編碼規範。或者在shell腳本中執行程序時可能會傳入一些Path
參數,並需要把這些參數變成有意義的Path
。在NIO.2里可以很容易地合併Path
,在兩個Path
中再創建Path
或對Path
進行比較。
下面的代碼將兩個Path
合併,通過調用resolve
方法,將uat和conf/application.properties合併成表示/uat/conf/application.properties的完整Path
。
Path prefix = Paths.get(\"/uat/\");
Path completePath = prefix.resolve(\"conf/application.properties\");
要取得兩個Path
之間的路徑,可以用relativize(Path)
方法。下面的代碼計算了從日誌目錄到配置目錄之間的路徑。
String logging = args[0];
String configuration = args[1];
Path logDir = Paths.get(logging);
Path confDir = Paths.get(configuration);
Path pathToConfDir = logDir.relativize(confDir);
如你所願,你可以用startsWith(Path prefix)
、equals(Path path)
等值比較或endsWith(Path suffix)
來對路徑進行比較。
現在使用Path
類對你來說應該沒有問題了,但Java 7之前版本的那些代碼怎麼辦呢?負責NIO.2的團隊考慮到了向後兼容性,所以加了兩個新的API特性來保證基於Path
的新I/O和老版本代碼之間的互操作性。
2.2.5 NIO.2 Path和Java已有的File類
新API中的類可以完全替代過去基於java.io.File
的API。但你不可避免地要和大量遺留下來的I/O代碼交互。Java 7提供了兩個新方法:
java.io.File
類中新增了toPath
方法,它可以馬上把已有的File
轉化為新的Path
。Path
類中有個toFile
方法,它可以馬上把已有的Path
轉化為File
。
下面的代碼演示了這一功能。
File file = new File(\"../Listing_2_1.java\");
Path listing = file.toPath;
listing.toAbsolutePath;
file = listing.toFile;
現在,我們完成了對Path
類的探索。接著要研究一下Java 7對目錄處理的支持,特別是對目錄樹。