如何在Java中使用檔案操作API: java.nio.file.Path?- marcobehler

banq發表於2020-05-30

本文有關學習如何在Java中使用檔案:從讀取和寫入檔案到觀察目錄和使用記憶體檔案系統。
Java有兩個檔案API。
  • 原始java.io.File API,自Java 1.0(1996)起可用。
  • java.nio.file.Path從Java 1.7(2011)開始可用的較新API。

File和Path API有什麼區別?
舊檔案API用於大量舊專案,框架和庫。儘管它已經很久了,但它並沒有被棄用(並且可能永遠不會被棄用),您仍然可以將其與任何最新的Java版本一起使用。
但是,java.nio.file.Path做一切java.io.File可以做的,但總的來說,它可以做得更好。一些例子:
  • 檔案功能:新類支援符號連結,適當的檔案屬性和後設資料支援(認為:PosixFileAttributes),ACL等。
  • 更好的用法:例如,刪除檔案時,您會收到一個異常提示,並帶有有意義的錯誤訊息(沒有此類檔案,檔案被鎖定等),而不是簡單的布林型說法false。
  • 解耦:啟用對記憶體中檔案系統的支援,我們將在後面介紹。

有關這兩種API之間差異的完整列表,請檢視本文:https : //www.oracle.com/technical-resources/articles/javase/nio.html

由於上述原因,如果您要啟動一個新的Java專案,強烈建議您在Paths API而不是File API。(即使檔案比path讀起來好多了,不是嗎?)因此,Paths在本文中,我們將僅專注於API。

Path.of:如何引用檔案
要使用Java處理檔案,您首先需要引用檔案(真是令人驚訝!)。如前所述,從Java 7開始,您將使用Paths API來引用檔案,因此,一切都始於構造Path物件。
讓我們看一些程式碼。

public static void main(String[] args) throws URISyntaxException {

    // Java11+  : Path.of()

    Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
    System.out.println(path);

    path = Path.of("c:/dev/licenses/windows/readme.txt");
    System.out.println(path);

    path = Path.of("c:" , "dev", "licenses", "windows", "readme.txt");
    System.out.println(path);

    path = Path.of("c:" , "dev", "licenses", "windows").resolve("readme.txt"); // resolve == getChild()
    System.out.println(path);

    path = Path.of(new URI("file:///c:/dev/licenses/windows/readme.txt"));
    System.out.println(path);

    // Java < 11 equivalent: Paths.get()
    path = Paths.get("c:/dev/licenses/windows/readme.txt");
    System.out.println(path);

    // etc...
}

從Java 11開始,您應該使用static Path.of方法構造路徑。
如果在Windows上使用正斜槓都沒有關係,因為Path API足夠聰明,可以獨立於作業系統和任何正反斜槓問題構造正確的路徑。
構造路徑時,還有更多選擇:您不必將完整路徑指定為一個長字串:

path = Path.of("c:" , "dev", "licenses", "windows", "readme.txt");
System.out.println(path);

path = Path.of("c:" , "dev", "licenses", "windows").resolve("readme.txt"); // resolve == getChild()
System.out.println(path);


相反,您可以將字串序列傳遞給該Path.of方法,或者構造父目錄並使用它來獲取子檔案(.resolve(child))。
最後但並非最不重要的一點是,您還可以將URI傳遞給Path.of呼叫。

path = Path.of(new URI("file:///c:/dev/licenses/windows/readme.txt"));
System.out.println(path);


因此,構造Path物件有多種選擇。
但是,有兩個要點:
  1. 構造路徑物件或解析子物件並不意味著檔案或目錄實際存在。該路徑僅是對潛在檔案的引用。因此,您必須單獨驗證其存在。
  2. Java-11 Path.of之前的版本稱為Paths.get,如果您使用較舊的Java版本或構建需要向後相容的庫,則需要使用它。從Java 11開始,Paths.get內部重定向到Path.of。
    // Java < 11 equivalent: Paths.get()path = Paths.get("c:/dev/licenses/windows/readme.txt");System.out.println(path);
    


