詳解Java 7中新的文件API

新文件包的用途

Java 7向語言中引入了一些有用的特性,其中包括一個新的I/O文件包。相對於老的java.io包,這個包針對文件系統——特別是基於POSIX的系統——提供了粒度更細的控制功能。本文首先介紹一下新的API,之後通過一個基於Web的文件管理器項目WebFolder來詳細探索這些API。該項目提供了一種管理遠程計算機上文件系統的機制。它支持文件系統的遍歷以及文件的查看、重命名、複製和刪除等操作。我們可以利用新的I/O文件包擴展該項目,使之能夠操作ZIP文件的內容,並能監視修改操作。WebFolder可以免費從http://webfolder.sf.net下載。

儘管基本文件操作API在不同版本之間的確也有些更新,但Java團隊決定爲Java 7提供一個新設計的替代包,以一種新的方式來涵蓋文件系統操作。

基本文件操作API仍然位於java.nio.file包及其兩個子包java.nio.file.attribute和java.nio.file.spi中。新API把文件相關的操作從java.io包中分離出來,而且爲使文件系統的管理更爲直觀,還提供了一些額外的方法。概念上,新API構建爲一組實體接口和操作類,其中實體接口包含的是一個文件系統中的基本對象,而操作類包含的是文件系統自身之上的操作。這一理念從java.util包繼承而來,在java.util包中,像Collections和Arrays等類提供了很多操作,分別用於集合和數組等基本聚合數據結構。爲避免混淆,尤其是要避免java.io包和java.nio.file一起使用時出現問題,新包中的基類和接口採取了不同的命名方法。

新包不僅重新組織了支持文件和文件系統操作的類,還擴展了API的功能,比如提供了更爲簡單的文件複製和移動方式。

常規文件操作與新文件操作相關類之對比

下表是這些包中基類和接口的簡單概述:

Java < 7 java.io,javax.swing.filechooser Java >= 7 java.nio.file 註釋
File Path和Files File類同時提供了文件位置和文件系統操作,而新API將其分爲兩部分。Path提供的只是一個文件位置,還支持與路徑相關的額外操作;Files支持文件操作,還提供了很多File類中沒有的新功能,比如複製或讀取整個文件的內容,或者設置文件屬主。
FileSystemView FileSystem FileSystemView類提供了底層文件系統的一個視圖,僅用於Swing文件選擇器的上下文中。FileSystem類可以表示定義於本地、遠程或其他可選存儲機制(如ISO映像或ZIP歸檔)之上的不同文件系統。FileSystem類包含了一些工廠,用於提供如Path等不同接口的具體實現。
沒有類似的類 FileStore 表示文件存儲相關的某些屬性,如文件大小。可以從一個特定的Path或FileSystem類重新獲取。

除了對象和操作的組織方式不同之外,新文件系統API能夠在大多數方法和構造器中利用相當新的Java特性,如自動裝箱(autoboxing),因而新API用起來更整潔,也更容易。
下面幾部分我們會更詳細地看一下特定改進。

文件系統遍歷與分組操作

新文件包引入了一種新的文件系統遍歷方法,相比於之前基於數組和過濾器的版本,內存使用效率有所改進。此外,新方法也使深度遍歷文件系統成爲可能。新的實現使用了訪問者設計模式。儘管可以模仿訪問者模式,使用支持普通文件的過濾器來執行遍歷操作,但要提供簡單且內存高效的多層遍歷算法會困難得多。

訪問者模式是作爲FileVisitor接口引入的。因爲這是個泛型接口,你可能會認爲可以使用基於File的實現來遍歷文件系統,然而新I/O文件只支持實現了Path接口的對象。該接口聲明瞭四個方法,SimpleFileVisitor類是該接口的一個實現,開發者可以繼承這個類,這樣在給定情況下只需實現所需的任何方法即可。下表簡要概述了FileVisitor的各個方法以及它們在SimpleFileVisitor類中的行爲:

方法名 用途 默認情況
visitFile 除非定義了過濾控制,否則會在遍歷的每個普通文件(包括符號鏈接)上調用該方法。任何有意義的文件相關操作都可以在此處理,比如備份文件或查找文件內容。也可以在這裏決定遍歷是繼續還是停止。該方法不會在目錄上調用。 返回CONTINUE
preVisitDirectory 如果訪問的項是目錄而非文件,調用的將是該方法而非visitFile。它支持跳過特定目錄,也支持爲複製操作在目標位置創建相應的目錄。 返回CONTINUE
postVisitDirectory 該方法在整個目錄的遍歷已經完成時調用,可以方便地結束目錄上的操作。比如,如果遍歷的目的是刪除所有文件,那麼目錄本身可以在該方法中刪除。 返回CONTINUE
visitFileFailed 如果在文件系統遍歷過程中出現任何未處理的異常,則會調用該方法。如果異常被重新拋出,那麼所有遍歷都將停止,而且異常會被傳播到使用Files.walkFileTree啓動文件系統遍歷的代碼處。可以在這裏分析異常並決定是否繼續遍歷。 重新拋出IOException

正如你所看到的,該接口非常強大,支持文件系統上的大部分習慣操作,包括歸檔、搜索、備份和刪除文件。其異常處理也非常靈活。然而,如果只是需要獲取某個目錄的內容而無需深度遍歷,使用老式的File.list()操作就很方便,新IO文件中也有一個類似的功能,不過返回的是一個集合而非純數組。

java.io包中沒有的新特性

儘管新IO文件提供的文件系統遍歷和分組操作確實非常有用,但標準的java.io包也支持這些操作。不過新IO文件提供了舊包所沒有的特定於操作系統的功能。對鏈接和符號鏈接的支持就是一個重要例子,現在它們可以在任何文件系統遍歷操作中創建或處理。當然,只有在支持鏈接和符號鏈接的文件系統中才能工作,否則會拋出UnsupportedOperationException。另一個擴展是能夠管理文件屬性,如屬主和權限。重複一下,如果底層文件系統不支持,會拋出IOException或UnsupportedOperationException。下表是對鏈接和擴展文件屬性相關操作的簡要概述。所有這些操作都可以從Files類調用。

操作 用途 註釋
createLink 創建映射到某個文件的硬連接  
createSymbolicLink 創建映射到文件或目錄的符號鏈接  
getFileAttributeView 以特定於文件系統實現的FileAttributeView形式訪問屬性 雖然該方法帶來了提供一組預定義屬性集的靈活性,但使用的仍是具體實現類,因此限制了代碼的可移植性
getOwner 獲得文件屬主 只能用於支持屬主屬性的文件系統
getPosixFilePermissions 獲得文件權限 特定於POSIX系統
isSymbolicLink 判斷給定路徑是否爲符號鏈接 特定文件系統
readSymbolicLink 讀取符號鏈接的目標路徑 特定文件系統
readAttributes 讀取文件屬性 該方法有兩個以不同形式返回屬性的變體
setAttribute 設置文件屬性 屬性名可能包含FileAttributeView限定詞

如果打算使用表中列出的操作,請參考新IO文件的文檔。

監視

該API也提供了一種監視機制,因此可以針對事件(如創建、修改和刪除)監視特定文件或目錄的狀態。遺憾的是,該API並不保證爲監視事件採用推送模型,而且大部分情況下會使用輪詢機制,在我看來,這降低了實現的吸引力。監視服務也依賴於系統,所以無法利用這種服務構建真正可移植的應用。有5個接口涵蓋了該功能。下表是這些接口及其用法的簡要概述。

接口 用途 用法
Watchable 這種類型的對象可以註冊到監視服務中。註冊後得到的WatchKey可用於監控事件修改。 必須通過該接口的某個具體實現來註冊感興趣的與對象關聯的監視事件。請注意,Path也擴展了Watchable接口。
WatchService 文件系統中用於註冊Watchable對象的服務,使用WatchKey來監控修改。 WatchService可以從FileSytem對象獲得。
WatchKey 監視鍵是註冊所得到的憑據,用於查詢修改事件。 該對象可以保存下來,之後用於查詢修改事件。當存在相關修改事件時,可以直接從WatchService獲得WatchKey對象。
WatchEvent 攜帶監視事件。 WatchEvent對象會被傳給事件通知調用,可以從中獲得事件種類和受影響對象的路徑。
WatchEvent.Kind 攜帶監視事件的種類信息。 用於在註冊Watchable對象時指定感興趣的特定事件類型。在通知調用的WatchEvent中也有提供。

