1. 程式人生 > >瘋狂Java學習筆記(76)------------NIO.2第二篇

瘋狂Java學習筆記(76)------------NIO.2第二篇

在該系列的上一篇中我演示了NIO.2的三個方法:檔案拷貝、檔案和目錄的刪除和檔案移動。在這篇文章中,我將向大家展示路徑相關的方法(如獲取路徑、檢索路徑資訊)、檔案和目錄測試方法(如檔案或目錄的存在性測試)以及面向屬性的方法。

獲取路徑

問:怎樣獲得一個 java.nio.file.Path 物件?

答:你可以呼叫 java.nio.file.Paths 類的下列任何一靜態個方法,通過檔案或目錄在檔案系統中的名稱來獲得一個Path物件:

  • Path get(String first, String… more):將一個路徑字串或者可以連線成路徑字串的字元序列轉換成一個Path物件。如果該字串包含無效的字元或者該路徑字串對特定檔案系統是無效的,則該字串不能轉換為一個Path物件而會丟擲 java.nio.file.InvalidPathException 異常。也可以通過呼叫 java.nio.file.FileSystem 例項的預設 Path getPath(String first, String… more) 方法來獲取Path物件。
  • Path get(URI uri):將統一資源標示符(URI)轉換為Path物件。當該uri不符合uri的規範時(例如:uri必須包含一個schema),此方法會丟擲 java.lang.IllegalArgumentException 的異常。當uri標示符在檔案系統中不存在或不能建立,或者是uri的schema沒有安裝,該方法將會丟擲 java.nio.file.FileSystemNotFoundException 異常。如果安裝的任何安全管理器不允許未知訪問該檔案系統,將丟擲 java.lang.SecurityException。也可以遍歷查詢已安裝的 java.nio.file.spi.FileSystemProvider 來獲取的provider例項,然後呼叫provider的 Path getPath(URI uri) 方法來獲取Path物件。

第一個 get() 方法可以通過任意數量的字元組成一個路徑,例如,Path path = Paths.get(“a”, “b”); 在將字元轉換為字串的時候,該方法將使用適合特定檔案系統的檔案分隔符來分隔元素。例如,在Windows平臺上執行 System.out.println(path); 方法,結果輸出為 ab。

你也可以在元素之間硬編碼一個分隔符,如Path path = Paths.get(“a”, “”, “b”);。但是,這不是一個好的想法。在Windows平臺上,最終會獲得ab.但是,在一個以冒號為檔案分隔符的平臺上,你將很有可能會觀察到一個 InvalidPathException 異常,指出斜槓是一個非法的字元。

你可能想使用 File.separator 來替代硬編碼一個檔案分隔符。但是,這也不是一個好的想法。在NIO.2 中支援多種檔案系統,但是 File.separator 指的是預設的檔案系統。嘗試通過一個包含 File.separator 路徑來獲取Path物件,如果該檔案系統環境下,不能識別該常量分割符將丟擲 InvalidPathException 異常。

我們應該使用 FileSystem 的 String getSeparator() 的方法來代替硬編碼一個分割符或者指定File.separator。getSeparator() 方法返回一個FileSystem 例項呼叫關聯的檔案系統的檔案分隔符。例如,FileSystem.getDefault().getSeparator() 返回預設檔案系統的分隔符。

在該系統的第一部分我簡單的演示了 get() 方法的使用。在列表1中展示了另外一個小應用的原始碼,在這個應用中,我強制使用了上述的檔案分隔符。

import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;

public class ObtainPath
{
   public static void main(String[] args)
   {
      Path path = Paths.get("a", "b");
      System.out.println(path);

      path = Paths.get(FileSystems.getDefault().getSeparator() + "a", "b", "c");
      System.out.println(path);

      path = Paths.get("a", ":", "b");
      System.out.println(path);
   }
}

列表1、ObtainPath.java(版本1)

在上述程式碼中的第二個例子中,該檔案分隔符被作為根目錄使用,但是存在一種更好的方式來獲取該目錄,我將在此文的後面演示。

編譯列表1 (javac ObtainPath.java) 並執行該應用(java ObtainPath)。在Windows平臺上,我看到的結果如下:

ab
abc
Exception in thread "main" java.nio.file.InvalidPathException: Illegal char <:> at index 2: a:b
    at sun.nio.fs.WindowsPathParser.normalize(WindowsPathParser.java:182)
    at sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:153)
    at sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:77)
    at sun.nio.fs.WindowsPath.parse(WindowsPath.java:94)
    at sun.nio.fs.WindowsFileSystem.getPath(WindowsFileSystem.java:255)
    at java.nio.file.Paths.get(Paths.java:84)
    at ObtainPath.main(ObtainPath.java:15)

順便提一下,如果將null做為路徑元素傳 get() 方法,如path = Paths.get(“a”, null, “b”),將會出現什麼現象?

第二個 get() 方法是從一個 URI 轉換為path,列表2的一個小應用程式演示了使用該方法時的兩個問題。

import java.net.URI;
import java.net.URISyntaxException;

import java.nio.file.FileSystemNotFoundException;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;

public class ObtainPath
{
   public static void main(String[] args) throws URISyntaxException
   {
      try
      {
         Path path = Paths.get(new URI(""));
         System.out.println(path);
      }
      catch (IllegalArgumentException iae)
      {
         System.err.println("You cannot pass a URI object initialized to the " +
                            "empty string to get().");
         iae.printStackTrace();
      }

      try
      {
         Path path = Paths.get(new URI("nntp://x"));
         System.out.println(path);
      }
      catch (FileSystemNotFoundException fsnfe)
      {
         System.err.println("No file system associated with the specified scheme.");
         fsnfe.printStackTrace();
      }
   }
}

列表2、ObtainPath.java(版本2)

編譯列表2並執行該應用程式。你就能看到如下的異常輸出:

You cannot pass a URI object initialized to the empty string to get().
java.lang.IllegalArgumentException: Missing scheme
    at java.nio.file.Paths.get(Paths.java:134)
    at ObtainPath.main(ObtainPath.java:15)
No file system associated with the specified scheme.
java.nio.file.FileSystemNotFoundException: Provider "nntp" not installed
    at java.nio.file.Paths.get(Paths.java:147)
    at ObtainPath.main(ObtainPath.java:27)

第一個例子演示了因缺失scheme的URI而出現的 IllegalArgumentException 異常。第二個例子演示了因為沒有nntp scheme的提供者而出現的 FileSystemNotFoundException 的異常。

路徑資訊檢索

問:我能從Path物件中檢索到什麼資訊呢?

答:Path介面聲明瞭幾個方法。通過Path物件,可以檢索到單個的名稱元素和其他種類的資訊,下面列出了這些方法的描述:

  • Path getFileName():返回由該檔案或目錄對應的名稱(頂級的父元素是檔案繼承層級的根目錄),該path物件是由該檔案或目錄的路徑生成的。如果該path物件沒有元素則返回空。
  • Path getName(int index):根據索引返回元素名稱,索引的範圍是從0(離跟目錄最近的元素)到 getNameCount() – 1。如果index為負值或者index大於等於路徑中元素的個數或者該路徑沒有元素,將丟擲 IllegalArgumentException 異常。
  • int getNameCount():返回路徑中元素的個數,如果path物件僅代表根目錄,則返回0。
  • Path getParent():返回父路徑,如果沒有父路徑,則返回空。父路徑是由路徑的根目錄和除去自身外的其他所有元素組成。
  • Path getRoot():返回代表根目錄的path物件或者如果不存在根元素的話則返回null。
  • boolean isAbsolute():如果是絕對路徑(完整,不需要跟其他路徑資訊組合就能定位檔案)返回true,否則返回false。
  • Path subpath(int beginIndex, int endIndex):返回一個由原路徑的子序列名稱組成的相對路徑。名稱元素的範圍 beginIndex 需要小於endIndex。當 beginIndex 的值為負數或者大於等於元素的數量時,或者是 endIndex 小於 beginIndex 或 endIndex 大於等於元素的個數時,將丟擲 IllegalArgumentException 的異常。
  • String toString():返回代表路徑的字串。分隔符使用是當前 FileSystem 物件呼叫 getSeparator() 方法返回的字元。

列表3展示了一個小應用程式的原始碼,該應用程式演示了這些路徑資訊檢索的方法。該應用通過合適的方式增加了根目錄。

