1. 程式人生 > >Java NIO 學習筆記(五)----路徑、檔案和管道 Path/Files/Pipe

Java NIO 學習筆記(五)----路徑、檔案和管道 Path/Files/Pipe

目錄:
Java NIO 學習筆記(一)----概述,Channel/Buffer
Java NIO 學習筆記(二)----聚集和分散,通道到通道
Java NIO 學習筆記(三)----Selector
Java NIO 學習筆記(四)----檔案通道和網路通道
Java NIO 學習筆記(五)----路徑、檔案和管道 Path/Files/Pipe
Java NIO 學習筆記(六)----非同步檔案通道 AsynchronousFileChannel

Path 介面和 Paths 類

Path 介面是 NIO2(AIO) 的一部分,是對 NIO 的更新,Path 介面已新增到 Java 7 中,完全限定類名是 java.nio.file.Path 。

Path 例項表示檔案系統中的路徑。 路徑可以指向檔案或目錄,也可以是絕對的或相對的。在某些作業系統中,不要將檔案系統路徑與環境變數中的 path 路徑相混淆。 java.nio.file.Path 介面與路徑環境 path 變數無關。

在許多方面,java.nio.file.Path 介面類似於 java.io.File 類,但存在一些細微差別。 但在許多情況下,可以使用 Path 介面替換 File 類的使用。

建立 Path 物件

可以使用名為 Paths.get() 的 Paths 類(java.nio.file.Paths)中的靜態方法建立 Path 例項,get()方法是 Path 例項的工廠方法,一個示例如下:

public class PathExample {
    public static void main(String[] args) {
        // 使用絕對路徑建立
        Path absolutePath = Paths.get("D:\\test\\1.txt");
        // 使用相對路徑建立
        Path relativePath = Paths.get("D:\\test", "1.txt");
        System.out.println(absolutePath.equals(relativePath)); // ture
    }
}

注意路徑分隔符在 Windows 上是“\”,在 Linux 上是 “/”。

Paths 類只有2個方法:

方法 描述
static Path get(String first, String... more) 將路徑字串或在連線時形成路徑字串的字串序列轉換為路徑。
static Path (URI uri) 將給定URI轉換為路徑物件。

Path 介面部分方法:

方法 描述
boolean endsWith(Path other) 測試此路徑是否以給定路徑結束。
boolean equals(Object other) 取決於檔案系統的實現。一般不區分大小寫,有時區分。 不訪問檔案系統。
Path normalize() 返回一個路徑,該路徑消除了冗餘的名稱元素,比如'.', '..'
Path toAbsolutePath() 返回表示該路徑的絕對路徑的路徑物件。
File toFile() 返回表示此路徑的 File 物件。
String toString() 返回的路徑字串使用預設名稱分隔符分隔路徑中的名稱。

Files

NIO 檔案類(java.nio.file.Files)為操作檔案系統中的檔案提供了幾種方法,File 類與 java.nio.file.Path 類一起工作,需要了解 Path 類,然後才能使用 Files 類。

判斷檔案是否存在

static boolean exists(Path path, LinkOption... options)

options 引數用於指示,在檔案是符號連結的情況下,如何處理該符號連結,預設是處理符號連結的。其中 LinkOption 物件是一個列舉類,定義如何處理符號連結的選項。整個類只有一個 NOFOLLOW_LINKS; 常亮,代表不跟隨符號連結。

createDirectory(Path path) 建立目錄

Path output = Paths.get("D:\\test\\output");
Path newDir = Files.createDirectory(output);
// Files.createDirectories(output); // 這個方法可以一併建立不存在的父目錄
System.out.println(output == newDir); // true
System.out.println(Files.exists(output)); // true

如果建立目錄成功,則返回指向新建立的路徑的 Path 例項,此例項和引數是同一個例項。
如果該目錄已存在,則丟擲 FileAlreadyExistsException 。 如果出現其他問題,可能會丟擲IOException ,例如,如果所需的新目錄的父目錄不存在。

複製檔案

一共有 3 個複製方法:

static long copy(Path source, OutputStream out);
static Path copy(Path source, Path target, CopyOption... options);
static long copy(InputStream in, Path target, CopyOption... options)

