目錄:
Java NIO 學習筆記(一)----概述,Channel/Buffer
Java NIO 學習筆記(二)----聚集和分散,通道到通道
Java NIO 學習筆記(三)----Selector
Java NIO 學習筆記(四)----文件通道和網絡通道
Java NIO 學習筆記(五)----路徑、文件和管道 Path/Files/Pipe
Java NIO 學習筆記(六)----異步文件通道 AsynchronousFileChannel
Java NIO 學習筆記(七)----NIO/IO 的對比和總結
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 種模式,第二個參數是可變形參,可以多個組合一起使用:
ATOMIC_MOVE
:原子復制,不會被線程調度機制打斷的操作;一旦開始,就一直運行到結束;COPY_ATTRIBUTES
:同時復制屬性,默認是不復制屬性的;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 通道讀取該數據。
以下是管道原理的說明:
使用管道進行讀取數據
先看一個完整的例子:
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);