這裏強調兩個可能會使用監視服務的場景。一個是,只需要監控特定對象的修改時。在這種情況下,Watchable對象可以註冊到監視服務中並獲得監視鍵,監視鍵用於輪詢修改事件。針對監視鍵的輪詢機制不是阻塞的,因此即使未出現新事件,輪詢仍然會獲得一個空列表。爲減輕輪詢的負載,可以在兩次輪詢間引入一個延遲;作爲代價,這會丟失一些通知事件發生時的精度。第二個場景利用了監視服務的監視機制,適於輪詢與多個被監視對象相關的修改事件。和第一個場景一樣,需要註冊所有的Watchable對象,不過可以忽略返回的監視鍵。這裏沒有使用監視鍵的輪詢機制,而是使用服務輪詢機制來檢索與所激發修改事件相關的監視鍵,然後使用針對監視鍵的輪詢操作來處理事件。在這種情況下,監視鍵應保證指定了某些事件。可以使用一個線程來管理所有的監視鍵。監視服務的輪詢機制更爲靈活,因爲它支持阻塞(blocking)、非阻塞(non-blocking)和帶超時的阻塞(blocking with timeout)等操作。所以也能夠更精確。後面我們會看一個有關第二個場景的例子,前面提到的WebFolder項目用到了它。

工具操作

新I/O文件的下一個主要特性是一組工具方法。這組方法使新包成爲自給自足的,因爲大部分使用情況下都不需要調用標準java.io包中的功能。輸入流、輸出流和字節通道都可以直接使用Files類的方法獲得。該API支持完整的操作,如複製或移動文件。此外,整個文件的內容可被當作字符串列表或字節數組讀出。不過需要注意的是,因爲沒有大小控制參數,所以爲避免可能出現的內存問題,必須添加獲取文件大小的操作。

新I/O文件組織的更多信息

最後,文件系統與存儲是新I/O文件包的主要部分。正如我們所看到的,文件位置是通過Path接口表示的,這是該包的關鍵要素。開發者需要利用FileSystem工廠獲得該接口的具體實現,而文件系統工廠又必須通過FileSystems工廠獲得。下表顯示了新I/O關鍵要素之間的關係。

存儲信息可以從文件系統上的特定文件(Path)獲得。

使用文件系統

所有文件系統實現都由相應提供者負責支持,實現的基類定義在java.nio.file.spi包中。服務提供者的概念使開發者能夠輕鬆地擴展到更多文件系統。有些有趣的文件系統提供者是包裝過的,比如有的會變換ZIP文件的內容,支持如內容的遍歷和文件的創建、刪除及修改等功能。後面我們會看一個例子。

併發與原子操作

如果不提一下新IO文件對併發的支持,概述將是不完整的。新IO文件高度支持併發,因此大部分操作在併發環境中是安全的。移動文件也是原子的。通過獲取SecureDirectoryStream接口的具體實現來操作目錄內容也是安全的。在這種情況下,即使目錄被外部攻擊者移動或修改,所有目錄相關操作仍然具有一致性。這裏只接收相對路徑。

實例

學習新東西最好的方法就是動手編程。上面提到的基於Web的文件管理器WebFolder最初是用java.io包開發的,因此我決定使用新IO文件來遷移一下該項目。如此將有助於更好地理解I/O文件中的概念,而且相對於其他更嚴肅的項目,我可以用特定應用來評估新的API。這裏我有意讓示例代碼小一些,完整的源代碼可以從項目網站下載。

1.獲取一個目錄下的內容

try (RequestTransalated rt = translateReq(getConfigValue("TOPFOLDER", File.separator),
req.getPathInfo());

	DirectoryStream<Path> stream = Files.newDirectoryStream(rt.transPath);) {

             for (Path entry : stream) {

                    result.add(new Webfile(entry, rt.reqPath)); // 添加目錄
element info in model

             }

} catch (Exception ioe) {

	log("", ioe);

}  // 因爲API支持AutoCloseable和新的try塊語法,所以這裏沒有finally塊

這個例子填充了一個將由頁面視圖繪製的目錄模型。Files.newDirectoryStream用於獲取目錄內容的迭代器。

2. 深度遍歷

Path ffrom = ….