其中 CopyOption 選項可以選擇指定複製模式,一般是其子列舉類 StandardCopyOption 提供選項,有 3 種模式,第二個引數是可變形參,可以多個組合一起使用:

  1. ATOMIC_MOVE :原子複製,不會被執行緒排程機制打斷的操作;一旦開始,就一直執行到結束;
  2. COPY_ATTRIBUTES :同時複製屬性,預設是不復制屬性的;
  3. REPLACE_EXISTING :重寫模式,會覆蓋已存在的目的檔案;

一個例子如下:

Path sourcePath = Paths.get("D:\\test\\source.txt"); // 原始檔必須先存在
Path desPath = Paths.get("D:\\test\\des.txt"); // 目的檔案可以不存在
Files.copy(sourcePath, desPath); // 預設情況,如果目的檔案已存在則丟擲異常
Files.copy(sourcePath, desPath, StandardCopyOption.REPLACE_EXISTING); // 覆蓋模式

注意:複製資料夾的時候,只能複製空資料夾,如果資料夾非空,需要遞迴複製,否則只能得到一個空資料夾,而資料夾裡面的檔案不會被複制。

移動檔案/資料夾

只有 1 個移動檔案或資料夾的方法:

static Path move(Path source, Path target, CopyOption... options);

如果檔案是符號連結,則移動符號連結本身,而不是符號連結指向的實際檔案。
和移動檔案一樣,也存在第三個可選引數 CopyOption ,參考上述。如果移動檔案失敗,可能會丟擲 IOException,例如,如果檔案已存在於目標路徑中,並且遺漏了覆蓋選項,或者要移動的原始檔不存在等。

和複製資料夾不一樣,如果資料夾裡面有內容,複製只會複製空資料夾,而移動會把資料夾裡面的所有東西一起移動過去,以下是一個移動資料夾的示例:

// 移動 s 目錄到一個不存在的新目錄
Path s = Paths.get("D:\\s");
Path d = Paths.get("D:\\test\\test");
Files.createDirectories(d.getParent());
Files.move(s, d);

和 Linux mv 命令一樣,重新命名檔案與移動檔案方式相同,移動檔案還可以將檔案移動到不同的目錄並可以同時更改其名稱。 另外 java.io.File 類也可以使用它的 renameTo() 方法來實現移動檔案,但現在 java.nio.file.Files 類中也有檔案移動功能。

刪除檔案/資料夾

static void delete(Path path);
static boolean deleteIfExists(Path path); // 如果檔案被此方法刪除則返回 true

如果檔案是目錄,則該目錄必須為空才能刪除。

Files.walkFileTree() 靜態方法

刪除和複製資料夾的時候,如果資料夾為空,那麼會刪除失敗或者只能複製空資料夾,此時可以使用 walkFileTree() 方法進行遍歷檔案樹,然後在 FileVisitor 物件的 visitFile() 方法中執行刪除或複製檔案操作。
Files 類有 2 個過載的 walkFileTree() 方法,如下:

static Path walkFileTree(Path start,
                                FileVisitor<? super Path> visitor);

static Path walkFileTree(Path start,
                                Set<FileVisitOption> options,
                                int maxDepth,
                                FileVisitor<? super Path> visitor);

將 Path 例項和 FileVisitor 作為引數,walkfiletree() 方法可以遞迴遍歷目錄樹。Path 例項指向要遍歷的目錄。在遍歷期間呼叫 FileVisitor ,首先介紹 FileVisitor 介面:

public interface FileVisitor<T> {
    
    FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException;

    FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException;
    
    FileVisitResult visitFileFailed(T file, IOException exc) throws IOException;
    
    FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException;
}

必須自己實現 FileVisitor 介面,並將其實現的例項傳遞給 walkFileTree() 方法。在目錄遍歷期間,將在不同的時間呼叫 FileVisitor 實現的 4 個方法,代表對遍歷到的檔案或目錄進行什麼操作。如果不需要使用到所有方法,可以擴充套件 SimpleFileVisitor 類,該類包含 FileVisitor 介面中所有方法的預設實現。