import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;

public class RetrievePathInfo
{
   public static void main(String[] args)
   {
      dumpPathInfo(Paths.get("a", "b", "c"));

      FileSystem fs = FileSystems.getDefault();
      Iterable<Path> roots = fs.getRootDirectories();
      for(Path root: roots)
      {
         dumpPathInfo(Paths.get(root.toString(), "a", "b", "c"));
         break;
      }
   }

   static void dumpPathInfo(Path path)
   {
      System.out.printf("Path: %s%n", path);
      System.out.printf("Filename: %s%n", path.getFileName());
      System.out.println("Components");
      for (int i = 0; i < path.getNameCount(); i++)
         System.out.printf("   %s%n", path.getName(i));
      System.out.printf("Parent: %s%n", path.getParent());
      System.out.printf("Root: %s%n", path.getRoot());
      System.out.printf("Absolute: %b%n", path.isAbsolute());
      System.out.printf("Subpath [0, 2): %s%n%n", path.subpath(0, 2));   
   }
}

列表3、RetrievePathInfo.java

列表3建立了兩個路徑,一個是相對路徑、一個絕對路徑。然後,呼叫之前列出來的方法顯示不同種類的資訊。在第二個路徑中,呼叫 FileSystem 的 Iterable<Path> getRootDirectories() 方法來檢索檔案系統預設的根目錄。第一個根目錄被預先新增到路徑中用來顯示路徑資訊。

編譯列表3(javac RetrievePathInfo.java)然後執行該應用(java RetrievePathInfo)。在Windows平臺下,我看的輸出結果如下:

Path: abc
Filename: c
Components
   a
   b
   c
Parent: ab
Root: null
Absolute: false
Subpath [0, 2): ab

Path: C:abc
Filename: c
Components
   a
   b
   c
Parent: C:ab
Root: C:
Absolute: true
Subpath [0, 2): ab

注意:如果預置了根目錄,isAbsolute() 將返回true。如果只新增一個檔案分隔符,如 Paths.get(FileSystems.getDefault().getSeparator() + "a", "b", "c")。在Windows平臺下,isAbsolute() 將返回false。在Windows平臺下一個絕對路徑必須包含驅動說明符,如C:.

更多關於路徑的操作

問:我還能對Path物件做哪些操作?

答:Path介面提供移除冗餘的路徑,從另個路徑中建立相對路徑,連線兩個路徑等等操作。前三種操作可以通過下列方法實現:

Path normalize()
Path relativize(Path other)
Path resolve(Path other) (and the Path resolve(String other) variant)
  • normalize() 是一個非常有用的移除路徑中冗餘資訊的方法。例如,reports/./2015/jan 中包含冗餘資訊(當前目錄)元素。通過規範化,該路徑變得更短:reports/2015/jan。
  • relativize() 方法從兩個路徑中建立一個相對路徑。如當前目錄jan在 reports/2015/jan 中,相對路徑可以通過 ../../2016/mar 導航到 reports/2016/mar。
  • resolve() 方法與 relativize() 方法相反。該方法是連線路徑的一部分(沒有根目錄元素的路徑)到另一個路徑上。如連線apr到 reports/2015上,結果是 reports/2015/apr。

列表4演示這些方法(為了方便,我在兩個地方硬編碼,這應該通過獲取根目錄來代替的)

import java.nio.file.Path;
import java.nio.file.Paths;

public class MorePathOp
{
   public static void main(String[] args)
   {
      Path path1 = Paths.get("reports", ".", "2015", "jan");
      System.out.println(path1);
      System.out.println(path1.normalize());
      path1 = Paths.get("reports", "2015", "..", "jan");
      System.out.println(path1.normalize());
      System.out.println();

      path1 = Paths.get("reports", "2015", "jan");
      System.out.println(path1);
      System.out.println(path1.relativize(Paths.get("reports", "2016", "mar")));
      try
      {
         System.out.println(path1.relativize(Paths.get("/reports", "2016", 
                                                       "mar")));
      }
      catch (IllegalArgumentException iae)
      {
         iae.printStackTrace();
      }
      System.out.println();

      path1 = Paths.get("reports", "2015");
      System.out.println(path1);
      System.out.println(path1.resolve("apr"));
      System.out.println(path1.resolve("/apr"));
   }
}