Files.walkFileTree(ffrom, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,

        new SimpleFileVisitor<Path>() {

            @Override

            public FileVisitResult preVisitDirectory(Path dir,

BasicFileAttributes attrs)

                    throws IOException {

                Path targetdir =
fto.resolve(fto.getFileSystem().getPath(ffrom.relativize(dir).toString()));

                try {

                    Files.copy(dir, targetdir,
StandardCopyOption.COPY_ATTRIBUTES);

                } catch (FileAlreadyExistsException e) {

                    if (!Files.isDirectory(targetdir))

                        throw e;

                }

                return FileVisitResult.CONTINUE;

            }

            @Override

            public FileVisitResult visitFile(Path file, BasicFileAttributes
attrs) throws IOException {

                Path targetfile = fto.resolve(fto.getFileSystem()

       .getPath(ffrom.relativize(file).toString()));

                Files.copy(file, targetfile,
StandardCopyOption.COPY_ATTRIBUTES);

                return FileVisitResult.CONTINUE;

        }

});

這段代碼將文件系統上一個目錄的內容複製到另一個位置。preVisitDirectory負責複製目錄本身。因爲目標可以是另一個文件系統,該例子既可以方便地在保存目錄結構的同時提取ZIP歸檔文件的全部內容,也可以方便地將目錄結構存入ZIP歸檔文件中。COPY_ATTRIBUTES選項會把源文件的所有屬性(包括時間戳)保存到目標文件中。

類似實現可用於刪除一個目錄的所有內容,在這種情況下必須實現postVisitDirectory方法,而不是preVisitDirectory,因爲刪除內容之後才能刪除目錄本身。

@Override

public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {

    if (e == null) {        

        if (dir.getParent() != null) {

            Files.delete(dir);

            return FileVisitResult.CONTINUE;

        } else

            return FileVisitResult.TERMINATE;

    } else {

        // 目錄迭代失敗

        throw e;

    }

}

該例子在刪除前會檢查以確保目標並非根目錄。所有可能的異常都會向上傳播,由某個調用者處理。

3.來自ZIP的文件系統

FileSystem fs = FileSystems.newFileSystem(zipPath, null);

Path zipRootPath = fs.getPath(fs.getSeparator());

….

Fs.close();

zipRootPath可以隨意遍歷ZIP文件的內容。所得的文件系統功能全面,支持大部分操作(包括複製、移動和刪除)。不過ZIP文件系統不能使用監視服務。還請注意,該文件系統用完後必須關閉。如果要在同一個ZIP上打開另一個文件系統,操作會失敗,因此編寫代碼時請將這種可能性牢記在心。然而默認的文件系統無需關閉。看起來新I/O文件包只維護了一個文件系統實例,並負責處理併發。

4.監視

監視服務有多種使用方法,這裏會說明兩種最常見的,前面也有所提及。

WatchService ws = dir.getFileSystem().newWatchService();

WatchKey wk = dir.register(ws,  StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);

獲得監視鍵之後,可以將其傳給監視線程來監控相關事件。

@Override

