讀古今文學網 > Java程序員修煉之道 > 2.2 文件I/O的基石:Path >

2.2 文件I/O的基石:Path

在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中,PathPaths中的各種方法拋出的受檢異常只有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方法也很有效,它融合了toAbsolutePathnormalize兩個方法的功能,還能檢測並跟隨符號連接。

還是回到*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對目錄處理的支持,特別是對目錄樹。