列表4、MorePathOp.java

列表4的 main() 方法首先演示了 normalize() 處理當前目錄,然後演示了父目錄(..)。結果是 reports/jan。

接下來,main() 演示了之前 relativize() 的例子。然後演示了不能通過絕對路徑來獲取相對路徑,如果嘗試這麼做的,將丟擲IllegalArgumentException的異常。

最後,main() 方法演示了先前 resolve() 的例子,然後展示通過使用一個絕對路徑來對比產生的結果。

你可以使用Path物件的其他方法,例如,通過 Path toAbsolutePath() 方法來將一個路徑轉換為絕對路徑,通過 boolean equals(Object other) 方法來比較兩個路徑是否相同。

測試檔案的可訪問性

問:怎麼知道一個檔案是可執行的、可讀的或可寫的呢?

答:File類提供下列的靜態方法來獲取檔案的可執行性、可讀性和可寫性:

  • boolean isExecutable(Path path):檢測路徑代表的檔案是否存在以及虛擬機器是否擁有可執行的許可權。如果檔案存在並且是可執行的,則返回true。如果檔案不存在,或者虛擬機器沒有執行的許可權或不能確認訪問的許可權,則返回false。
  • boolean isReadable(Path path):檢測路徑代表的檔案是否存在以及虛擬機器是否擁有可讀的許可權。如果檔案存在並且是可讀的,則返回true。如果檔案不存在,或者虛擬機器沒有可讀許可權或不能確認訪問的許可權,則返回false。
  • boolean isWritable(Path path):檢測路徑代表的檔案是否存在以及虛擬機器是否擁有可寫的許可權。如果檔案存在並且是可修改,則返回true。如果檔案不存在,或者虛擬機器沒有寫的許可權或不能確認訪問的許可權,則返回false。

通常在執行、讀或寫一個檔案時,都需要呼叫其中的一個方法來判斷。例如,在嘗試寫一個檔案時,你可能需要先檢測一個檔案是否是可寫的,如:

if (!Files.isWritable(Paths.get("file")))
{
   System.out.println("file is not writable");
   return;
}
byte[] data = { /* some comma-separated list of byte data items */ };
Files.write(Paths.get("file"), data);

但是,這段程式碼有一個問題。檢測完這個檔案是可寫的以後,在寫之前,其他程式訪問了該檔案,並把它設定為可讀的。這樣,寫檔案的程式碼將會執行失敗。

檢測時間和使用時間之間的競態條件即為廣為人知的 Time of check to time of use (TOCTTOU — 發音為TOCK-too) 問題,這也是為什麼 javaodc中說,這些方法的檢測結果會瞬間過時。

TOCTTOU對外界干擾的概率很低的很多應用來說不是什麼大問題。但是,對安全敏感的應用來說,則是一個重大問題,黑客可能利用這點盜取使用者的認證資訊。對於這樣的應用,

你可能需要避免使用 isWritable() 及它的同類方法,而使用 try/catch 包裹檔案的 I/O 程式碼來代替。

測試檔案或目錄是否存在

問:我怎麼知道一個檔案或目錄是否存在?

答:Files類提供下列靜態方法來判斷一個檔案或目錄是否存在:

  • boolean exists(Path path, LinkOption… options):測試路徑所代表的的檔案或目錄是否存在。預設情況,符號連結會跟蹤的。但是,如果傳遞LinkOption.NOFOLLOW_LINKS引數,符號連結就會跟蹤了。如果目錄或檔案存在,則返回true,如果目錄或檔案不存在或者不知道其存在性,則返回false。
  • boolean notExists(Path path, LinkOption… options):測試路徑所代表的的檔案或目錄是否不存在。預設情況,符號連結是會跟從的。但是,如果傳遞LinkOption.NOFOLLOW_LINKS引數,符號連結就不會跟從了。如果目錄或檔案不存在,則返回true,如果目錄或檔案存在或者不知道其存在性,則返回false。

在進行某種操作之前,你可能需要呼叫 exists() 方法來判斷,以防在檔案不存在時丟擲異常。例如,你可能在刪除檔案之前測試檔案的存在性:

