目錄
- 一、NIO 與 IO 的深度剖析
- 1.1 IO 的局限性
- 1.2 NIO 核心特性
- 1.3 NIO 核心組件
- 1.4 NIO 適用場景
- 二、NIO 核心組件實戰
- 2.1 Buffer 緩沖區
- 2.2 Channel 通道
- 2.3 Selector 選擇器
- 2.4 NIO 文件操作案例
- 三、NIO2.0 實戰
- 3.1 Path 類
- 3.2 Files 類
- 3.3 Files 類高級操作
- 3.4 NIO2.0 實戰案例
一、NIO 與 IO 的深度剖析
在 Java 編程領域,輸入輸出(I/O)操作是與外部資源交互的基礎,如文件、網絡連接等。傳統的 I/O 模型在處理簡單場景時表現出色,但隨著應用程序對性能和并發處理能力要求的不斷提高,其局限性逐漸顯現。Java NIO(New I/O)的出現,為開發者提供了一種更高效、更靈活的 I/O 處理方式,尤其在高并發和大數據傳輸場景中展現出顯著優勢。接下來,我們將深入探討 NIO 與傳統 IO 的區別,以及 NIO 的核心特性、組件和適用場景。
1.1 IO 的局限性
傳統的 Java IO 是基于流(Stream)的操作,其主要特點是面向字節或字符序列,數據以順序的方式從數據源讀取或寫入到目的地。這種方式在處理簡單的 I/O 任務時非常直觀和方便,但在高并發和大數據量處理場景下,暴露出了一些明顯的局限性。
- 阻塞問題:Java IO 是阻塞式的,當一個線程調用 read () 或 write () 方法時,該線程會被阻塞,直到有數據可讀或數據完全寫入。這意味著在 I/O 操作進行期間,線程無法執行其他任務,嚴重浪費了 CPU 資源。例如,在一個服務器應用中,如果同時有大量客戶端連接,每個連接都需要一個獨立的線程來處理 I/O 操作,那么隨著連接數的增加,線程數量也會急劇增加,導致線程上下文切換開銷增大,系統性能急劇下降。
- 面向流的局限性:IO 是面向流的,流是單向的,要么是輸入流,要么是輸出流。這就限制了數據處理的靈活性,對于一些需要同時進行讀寫操作的場景,需要分別創建輸入流和輸出流,增加了代碼的復雜性。此外,流操作是順序的,無法隨機訪問數據,對于一些需要隨機讀寫的場景,如文件的部分內容更新,處理起來比較困難。
- 單線程處理能力有限:由于每個 I/O 操作都需要一個線程來處理,當并發連接數較多時,線程資源會被大量消耗,系統的可擴展性受到限制。而且,線程的創建和銷毀也會帶來一定的開銷,進一步降低了系統的性能。
1.2 NIO 核心特性
Java NIO 旨在解決傳統 IO 的局限性,提供了一系列新的特性,使其在高并發和大數據處理場景中表現出色。
- 非阻塞特性:NIO 支持非阻塞 I/O 操作,當一個線程從通道請求數據時,如果沒有數據可用,該線程不會被阻塞,而是立即返回。這使得一個線程可以管理多個通道,大大提高了系統的并發處理能力。例如,在一個網絡服務器中,可以使用一個線程來監聽多個客戶端連接的事件,當有事件發生時,才對相應的連接進行處理,避免了線程的阻塞等待,提高了資源利用率。
- 面向緩沖區:NIO 基于緩沖區(Buffer)進行數據操作,所有數據都要先寫入緩沖區,然后再從緩沖區讀取。緩沖區是一塊連續的內存區域,提供了更靈活的數據處理方式,可以隨機訪問數據,并且支持數據的讀寫、標記、重置等操作。與面向流的 IO 相比,面向緩沖區的 NIO 在處理大數據量時效率更高,因為它減少了數據的拷貝次數。
- 選擇器:選擇器(Selector)是 NIO 的一個重要組件,它可以用于同時監控多個通道的讀寫事件,并在有事件發生時立即做出響應。通過選擇器,可以實現單線程監聽多個通道的效果,從而提高系統吞吐量和運行效率。例如,在一個網絡服務器中,可以將所有的客戶端連接通道注冊到一個選擇器上,選擇器不斷輪詢這些通道,當有通道準備好進行 I/O 操作時,選擇器就會通知相應的線程進行處理,避免了每個連接都需要一個線程來處理的情況,節省了系統資源。
1.3 NIO 核心組件
NIO 的核心組件包括通道(Channel)、緩沖區(Buffer)和選擇器(Selector),它們協同工作,實現了高效的 I/O 操作。
- Channel(通道):通道是連接數據源或目的地的雙向通道,可以進行讀、寫或同時進行讀寫操作。與傳統的流不同,通道支持異步操作,并且可以與多個緩沖區進行交互。常見的通道類型有 FileChannel(用于文件的讀寫操作)、SocketChannel(用于通過 TCP 協議進行網絡通信)、ServerSocketChannel(用于監聽客戶端的連接請求)和 DatagramChannel(用于通過 UDP 協議進行網絡通信)。例如,通過 FileChannel 可以實現對文件的高效讀寫,支持隨機訪問和內存映射等高級操作;通過 SocketChannel 可以實現非阻塞的網絡通信,提高網絡應用的并發性能。
- Buffer(緩沖區):緩沖區是 NIO 中數據的容器,用于存儲數據。所有數據的讀寫都要通過緩沖區進行,緩沖區提供了對數據的結構化訪問以及維護讀寫位置等信息。常用的緩沖區類型有 ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer 和 DoubleBuffer 等,分別用于存儲不同類型的數據。在使用緩沖區時,通常需要經歷分配 Buffer、寫入數據到 Buffer、切換 Buffer 為讀模式和從 Buffer 中讀取數據等步驟。例如,通過 ByteBuffer 的 allocate () 方法可以分配一個指定容量的緩沖區,然后使用 put () 方法將數據寫入緩沖區,再通過 flip () 方法將緩沖區從寫模式切換為讀模式,最后使用 get () 方法從緩沖區中讀取數據。
- Selector(選擇器):選擇器用于監聽多個通道的事件,如連接打開、數據到達等。一個選擇器可以注冊多個通道,當其中的某些通道上有感興趣的事件發生時,這些通道就會變為可用狀態,可以在選擇器的選擇操作中被選中。通過選擇器,一個線程可以管理多個通道,實現非阻塞的 I/O 操作,提高系統的并發處理能力。使用選擇器的基本流程包括創建 Selector、將通道注冊到 Selector 上并指定感興趣的事件類型,以及不斷循環地調用 Selector 的 select () 方法來檢查是否有通道已經準備好進行 I/O 操作,最后處理準備就緒的通道。
1.4 NIO 適用場景
NIO 的特性使其在以下場景中具有明顯的優勢:
- 高并發網絡應用:在開發高性能的網絡服務器或客戶端時,NIO 可以處理大量并發連接,通過非阻塞 I/O 和選擇器機制,提高系統的并發處理能力和資源利用率。例如,常見的網絡通信框架如 Netty、Mina 等,都基于 NIO 實現,能夠支持海量的并發連接,廣泛應用于互聯網、游戲、金融等領域。
- 大數據傳輸:NIO 提供的通道和緩沖區概念,可以高效地進行大規模數據的傳輸。在處理大文件讀寫時,通過 FileChannel 和 ByteBuffer 的配合,可以減少 I/O 操作的次數,提高數據傳輸的效率。例如,在進行文件的拷貝、備份等操作時,使用 NIO 可以大大縮短操作時間。
- 需要高效利用系統資源的場景:由于 NIO 可以使用較少的線程來處理大量的 I/O 操作,減少了線程上下文切換的開銷,因此在對系統資源利用率要求較高的場景中,NIO 是更好的選擇。例如,在一些嵌入式系統或資源受限的環境中,使用 NIO 可以在有限的資源條件下實現高效的 I/O 處理。
二、NIO 核心組件實戰
了解了 NIO 的基本概念和原理后,接下來我們通過實際的代碼示例來深入學習 NIO 的核心組件 —— 緩沖區(Buffer)、通道(Channel)和選擇器(Selector)的使用。
2.1 Buffer 緩沖區
在 NIO 中,Buffer 是一個用于存儲數據的容器,所有數據的讀寫都要通過緩沖區進行。常見的緩沖區類型有 ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer 和 DoubleBuffer 等,分別用于存儲不同類型的數據。下面以 ByteBuffer 為例,介紹緩沖區的創建、讀寫操作以及 flip 和 clear 方法的使用。
創建緩沖區:
可以使用靜態方法 allocate 來創建一個指定容量的緩沖區。例如,創建一個容量為 1024 字節的 ByteBuffer:
ByteBuffer buffer = ByteBuffer.allocate(1024);
也可以通過 wrap 方法將一個現有的數組包裝成緩沖區:
byte[] array = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(array);
寫入數據:
使用 put 方法將數據寫入緩沖區。例如,將一個字符串寫入 ByteBuffer:
String message = "Hello, NIO!";
buffer.put(message.getBytes());
也可以從通道中讀取數據到緩沖區,假設我們有一個 FileChannel:
FileInputStream fis = new FileInputStream("example.txt");
FileChannel channel = fis.getChannel();
channel.read(buffer);
讀取數據:
在讀取數據之前,需要先調用 flip 方法將緩沖區從寫模式切換為讀模式。flip 方法會將 position 設置為 0,并將 limit 設置為當前 position 的值,這樣就可以從緩沖區的開頭開始讀取數據了。例如:
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String result = new String(data);
System.out.println(result);
flip 與 clear 方法:
- flip 方法:如上述所說,用于將緩沖區從寫模式切換為讀模式。
- clear 方法:用于清空緩沖區,將 position 設置為 0,limit 設置為容量大小。但需要注意的是,clear 方法并不會真正刪除緩沖區中的數據,只是重置了緩沖區的狀態,以便重新寫入數據。例如:
buffer.clear();
通過以下完整代碼示例,可以更清晰地看到緩沖區的工作原理:
import java.nio.ByteBuffer;public class BufferExample {public static void main(String[] args) {// 創建緩沖區ByteBuffer buffer = ByteBuffer.allocate(1024);// 寫入數據String message = "Hello, NIO!";buffer.put(message.getBytes());// 切換為讀模式buffer.flip();// 讀取數據byte[] data = new byte[buffer.remaining()];buffer.get(data);String result = new String(data);System.out.println(result);// 清空緩沖區buffer.clear();}
}
2.2 Channel 通道
Channel 是 NIO 中用于與數據源或目的地進行數據傳輸的通道,它可以進行讀、寫或同時進行讀寫操作。常見的通道類型有 FileChannel(用于文件的讀寫操作)、SocketChannel(用于通過 TCP 協議進行網絡通信)、ServerSocketChannel(用于監聽客戶端的連接請求)和 DatagramChannel(用于通過 UDP 協議進行網絡通信)。下面分別介紹 FileChannel 和 SocketChannel 的使用。
FileChannel 文件操作:
FileChannel 主要用于文件的讀寫操作,它可以實現對文件的高效讀寫,支持隨機訪問和內存映射等高級操作。以下是使用 FileChannel 讀取和寫入文件的示例:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;public class FileChannelExample {public static void main(String[] args) {try (FileInputStream fis = new FileInputStream("input.txt");FileOutputStream fos = new FileOutputStream("output.txt");FileChannel inChannel = fis.getChannel();FileChannel outChannel = fos.getChannel()) {// 創建緩沖區ByteBuffer buffer = ByteBuffer.allocate(1024);// 從輸入通道讀取數據到緩沖區while (inChannel.read(buffer) != -1) {// 切換緩沖區為讀模式buffer.flip();// 從緩沖區寫入數據到輸出通道outChannel.write(buffer);// 清空緩沖區,為下一次讀取做準備buffer.clear();}System.out.println("文件復制完成!");} catch (Exception e) {e.printStackTrace();}}
}
SocketChannel 網絡操作:
SocketChannel 用于通過 TCP 協議進行網絡通信,可以實現非阻塞的網絡通信,提高網絡應用的并發性能。以下是一個簡單的 SocketChannel 客戶端和服務器端示例:
服務器端:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;public class NioServer {public static void main(String[] args) {try {// 創建選擇器Selector selector = Selector.open();// 創建服務器套接字通道ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.socket().bind(new InetSocketAddress(8080));// 將服務器通道注冊到選擇器上,監聽連接事件serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("服務器啟動,監聽端口8080...");while (true) {// 阻塞直到有事件發生selector.select();Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();keyIterator.remove();if (key.isAcceptable()) {// 處理新連接ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();SocketChannel clientChannel = serverChannel.accept();clientChannel.configureBlocking(false);clientChannel.register(selector, SelectionKey.OP_READ);System.out.println("新客戶端連接: " + clientChannel.getRemoteAddress());} else if (key.isReadable()) {// 處理讀事件SocketChannel clientChannel = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int bytesRead = clientChannel.read(buffer);if (bytesRead > 0) {buffer.flip();byte[] data = new byte[buffer.remaining()];buffer.get(data);String message = new String(data);System.out.println("收到客戶端消息: " + message);// 回顯消息給客戶端ByteBuffer outBuffer = ByteBuffer.wrap(("服務器已收到消息: " + message).getBytes());clientChannel.write(outBuffer);} else if (bytesRead == -1) {// 客戶端關閉連接clientChannel.close();System.out.println("客戶端斷開連接: " + clientChannel.getRemoteAddress());}}}}} catch (IOException e) {e.printStackTrace();}}
}
客戶端:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;public class NioClient {public static void main(String[] args) {try {// 創建SocketChannelSocketChannel clientChannel = SocketChannel.open();clientChannel.configureBlocking(false);// 連接服務器clientChannel.connect(new InetSocketAddress("localhost", 8080));// 等待連接完成while (!clientChannel.finishConnect()) {// 可以做點別的事,或者稍微等等}System.out.println("已連接到服務器");// 發送消息String message = "你好,服務器!";ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());clientChannel.write(buffer);// 接收服務器回顯消息buffer.clear();int bytesRead = clientChannel.read(buffer);if (bytesRead > 0) {buffer.flip();byte[] data = new byte[buffer.remaining()];buffer.get(data);String response = new String(data);System.out.println("收到服務器回顯: " + response);}// 關閉通道clientChannel.close();} catch (IOException e) {e.printStackTrace();}}
}
2.3 Selector 選擇器
Selector 是 NIO 中的一個重要組件,它可以用于同時監控多個通道的讀寫事件,并在有事件發生時立即做出響應。通過選擇器,可以實現單線程監聽多個通道的效果,從而提高系統吞吐量和運行效率。以下是使用 Selector 的基本步驟和代碼示例:
注冊通道:
首先需要創建一個 Selector,然后將通道注冊到 Selector 上,并指定感興趣的事件類型。例如,將 ServerSocketChannel 注冊到 Selector 上,監聽連接事件:
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
監聽事件:
使用 selector 的 select 方法來監聽注冊通道上的事件。select 方法會阻塞,直到有感興趣的事件發生。例如:
int readyChannels = selector.select();
if (readyChannels > 0) {// 處理就緒事件
}
處理就緒事件:
通過 selectedKeys 方法獲取已就緒的 SelectionKey 集合,然后遍歷該集合,根據不同的事件類型處理相應的通道。例如:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();keyIterator.remove();if (key.isAcceptable()) {// 處理新連接ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();SocketChannel clientChannel = serverChannel.accept();clientChannel.configureBlocking(false);clientChannel.register(selector, SelectionKey.OP_READ);System.out.println("新客戶端連接: " + clientChannel.getRemoteAddress());} else if (key.isReadable()) {// 處理讀事件SocketChannel clientChannel = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int bytesRead = clientChannel.read(buffer);if (bytesRead > 0) {buffer.flip();byte[] data = new byte[buffer.remaining()];buffer.get(data);String message = new String(data);System.out.println("收到客戶端消息: " + message);// 回顯消息給客戶端ByteBuffer outBuffer = ByteBuffer.wrap(("服務器已收到消息: " + message).getBytes());clientChannel.write(outBuffer);} else if (bytesRead == -1) {// 客戶端關閉連接clientChannel.close();System.out.println("客戶端斷開連接: " + clientChannel.getRemoteAddress());}}
}
完整的服務器端代碼示例(包含 Selector 的使用):
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;public class SelectorExample {public static void main(String[] args) {try {// 創建選擇器Selector selector = Selector.open();// 創建服務器套接字通道ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.socket().bind(new InetSocketAddress(8080));// 將服務器通道注冊到選擇器上,監聽連接事件serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("服務器啟動,監聽端口8080...");while (true) {// 阻塞直到有事件發生selector.select();Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();keyIterator.remove();if (key.isAcceptable()) {// 處理新連接ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();SocketChannel clientChannel = serverChannel.accept();clientChannel.configureBlocking(false);clientChannel.register(selector, SelectionKey.OP_READ);System.out.println("新客戶端連接: " + clientChannel.getRemoteAddress());} else if (key.isReadable()) {// 處理讀事件SocketChannel clientChannel = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int bytesRead = clientChannel.read(buffer);if (bytesRead > 0) {buffer.flip();byte[] data = new byte[buffer.remaining()];buffer.get(data);String message = new String(data);System.out.println("收到客戶端消息: " + message);// 回顯消息給客戶端ByteBuffer outBuffer = ByteBuffer.wrap(("服務器已收到消息: " + message).getBytes());clientChannel.write(outBuffer);} else if (bytesRead == -1) {// 客戶端關閉連接clientChannel.close();System.out.println("客戶端斷開連接: " + clientChannel.getRemoteAddress());}}}}} catch (IOException e) {e.printStackTrace();}}
}
2.4 NIO 文件操作案例
為了更直觀地展示 NIO 在文件操作中的優勢,我們通過一個大文件高效讀寫的案例來進行說明。假設我們有一個大小為 1GB 的大文件,需要將其讀取并復制到另一個文件中,對比傳統 IO 和 NIO 的實現方式和性能表現。
傳統 IO 實現:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;public class TraditionalIoExample {public static void main(String[] args) {long startTime = System.currentTimeMillis();try (FileInputStream fis = new FileInputStream("largeFile.txt");FileOutputStream fos = new FileOutputStream("copy_largeFile.txt")) {byte[] buffer = new byte[1024];int length;while ((length = fis.read(buffer)) != -1) {fos.write(buffer, 0, length);}} catch (IOException e) {e.printStackTrace();}long endTime = System.currentTimeMillis();System.out.println("傳統IO復制文件耗時: " + (endTime - startTime) + " 毫秒");}
}
NIO 實現:
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;public class NioFileExample {public static void main(String[] args) {long startTime = System.currentTimeMillis();try (FileChannel inChannel = FileChannel.open(Paths.get("largeFile.txt"), StandardOpenOption.READ);FileChannel outChannel = FileChannel.open(Paths.get("copy_largeFile.txt"), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); // 1MB緩沖區while (inChannel.read(buffer) != -1) {buffer.flip();outChannel.write(buffer);buffer.clear();}} catch (IOException e) {e.printStackTrace();}long endTime = System.currentTimeMillis();System.out.println("NIO復制文件耗時: " + (endTime - startTime) + " 毫秒");}
}
通過實際測試可以發現,NIO 在處理大文件讀寫時,由于其采用了緩沖區和通道的機制,減少了系統調用次數和數據拷貝次數,相比傳統 IO 具有更高的效率。在上述案例中,NIO 復制文件的耗時通常會明顯低于傳統 IO,尤其是在處理超大文件時,這種優勢更加顯著。這是因為 NIO 的緩沖區可以一次性讀取和寫入大量數據,減少了 I/O 操作的頻率,從而提高了文件讀寫的性能。
三、NIO2.0 實戰
Java NIO2.0 是 Java 7 引入的一組增強的 I/O API,它在 NIO 的基礎上提供了更強大、更便捷的文件和目錄操作功能。NIO2.0 引入了新的類和接口,如 Path、Files 和 WatchService 等,使得文件系統的操作更加靈活和高效。接下來,我們將深入探討 NIO2.0 中 Path 類和 Files 類的使用,并通過實戰案例展示其強大功能。
3.1 Path 類
在 Java NIO2.0 中,Path 類用于表示文件系統中的路徑,它是一個平臺無關的抽象路徑。Path 接口提供了一系列方法來操作路徑,包括獲取路徑的各個部分、解析路徑、規范化路徑等。
路徑表示:
可以使用 Paths 類的 get 方法來創建 Path 對象。例如,創建一個表示文件路徑的 Path 對象:
import java.nio.file.Path;
import java.nio.file.Paths;public class PathExample {public static void main(String[] args) {// 創建Path對象Path path = Paths.get("C:/Users/Username/Documents/example.txt");// 獲取文件名System.out.println("文件名: " + path.getFileName());// 獲取父路徑System.out.println("父路徑: " + path.getParent());// 獲取根路徑System.out.println("根路徑: " + path.getRoot());}
}
路徑操作:
Path 接口提供了豐富的方法來操作路徑,如拼接路徑、規范化路徑等。
- 拼接路徑:使用 resolve 方法可以將兩個路徑拼接在一起。例如:
Path path1 = Paths.get("C:/Users/Username");
Path path2 = path1.resolve("Documents/example.txt");
System.out.println("拼接后的路徑: " + path2);
- 規范化路徑:使用 normalize 方法可以消除路徑中的冗余部分,如 “./” 和 “…/”。例如:
Path unnormalizedPath = Paths.get("C:/Users/Username/../Documents/./example.txt");
Path normalizedPath = unnormalizedPath.normalize();
System.out.println("規范化后的路徑: " + normalizedPath);
3.2 Files 類
Files 類是 NIO2.0 中用于文件操作的核心類,它提供了大量的靜態方法來執行文件的創建、刪除、復制、移動、讀取和寫入等操作。
文件創建:
使用 createFile 方法可以創建一個新文件。例如:
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;public class FilesExample {public static void main(String[] args) {Path filePath = Paths.get("C:/Users/Username/newfile.txt");try {Files.createFile(filePath);System.out.println("文件創建成功");} catch (IOException e) {e.printStackTrace();}}
}
文件刪除:
使用 delete 方法可以刪除文件或空目錄。如果要刪除的文件或目錄不存在,會拋出 NoSuchFileException 異常;如果要刪除的目錄非空,會拋出 DirectoryNotEmptyException 異常。例如:
Path filePath = Paths.get("C:/Users/Username/newfile.txt");
try {Files.delete(filePath);System.out.println("文件刪除成功");
} catch (IOException e) {e.printStackTrace();
}
文件復制:
使用 copy 方法可以復制文件。如果目標文件已存在,會拋出 FileAlreadyExistsException 異常。可以通過 StandardCopyOption.REPLACE_EXISTING 選項來覆蓋已存在的文件。例如:
Path sourcePath = Paths.get("C:/Users/Username/source.txt");
Path targetPath = Paths.get("C:/Users/Username/target.txt");
try {Files.copy(sourcePath, targetPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);System.out.println("文件復制成功");
} catch (IOException e) {e.printStackTrace();
}
文件讀取:
使用 readAllBytes 方法可以讀取文件的所有字節內容。例如:
Path filePath = Paths.get("C:/Users/Username/data.txt");
try {byte[] content = Files.readAllBytes(filePath);String data = new String(content);System.out.println("文件內容: " + data);
} catch (IOException e) {e.printStackTrace();
}
3.3 Files 類高級操作
除了基本的文件操作,Files 類還提供了一些高級操作方法,如獲取文件屬性和監聽文件變化。
文件屬性獲取:
可以使用 Files 類的 getAttribute 方法獲取文件的各種屬性,如文件大小、修改時間、創建時間等。例如,獲取文件的大小和修改時間:
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;public class FileAttributesExample {public static void main(String[] args) {Path filePath = Paths.get("C:/Users/Username/data.txt");try {BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);System.out.println("文件大小: " + attrs.size() + " 字節");System.out.println("修改時間: " + attrs.lastModifiedTime());System.out.println("創建時間: " + attrs.creationTime());} catch (IOException e) {e.printStackTrace();}}
}
文件監聽:
NIO2.0 引入了 WatchService 來監聽文件系統的變化,如文件的創建、修改和刪除等。以下是一個簡單的示例,展示如何監聽指定目錄下的文件變化:
import java.nio.file.*;
import java.io.IOException;public class FileWatcherExample {public static void main(String[] args) {try {// 創建WatchServiceWatchService watchService = FileSystems.getDefault().newWatchService();// 注冊要監聽的目錄Path directory = Paths.get("C:/Users/Username/Documents");directory.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);System.out.println("開始監聽目錄: " + directory);while (true) {// 等待事件發生WatchKey key = watchService.take();for (WatchEvent<?> event : key.pollEvents()) {WatchEvent.Kind<?> kind = event.kind();@SuppressWarnings("unchecked")WatchEvent<Path> ev = (WatchEvent<Path>) event;Path fileName = ev.context();System.out.println("事件類型: " + kind + ", 文件: " + fileName);}// 重置WatchKeyboolean valid = key.reset();if (!valid) {break;}}} catch (IOException | InterruptedException e) {e.printStackTrace();}}
}
3.4 NIO2.0 實戰案例
為了更好地展示 NIO2.0 的強大功能,我們通過一個目錄遍歷和文件篩選的案例來進行實踐。假設我們需要遍歷指定目錄及其子目錄,篩選出所有的 Java 源文件,并打印出它們的路徑。
import java.nio.file.*;
import java.io.IOException;public class DirectoryTraversalExample {public static void main(String[] args) {Path directory = Paths.get("C:/Users/Username/Projects");try {Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {if (file.getFileName().toString().endsWith(".java")) {System.out.println("找到Java源文件: " + file);}return FileVisitResult.CONTINUE;}});} catch (IOException e) {e.printStackTrace();}}
}
在上述代碼中,我們使用 Files 類的 walkFileTree 方法來遍歷指定目錄及其子目錄。walkFileTree 方法接受兩個參數,一個是要遍歷的起始目錄,另一個是實現了 FileVisitor 接口的對象。我們通過繼承 SimpleFileVisitor 類(它是 FileVisitor 接口的一個適配器類,提供了默認的實現),并重寫 visitFile 方法來實現文件篩選邏輯。在 visitFile 方法中,我們檢查文件的擴展名是否為 “.java”,如果是,則打印出文件的路徑。通過這個案例,我們可以看到 NIO2.0 提供的文件和目錄操作功能非常強大和靈活,能夠輕松應對各種復雜的文件處理需求。