public void run() {

    for (;;) {

        if (watchKey != null) {

            for (WatchEvent<?> event : watchKey.pollEvents()) {

                updateScreen(event.kind(), event.context());

        }

        boolean valid = watchKey.reset();

        if (!valid) {

            break;

        }

    }

}

如果事件消耗速度不夠快,則會收到OVERFLOW事件。如果對監視鍵的事件沒有興趣了,可以取消。監視服務也可以用完後關閉。還有一種方法是,在註冊了多個被監視對象的情況下,使用監視服務方法來輪詢修改事件。這種方法更適合WebFolder應用。

public void run() {

    for (;;) {

        try {

            WatchKey watchKey = watchService.take(); // poll(10, );

            processWatchKey(watchKey);

        } catch (InterruptedException ie) {

            break;

        }

    }

}

一個監視線程是爲默認文件系統獲取的,之後會用在一個單一的監控線程中。這裏使用了take操作,因爲它是阻塞的,所以不會浪費循環。爲支持輪詢,processWatchKey方法的實現與上面類似,也關聯了監視事件。不過,這裏不需要額外的循環,因爲從監視服務獲得的鍵已經與事件關聯了起來。

概括

新I/O文件提供的內容包括:

  1. 強大的文件系統遍歷機制,可以進行復雜的分組操作。
  2. 可以操作具體的文件、文件系統對象及其屬性(如鏈接、屬主和權限)。
  3. 用於處理整個文件內容的便捷的工具方法,如讀取、複製和移動等。
  4. 用於監控文件系統修改的監視服務。
  5. 文件系統上的原子操作,提供了針對文件系統的進程同步。
  6. 可以定製定義於特定文件組織形式(如歸檔文件)之上的文件系統。

遷移

之所以考慮將基於老式I/O包的系統遷移到新I/O包上,有如下四個原因:

  • 用到複雜的文件遍歷實現時,會發現內存問題
  • 需要支持ZIP歸檔文件中的文件操作
  • 需要細粒度地控制POSIX系統中的文件屬性
  • 需要監視服務

根據經驗,如果有兩項或兩項以上適用於項目,遷移就是值得的,否則我建議仍使用當前實現。一個不遷移的理由是,新I/O文件實現並不能使代碼更緊湊、可讀性更好。另一方面,在第一次訪問特定的運行時實現時,新的文件遍歷操作性能可能稍顯不好。看起來Oracle在Windows上的實現做了很多緩存,致使第一次訪問消耗的時間比較顯著。然而Linux上的OpenJDK(IcedTea)實現就沒有這種問題,所以該問題似乎依賴於具體的平臺/實現。

如果決定遷移,下表提供了一些技巧:

當前實現 遷移後 註釋
fileObj = new File(new File(pe1, pe2), pe3) pathObj = fsObj. getPath(pe1, pe2, pe3) fsObj可以作爲FileSystems.getDefault()的結果獲得,因爲文件系統保存在Path本身之中,所以該對象可以從來自同一文件系統的任何現有路徑獲得
fileObj.someOperation() Files.someOperation(pathObj) 儘管可以添加一些與鏈接和屬性相關的額外參數,但大部分情況下操作名是相同的
fileObj.listFiles() Files.newDirectoryStream(pathObj) Files.walkFileTree應該用於深度遍歷
new FileInputStrean(file) Files.newInputStream(pathObj) 可以指定如何打開文件的額外選項
new FileOutputStream(file) Files.newOutputStream(pathObj) 可以指定如何打開文件的額外選項
new FileWriter(file) Files.newBufferedWriter(pathObj) 可以指定如何打開文件的額外選項
new FileReader(file) Files.newBufferedReader(pathObj) 可以指定如何打開文件的額外選項
new RandomAccessFile(file) Files.newByteChannel(pathObj) 可以指定打開選項和文件創建屬性

File類和Path接口之間有兩種轉換方法:pathObj.toFile()和fileObj.toPath()。這有助於減少遷移所需的努力,人們得以將精力集中在新I/O文件提供的新功能上。作爲遷移過程的一部分,可以考慮用Files.copy替換定製的文件複製方式。Path接口本身提供了很多便利方法,可以減少以前基於File對象編碼時的代碼量。因爲新代碼將運行於Java 7或更高版本之上,改進異常處理和資源釋放是值得的。下面代碼說明了舊的機制和新的機制:

ClosableResource resource = null;

try {

     Resource = new Resource(…);

// 資源處理

} catch(Exception e) {

} finally {

    if (resource != null)

        try {

               resource.close();

        }  catch(Exception e) {

        }

}

可以替換爲下面更爲緊湊的代碼:

try (Resource = new Resource(…);) {

     // 資源處理

 

} catch(Exception e) {

}

Resource必須實現AutoCloseable接口,所有來自JRT的標準資源都實現了該接口。

關於作者

Dmitriy Rogatkin是WikiOrgCharts公司的CTO,負責把握公司的技術方向。他之前主要從事企業級軟件開發相關的技術:他在MetricStream這家領先的企業級GRC軟件公司擔任了十多年首席架構師。他喜歡通過創建開源軟件(從多媒體桌面應用到框架,再到應用服務器)來檢驗不同的想法。在他的諸多項目當中,TJWS是一個微型應用服務器,在完整的Java EE Profile應用服務器耗費太高時,可以將TJWS作爲一種選擇;而TravelsPal可以幫助人們在旅行和規劃時間時聯繫彼此。
查看英文原文:A Detailed Look at The New File API in Java 7

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章