Path path = Paths.get("file");
if (Files.exists(path))
   Files.delete(path);

注意:!exists(path) 不等於 notExists(path)(因為 !exists() 不一定是原子的,而 notExists() 是原子的)。同時,如果 exists() 和 notExists() 都返回false,則說明檔案的存在性不清楚。最後,類似於訪問性判斷方法,這些方法的結果也是會瞬間過時的,因此,在安全性敏感的應用中應該避免至少應改謹慎使用。

更多的測試操作

問:我怎麼知道還能在Path物件上進行哪種操作?

答:Files類提供以下靜態方法倆判斷一個路徑是不是一個目錄,是不是隱藏的,是不是一個正規檔案,兩個路徑是不是指的同一個檔案,以及路徑是不是一個符號連結:

boolean isDirectory(Path path, LinkOption... options):如果不想讓該方法跟蹤符號連結,則指定LinkOption.NOFOLLOW_LINKS引數。
boolean isHidden(Path path)
boolean isRegularFile(Path path, LinkOption... options) ---如果不想讓該方法跟蹤符號連結,則指定LinkOption.NOFOLLOW_LINKS引數。
boolean isSameFile(Path path, Path path2)
boolean isSymbolicLink(Path path)

Consider isHidden(Path) 如果路徑指向的檔案是隱藏的,則返回true,列表5中應用痛過使用這個方法過濾目錄中的隱藏檔案,其輸出結果如下:

import java.io.IOException;

import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class ListHiddenFiles
{
   public static void main(String[] args) throws IOException
   {
      if (args.length != 1)
      {
         System.err.println("usage: java ListHiddenFiles directory");
         return;
      }

      Path dir = Paths.get(args[0]);
      DirectoryStream<Path> stream = Files.newDirectoryStream(dir); 
      for (Path file: stream) 
         if (Files.isHidden(file))
            System.out.println(file);
   }
}

列表5、ListHiddenFiles.java

main() 首先驗證命令列引數,該唯一引數即為查詢影藏檔案的目錄。該字串被轉換為了一個 Path物件。

這個Path物件然後被傳遞到Files類的 DirectoryStream<Path> newDirectoryStream(Path dir) 方法中,該方法返回一個 java.nio.file.DirectoryStream 物件,然後遍歷該path物件指定的目錄下的所有實體。

DirectoryStream 實現了 Iterable 介面,所以你可以使用增強的 for迴圈來遍歷目錄下實體,每次遍歷出來的實體,都呼叫 isHidden() 方法,只有當該方法返回true的時候,才輸出。

編譯列表5(javac ListHiddenFiles.java)然後執行該應用,java ListHiddenFiles C:. 。在我的Windows平臺下,我看的輸出結果如下:

C:hiberfil.sys
C:pagefile.sys

獲取和設定單個屬性

問:怎麼獲取或者設定Path物件的屬性?

答:Files提供下列幾個靜態方法來獲取或設定Path物件的屬性:

FileTime getLastModifiedTime(Path path, LinkOption... options)
UserPrincipal getOwner(Path path, LinkOption... options)
Set<PosixFilePermission> getPosixFilePermissions(Path path, LinkOption... options)
boolean isDirectory(Path path, LinkOption... options)
boolean isHidden(Path path)
boolean isRegularFile(Path path, LinkOption... options)
boolean isSymbolicLink(Path path)
Path setAttribute(Path path, String attribute, Object value, LinkOption... options)
Path setLastModifiedTime(Path path, FileTime time)
Path setOwner(Path path, UserPrincipal owner)
Path setPosixFilePermissions(Path path, Set<PosixFilePermission> perms)
long size(Path path)

在此之前,我演示的 isDirectory()、 isHidden()、isRegularFile() 和 isSymbolicLink() 都對屬於Path物件。這些方法同時也返回一個Path物件的directory、hidden、regularFile 或 symbolicLink 屬性值。

列表6這個應用程式展示了通過 getLastModifiedTime(Path)、getOwner(Path) 和 size(Path) 方法獲取一個Path物件的最後修改事件、擁有者以及大小及這些值的輸出。