Files.walkFileTree(inputPath, new FileVisitor<Path>() {
    // 訪問資料夾之前呼叫此方法
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
        System.out.println("pre visit dir:" + dir);
        return FileVisitResult.CONTINUE;
    }

    // 訪問的每個檔案都會呼叫此方法,只針對檔案,不會對目錄執行
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        return FileVisitResult.CONTINUE;
    }

    // 訪問檔案失敗會呼叫此方法,只針對檔案,不會對目錄執行
    @Override
    public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
        return FileVisitResult.CONTINUE;
    }

    // 訪問資料夾之後會呼叫此方法
    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
        return FileVisitResult.CONTINUE;
    }
});

這四個方法都返回一個 FileVisitResult 列舉例項。FileVisitResult 列舉包含以下四個選項:

  • CONTINUE : 繼續
  • TERMINATE : 終止
  • SKIP_SIBLINGS : 跳過兄弟節點,然後繼續
  • SKIP_SUBTREE : 跳過子樹(不訪問此目錄的條目),然後繼續,僅在 preVisitDirectory 方法返回時才有意義,除此以外和 CONTINUE 相同。

通過返回其中一個值,被呼叫的方法可以決定檔案遍歷時接下來應該做什麼。

搜尋檔案

walkFileTree() 方法還可以用於搜尋檔案,下面這個例子擴充套件了 SimpleFileVisitor 來查詢一個名為 input.txt 的檔案:

Path rootPath = Paths.get("D:\\test");
String fileToFind = File.separator + "input.txt";

Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        String fileString = file.toAbsolutePath().toString();
        System.out.println("pathString: " + fileString);

        if (fileString.endsWith(fileToFind)) {
            System.out.println("file found at path: " + fileString);
            return FileVisitResult.TERMINATE;
        }
        return FileVisitResult.CONTINUE;
    }
});

同理,刪除有內容的目錄時,可以重寫 visitFile() 方法,並在裡面執行刪除檔案操作,重寫 postVisitDirectory() 方法,並在裡面執行刪除目錄操作即可。

Files 類中的其他方法

Files 類包含許多其他有用的函式,例如用於建立符號連結,確定檔案大小,設定檔案許可權等的函式。有關java.nio.file.Files 類的詳細資訊,請檢視 JavaDoc

管道 Pipe

Pipe 是兩個執行緒之間的單向資料連線。管道有 source 通道和一個 sink 通道,將資料寫入 sink 通道,就可以從 source 通道讀取該資料。
以下是管道原理的說明:

image

使用管道進行讀取資料

先看一個完整的例子:

public class PipeExample {
    public static void main(String[] args) throws IOException {
        Pipe pipe = Pipe.open();
        Pipe.SinkChannel sinkChannel = pipe.sink(); // sink 通道寫入資料
        String data = "some string";

        ByteBuffer buffer = ByteBuffer.allocate(32);
        buffer.clear();
        buffer.put(data.getBytes());

        buffer.flip(); // 反轉緩衝區,準備被讀取
        while (buffer.hasRemaining()) {
            sinkChannel.write(buffer); // 將 Buffer 的資料寫入 sink 通道
        }

        Pipe.SourceChannel sourceChannel = pipe.source(); // 源通道讀取資料
        ByteBuffer readBuffer = ByteBuffer.allocate(32);
        int bytesRead = sourceChannel.read(readBuffer); // 返回值代表讀取了多少資料
        System.out.println("Read: " + bytesRead); // Read: 11

        System.out.println(new String(readBuffer.array())); // some string
    }
}

如上程式碼,首先要建立管道,開啟管道之後是使用同一個管道物件獲取對應的 sink 通道和 source 通道的,這會自動地將兩個通道連線起來,作為對比,在標準 IO 管道中是分別建立讀管道和寫管道,然後在構造器中或者使用pipe1.connect(pipe2) 方法來連線起來,如下:

PipedOutputStream output = new PipedOutputStream();

PipedInputStream input = new PipedInputStream();
input.connect(output);
// 或者使用如下1行程式碼,可以代替上面2行程式碼來連線2個管道
//PipedInputStream input = new PipedInputStream(output);