檔案:常用操作

1. 檢查檔案是否存在

Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
boolean exists = Files.exists(path);
System.out.println("exists = " + exists);


檢查檔案或目錄是否存在。還允許您指定其他引數,以定義如何處理符號連結,即是否遵循(預設)。
執行此程式碼段時,您將獲得一個簡單的布林標誌:exists = true

2.如何獲取檔案的最後修改日期

Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
FileTime lastModifiedTime = Files.getLastModifiedTime(path);
System.out.println("lastModifiedTime = " + lastModifiedTime);


3.如何比較檔案(Java12 +)

Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
long mismatchIndex = Files.mismatch(path, Paths.get("c:\\dev\\whatever.txt"));
System.out.println("mismatch = " + mismatchIndex);

這是Java的相對較新的功能,自Java 12起可用。它比較兩個檔案的大小和位元組,並返回第一個(位元組)不匹配的位置。或者,如果沒有不匹配,則為-1L。

4.如何獲取檔案的所有者

Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
UserPrincipal owner = Files.getOwner(path);
System.out.println("owner = " + owner);

將檔案或目錄的所有者返回為UserPrincipal(從擴充套件Principal)。在Windows上,這將是WindowsUserPrincipal,其中包含使用者的帳戶名(如下所示)以及sidWindows使用者在Windows計算機上的唯一安全識別符號。

owner = DESKTOP-168M0IF\marco_local (User)


5.如何建立臨時檔案

Path tempFile1 = Files.createTempFile("somePrefixOrNull", ".jpg");
System.out.println("tempFile1 = " + tempFile1);

Path tempFile2 = Files.createTempFile(path.getParent(), "somePrefixOrNull", ".jpg");
System.out.println("tempFile2 = " + tempFile2);

Path tmpDirectory = Files.createTempDirectory("prefix");
System.out.println("tmpDirectory = " + tmpDirectory);


建立臨時檔案時,可以指定字首(第一引數)和字尾(第二引數)。兩者都可以為null。
該字首將以temp檔名作為字首(duh!),字尾本質上是副檔名,並且如果您省略它,則將使用預設副檔名“ .tmp”。檔案將在預設的臨時檔案目錄中建立
除了預設的臨時目錄,您還可以指定自己的目錄:

Path tmpDirectory = Files.createTempDirectory("prefix");
System.out.println("tmpDirectory = " + tmpDirectory);

除了檔案,您還可以建立臨時目錄。由於在建立目錄時不需要字尾引數,因此只需選擇指定字首引數即可。
注意:與流行的看法相反,臨時檔案不會刪除自己。在單元測試中建立它們或在生產中執行時,必須確保明確刪除它們。

6.如何建立檔案和目錄
您已經瞭解瞭如何建立臨時檔案,對於普通檔案和目錄也是如此。您將呼叫不同的方法:

Path newDirectory = Files.createDirectories(path.getParent().resolve("some/new/dir"));
System.out.println("newDirectory = " + newDirectory);

Path newFile = Files.createFile(newDirectory.resolve("emptyFile.txt"));
System.out.println("newFile = " + newFile);


有人對.resolve感到困惑:.resolve呼叫未建立檔案,它僅返回對您要建立的(子)檔案的引用。


7.如何獲得檔案的Posix許可權
如果在類似Unix的系統(包括Linux和MacOS)上執行Java程式,則可以獲得檔案的Posix許可權。認為:“-rw-rw-rw-”或“ -rwxrwxrwx”等。

Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
try {
    Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(path);
    System.out.println("permissions = " + permissions);
} catch (UnsupportedOperationException e) {
    System.err.println("Looks like you're not running on a posix file system");
}

在Linux或MacOS上執行此命令,您將獲得以下輸出:

OWNER_WRITE
OWNER_READ
GROUP_WRITE
OTHERS_READ
...



8.讀寫檔案
如何將字串寫入檔案:

Path utfFile = Files.createTempFile("some", ".txt");
Files.writeString(utfFile, "this is my string ää öö üü"); // UTF 8
System.out.println("utfFile = " + utfFile);

Path iso88591File = Files.createTempFile("some", ".txt");
Files.writeString(iso88591File, "this is my string ää öö üü", StandardCharsets.ISO_8859_1); // otherwise == utf8
System.out.println("iso88591File = " + iso88591File);


從Java 11(更具體地說是11.0.2 / 12.0,因為以前的版本中存在一個錯誤)開始,您應該使用該Files.writeString方法將字串內容寫入檔案。預設情況下,它將寫入UTF-8檔案,但是您可以透過指定其他編碼來覆蓋它。

9.如何將位元組寫入檔案

Path anotherIso88591File = Files.createTempFile("some", ".txt");
Files.write(anotherIso88591File, "this is my string ää öö üü".getBytes(StandardCharsets.ISO_8859_1));
System.out.println("anotherIso88591File = " + anotherIso88591File);

如果要向檔案中寫入位元組(在Java版本低於11的舊版本中,必須使用相同的API來編寫字串),則需要呼叫Files.write。

10.寫入檔案時的選項

Path anotherUtf8File = Files.createTempFile("some", ".txt");
Files.writeString(anotherUtf8File, "this is my string ää öö üü", StandardCharsets.UTF_8,
        StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
System.out.println("anotherUtf8File = " + anotherUtf8File);

Path oneMoreUtf8File = Files.createTempFile("some", ".txt");
Files.write(oneMoreUtf8File, "this is my string ää öö üü".getBytes(StandardCharsets.UTF_8),
        StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
System.out.println("oneMoreUtf8File = " + oneMoreUtf8File);

當呼叫這兩種write方法時,將自動建立檔案(如果檔案已經存在,則將其截斷)。這意味著,我們將不必像上面那樣建立顯式的臨時檔案。
如果您不希望這種行為(即,如果檔案已經存在則失敗)並獲得相應的異常,則需要傳遞另一個OpenOption

11.使用Writer和OutputStreams

try (BufferedWriter bufferedWriter = Files.newBufferedWriter(utfFile)) {
    // handle reader
}

try (OutputStream os = Files.newOutputStream(utfFile)) {
    // handle outputstream
}

最後但並非最不重要的一點是,如果要直接使用編寫器或輸出流,請確保呼叫相應的Files方法,而不要手動構造編寫器或流。

 12.如何從檔案中讀取字串

String s = Files.readString(utfFile);// UTF 8
System.out.println("s = " + s);

s = Files.readString(utfFile, StandardCharsets.ISO_8859_1); // otherwise == utf8
System.out.println("s = " + s);

在Java11 +上,您應該使用該Files.readString方法從檔案中讀取字串。確保傳遞適當的檔案編碼;預設情況下,Java將使用UTF-8編碼來讀入檔案。
 13.如何從檔案讀取位元組

s = new String(Files.readAllBytes(utfFile), StandardCharsets.UTF_8);
System.out.println("s = " + s);


14.使用reader和InputStreams

try (BufferedReader bufferedReader = Files.newBufferedReader(utfFile)) {
    // handle reader
}

try (InputStream is = Files.newInputStream(utfFile)) {
    // handle inputstream
}

無論何時建立,寫入或讀取檔案,您絕對應該使用顯式編碼,儘管新的Java 11方法預設使用UTF-8而不是特定於平臺的編碼有很大幫助。

15.移動,刪除和列出檔案

Path utfFile = Files.createTempFile("some", ".txt");

try {
    Files.move(utfFile, Path.of("c:\\dev"));  // this is wrong!
} catch (FileAlreadyExistsException e) {
    // welp, that din't work!
}

 Files.move方法不會將檔案移動到指定目錄(您可能會期望)。
  • test.jpg→c:\temp不起作用。
  • test.jpg→c:\temp\test.jpg有效。

Files.move(utfFile, Path.of("c:\\dev").resolve(utfFile.getFileName().toString()));

 不要將檔案移動到資料夾,而是將它們“移動”到它們的全新路徑,包括檔名和副檔名。

 
Path utfFile2 = Files.createTempFile("some", ".txt");
Files.move(utfFile2, Path.of("c:\\dev").resolve(utfFile.getFileName().toString()), StandardCopyOption.REPLACE_EXISTING);

Path utfFile3 = Files.createTempFile("some", ".txt");
Files.move(utfFile3, Path.of("c:\\dev").resolve(utfFile.getFileName().toString()), StandardCopyOption.ATOMIC_MOVE);
移動檔案時,還可以根據基礎檔案系統的功能指定移動方式。
  • 預設情況下,如果目標檔案已存在,FileAlreadyExistsException將丟擲。
  • 如果指定該StandardCopyOption.REPLACE_EXISTING選項,則目標檔案將被覆蓋。
  • 如果指定此StandardCopyOption.ATOMIC_MOVE選項,則可以將檔案移動到目錄中,並確保監視目錄的所有程式都可以訪問完整檔案,而不僅僅是部分檔案。


刪除檔案和目錄:
try {
    Files.delete(tmpDir);
} catch (DirectoryNotEmptyException e) {
    e.printStackTrace();
}

僅在目錄為空時刪除它們。不幸的是,沒有清除非空目錄的標誌,您只會得到一個DirectoryNotEmptyException。
如果要使用純Java版本刪除非空目錄樹,則需要執行以下操作:

try (Stream<Path> walk = Files.walk(tmpDir)) {
    walk.sorted(Comparator.reverseOrder()).forEach(path -> {
        try {
            Files.delete(path);
        } catch (IOException e) {
            // something could not be deleted..
            e.printStackTrace();
        }
    });
}

Files.walk從您指定的目錄開始,將深度優先遍歷檔案樹。該reverseOrder比較器將確保您刪除所有兒童,刪除實際目錄之前。不幸的是,Files.delete在forEach使用者內部使用時,您還需要捕獲IOException 。刪除非空目錄的大量程式碼,不是嗎?
羅列檔案:

try (var files = Files.list(tmpDirectory)) {
    files.forEach(System.out::println);
}

try (var files = Files.newDirectoryStream(tmpDirectory, "*.txt")) {
    files.forEach(System.out::println);
}

遞迴列出檔案:

try (var files = Files.walk(tmpDirectory)) {
    files.forEach(System.out::println);
}


 16.記憶體中檔案系統
一些開發人員認為使用檔案總是意味著您實際上必須將它們寫入磁碟。在測試過程中,這導致建立許多臨時檔案和目錄,然後必須確保再次將其刪除。但是,使用Java的Path-API,有一種更好的方法:記憶體檔案系統。它們使您可以完全在記憶體中寫入和讀取檔案,而無需打磁碟。超快速且非常適合測試(只要您不用完記憶體,erm…)。
有兩個值得關注的Java記憶體檔案系統。一種選擇是“ 記憶體檔案系統”
  
import com.github.marschall.memoryfilesystem.MemoryFileSystemBuilder; 
  try (FileSystem fileSystem = MemoryFileSystemBuilder.newMacOs().build()) {

            Path inMemoryFile = fileSystem.getPath("/somefile.txt");
            Files.writeString(inMemoryFile, "Hello World");

            System.out.println(Files.readString(inMemoryFile));
        }

透過呼叫newLinux()或newWindows(),newMacOs()您可以控制建立的檔案系統的語義。
 另一個選擇是JimFS。讓我們看看如何使用它建立一個記憶體檔案系統。

 
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;

        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix());) {

            Path inMemoryFile = fileSystem.getPath("/tmp/somefile.txt");
            Files.writeString(inMemoryFile, "Hello World");

            System.out.println(Files.readString(inMemoryFile));
        }


如何使您的應用程式與記憶體檔案系統一起使用:不要使用 Path.of 和 Paths.get, 使用 FileSystem 或 Path 

相關文章