import java.io.IOException;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class PathAttrib
{
   public static void main(String[] args) throws IOException
   {
      if (args.length != 1)
      {
         System.err.println("usage: java PathAttrib path-object");
         return;
      }

      Path path = Paths.get(args[0]);
      System.out.printf("Last modified time: %s%n", Files.getLastModifiedTime(path));
      System.out.printf("Owner: %s%n", Files.getOwner(path));
      System.out.printf("Size: %d%n", Files.size(path));
   }
}

列表6、PathAttrib.java(版本1)

編譯列表6(javac PathAttrib.java)),並執行該應用,如 java PathAttrib PathAttrib.java。我看到的輸出結果如下:

Last modified time: 2015-03-18T17:56:15.802635Z
Owner: Owner-PCOwner (User)
Size: 604

問:屬性檢視是什麼?我應該怎麼處理?

答:Javadoc中各種屬性方法(如:getAttribute()、getPosixFilePermissions()、setAttribute() 和 setPosixFilePermissions())涉及到 java.nio.file.attribute 包中的介面型別。這些介面被不同的屬性檢視識別,這些屬性檢視組成了檔案的屬性。一個屬性檢視對映到一個檔案系統的實現(例如POSIX)或一個通用的功能(如檔案所有者關係)。

NIO.2 提供 AttributeView 作為屬性檢視繼承層次中的跟介面。該介面申明一個 getName() 方法,用於返回屬性檢視的名稱。FileAttributeView 繼承自 AttributeView 介面,這是一個對檔案系統的檔案透明的值,是一個只讀的或者可更新的檢視。FileAttributeView 不提供方法,但是作為很多特定面向檔案的的屬性檢視的超類介面存在。

  • BasicFileAttributeView:一個必須被所有檔案系統支援的基本屬性檢視。
  • DosFileAttributeView:繼承自basic屬性檢視,對合法DOS作業系統只讀到,隱藏,檔案系統和歸檔標籤。
  • PosixFileAttributeView:擴充套件自basic屬性檢視,支援POSIX家族的標準(如UNIX).這些屬性包括檔案的所有者,所有者分組以及九中相關的訪問許可。
  • FileOwnerAttributeView:被任何檔案系統支援,任何檔案系統的實現都支援檔案擁有者的概念。
  • AclFileAttributeView:支援檔案的讀與更新一個檔案的訪問控制列表(ACL)。
  • UserDefinedFileAttributeView:支援使用者自定義元素。

注意:一個檔案系統的實現可能支援basic檔案屬性檢視,或者支援幾種後續提到的屬性檢視(甚至支援其他的不在上述列表中的屬性檢視)

Files類提供 <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption… options) 泛型方法來返回特定型別的檔案屬性檢視。例如,AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class);。如果屬性檢視是無效,則該方法返回空。

通常情況下,你需要直接處理 FileAttributeView 或它的子介面,而是通過 getAttribute() 和 setAttribute() 來代替,每個方法的引數要求一個如下語法的字串:

[view-name:]attribute-name

view-name 標示一個FileAttributeView,FileAttributeView可以識別一個檔案屬性的集合。如果指定了,它必須是basic、dos、posix、owner 或者acl 之中一個。如果沒有指定,預設是basic。basic是一個能識別大多數檔案系統通用屬性的檢視。(這裡沒有使用者自定義的檔案屬性檢視)。attribute-name 是屬性的名稱,各種屬性檢視能識別指定名稱的屬性。

如果指定的屬性檢視不被支援,則每個方法都會丟擲 java.lang.UnsupportedOperationException。為了找出什麼檢視是支援的,需要呼叫FileSystem的Set<String> supportedFileAttributeViews() 方法。

列表7演示了輸出指定路徑下,預設的檔案系統支援的屬性檢視,並輸出了所有的(總是支援)basic和(如果支援)acl及dos檢視的屬性值。

import java.io.IOException;

import java.nio.file.Files;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;

public class PathAttrib
{
   public static void main(String[] args) throws IOException
   {
      boolean isACL = false;
      boolean isDOS = false;

      FileSystem defFS = FileSystems.getDefault();
      for (String fileAttrView : defFS.supportedFileAttributeViews())
      {
         System.out.printf("Default file system supports: %s%n", fileAttrView);
         if (fileAttrView.equals("acl"))
            isACL = true;
         if (fileAttrView.equals("dos"))
            isDOS = true;
      }
      System.out.println();

      if (args.length != 1)
      {
         System.err.println("usage: java PathAttrib path-object");
         return;
      }

      Path path = Paths.get(args[0]);

      // Output basic attributes, which are always supported.

      System.out.println("Basic attributes:");
      String[] attrNames =
      {
         "lastModifiedTime",
         "lastAccessTime",
         "creationTime",
         "size",
         "isRegularFile",
         "isDirectory",
         "isSymbolicLink",
         "isOther", // something other that a regular file, directory, or 
                    // symbolic link
         "fileKey"  // an object that uniquely identifies the given file, or 
                    // null when a file key is not available.
      };
      for (String attrName: attrNames)
         System.out.printf("%s: %s%n", attrName,
                           Files.getAttribute(path, "basic:" + attrName));
      System.out.println();

      // Output ACL owner attribute when this view is supported.

      if (isACL)
      {
         System.out.println("ACL attributes:");
         System.out.printf("Owner: %s%n%n", 
                           Files.getAttribute(path, "acl:owner"));
      }

      // Output DOS attributes when this view is supported.

      if (isDOS)
      {
         System.out.println("DOS attributes:");
         attrNames = new String[] { "readonly", "hidden", "system", "archive" };
         for (String attrName: attrNames)
           System.out.printf("%s: %s%n", attrName,
                             Files.getAttribute(path, "dos:" + attrName));
         System.out.println();
      }
   }
}

列表7、PathAttrib.java(版本2)

編譯列表7並執行,如 java PathAttrib PathAttrib.java。我看到的輸出結果如下:

Default file system supports: acl
Default file system supports: basic
Default file system supports: owner
Default file system supports: user
Default file system supports: dos

Basic attributes:
lastModifiedTime: 2015-03-18T19:36:47.435624Z
lastAccessTime: 2015-03-18T19:21:16.681388Z
creationTime: 2015-03-18T19:21:16.681388Z
size: 2417
isRegularFile: true
isDirectory: false
isSymbolicLink: false
isOther: false
fileKey: null

ACL attributes:
Owner: Owner-PCOwner (User)

DOS attributes:
readonly: false
hidden: false
system: false
archive: true

獲取和設定多個屬性

問:聽說有更高效的方法來批量讀取檔案屬性,而不是單個的處理。我該怎麼樣批量讀取檔案屬性?

答:Files申明瞭兩個靜態方法來批量讀取檔案屬性。

<A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options)
Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options)

第一個方法返回 java.nio.file.attribute.BasicFileAttributes 或它的子介面物件,該物件包含了所有的指定型別屬性值。通過返回的Call型別的方法可以獲得物件這些值。

第二個方法返回一個map,map包含了所有由屬性引數識別的的名值對。這些引數的形式是 [view-name:]attribute-list;  例如, “posix:permissions,owner,size”、

列表8演示這些方法。該應用一個Path物件為引數,每個方法都去讀取basic檔案屬性,然後輸出相應的值:

import java.io.IOException;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import java.nio.file.attribute.BasicFileAttributes;

import java.util.Map;
import java.util.TreeMap;

public class PathAttrib
{
   public static void main(String[] args) throws IOException
   {
      if (args.length != 1)
      {
         System.err.println("usage: java PathAttrib path-object");
         return;
      }

      Path path = Paths.get(args[0]);

      BasicFileAttributes attrs;
      attrs = Files.readAttributes(path, BasicFileAttributes.class);

      System.out.printf("Basic Attributes for %s...%n%n", path);

      System.out.printf("creationTime: %s%n", attrs.creationTime());
      System.out.printf("fileKey: %s%n", attrs.fileKey());
      System.out.printf("isDirectory: %b%n", attrs.isDirectory());
      System.out.printf("isOther: %b%n", attrs.isOther());
      System.out.printf("isRegularFile: %b%n", attrs.isRegularFile());
      System.out.printf("isSymbolicLink: %b%n", attrs.isSymbolicLink());
      System.out.printf("lastAccessTime: %s%n", attrs.lastAccessTime());
      System.out.printf("lastModifiedTime: %s%n", attrs.lastModifiedTime());
      System.out.printf("size: %d%n", attrs.size());
      System.out.println();

      Map<String, Object> attrMap = new TreeMap<>(Files.readAttributes(path, 
                                                                       "*"));
      for (Map.Entry<String, Object> entry: attrMap.entrySet())
         System.out.printf("%s: %s%n", entry.getKey(), entry.getValue());
   }
}

列表8、PathAttrib.java(版本3)

在列表8中,我傳入 “*” 作為第二個引數傳遞給第二個方法 readAttributes(),而不是指定一個具體basic中的檢視名稱,這就意味著basic屬性檢視的所有屬性都需要返回。同時,我將 readAttributes() 的返回結果傳遞到 java.util.TreeMap 構造器中來獲取到一個排序好的map,所以,輸出實體的順序是經過排序的。
編譯列表8,並執行該應用。如:java PathAttrib PathAttrib.java,我觀察到的輸出的結果如下:

Basic Attributes for PathAttrib.java...

creationTime: 2015-03-18T20:20:43.635406Z
fileKey: null
isDirectory: false
isOther: false
isRegularFile: true
isSymbolicLink: false
lastAccessTime: 2015-03-18T20:20:43.635406Z
lastModifiedTime: 2015-03-18T20:35:20.446953Z
size: 1618

creationTime: 2015-03-18T20:20:43.635406Z
fileKey: null
isDirectory: false
isOther: false
isRegularFile: true
isSymbolicLink: false
lastAccessTime: 2015-03-18T20:20:43.635406Z
lastModifiedTime: 2015-03-18T20:35:20.446953Z
size: 1618

更多屬性操作

問:我還能在一個Path物件上進行哪些屬性操作?

答:你可以讀取一個Path物件關聯的檔案的儲存屬性值(儲存池、裝置、分割槽、卷、具體的檔案系統、其它檔案儲存的特定實現的內容)。可通過下列的方法完成該任務:

  • 呼叫Files的 FileStore getFileStore(Path path) 來獲取檔案的儲存物件。
  • 呼叫 java.nio.file.FileStore 物件的 getAttribute(String attribute) 方法來獲取給定儲存屬性的值,你必須指定引數格式為 view-name:attribute-name 的字串。

列表9是一個小應用程式的原始碼,這個應用程式演示了怎樣使用 getAttribute() 獲取NTFS檔案儲存實現的屬性並輸出屬性值:

import java.io.IOException;

import java.nio.file.Files;
import java.nio.file.FileStore;
import java.nio.file.Path;
import java.nio.file.Paths;

public class PathAttrib
{
   public static void main(String[] args) throws IOException
   {
      if (args.length != 1)
      {
         System.err.println("usage: java PathAttrib path-object");
         return;
      }

      Path path = Paths.get(args[0]);
      FileStore fs = Files.getFileStore(path);
      if (fs.type().equals("NTFS"))
      {
         String[] attrNames = 
         {
            "totalSpace",
            "usableSpace",
            "unallocatedSpace",
            "volume:vsn",
            "volume:isRemovable",
            "volume:isCdrom"
         };
         for (String attrName: attrNames)
            System.out.println(fs.getAttribute(attrName));
      }
   }
}

列表9、PathAttrib.java(版本4)

當FileStore的 String type() 方法返回的檔案儲存型別為NTFS時,該檔案儲存的實現可以識別列表中的屬性(不應該通過 getAttribute() 方法獲取總空間、使用空間、未分配空間,你應該使用更方便的方法,long getTotalSpace()、long getUsableSpace() 和 long getUnallocatedSpace() 方法)。

編譯列表9,並執行。如 java PathAttrib PathAttrib.java。我觀察到的輸出結果如下:如果檔案儲存型別不是NTFS,則沒有任何輸出:

499808989184
229907410944
229907410944
1079402743
false
false

FileStore也宣告一個 <V extends FileStoreAttributeView> V getFileStoreAttributeView(Class<V> type) 方法來獲取指定型別的FileStoreAttributeView(一個只讀或可更新的檔案儲存屬性檢視)物件,但因為官方沒有提供子介面,這些方法可能只適合於使用者自定義的檔案系統和API包。