Trước đó, chúng ta đã biết về IO API (Giao diện lập trình ứng dụng đầu vào/đầu ra) và gói java.io , có các lớp chủ yếu để làm việc với các luồng trong Java. Chìa khóa ở đây là khái niệm về luồng .

Hôm nay chúng ta sẽ bắt đầu xem xét NIO API (Đầu vào/Đầu ra mới).

Sự khác biệt chính giữa hai cách tiếp cận I/O là IO API được định hướng theo luồng trong khi NIO API được định hướng theo bộ đệm. Vì vậy, các khái niệm chính cần hiểu là bộ đệmkênh .

Bộ đệm là gì và kênh là gì?

Kênh là một cổng logic mà qua đó dữ liệu di chuyển vào và ra, trong khi bộ đệm là nguồn hoặc đích của dữ liệu được truyền này . Trong quá trình xuất, dữ liệu bạn muốn gửi được đưa vào bộ đệm và bộ đệm chuyển dữ liệu tới kênh. Trong quá trình nhập, dữ liệu từ kênh được đưa vào bộ đệm.

Nói cách khác:

  • bộ đệm chỉ đơn giản là một khối bộ nhớ mà chúng ta có thể ghi thông tin vào đó và từ đó chúng ta có thể đọc thông tin,
  • kênh là một cổng cung cấp quyền truy cập vào các thiết bị I/O chẳng hạn như tệp hoặc ổ cắm .

Các kênh rất giống với các luồng trong gói java.io. Tất cả dữ liệu đi đến bất kỳ đâu (hoặc đến từ bất kỳ đâu) phải đi qua một đối tượng kênh. Nói chung, để sử dụng hệ thống NIO, bạn có một kênh tới một thực thể I/O và một bộ đệm để lưu trữ dữ liệu. Sau đó, bạn làm việc với bộ đệm, nhập hoặc xuất dữ liệu khi cần.

Bạn có thể tiến và lùi trong vùng đệm, tức là "đi bộ" vùng đệm, đây là điều bạn không thể thực hiện trong luồng. Điều này giúp linh hoạt hơn khi xử lý dữ liệu. Trong thư viện chuẩn, bộ đệm được đại diện bởi lớp Buffer trừu tượng và một số hậu duệ của nó:

  • Bộ đệm byte
  • CharBuffer
  • Bộ đệm ngắn
  • IntBuffer
  • Bộ đệm phao
  • Bộ đệm đôi
  • bộ đệm dài

Sự khác biệt chính giữa các lớp con là kiểu dữ liệu mà chúng lưu trữ — bytes , ints , longs và các kiểu dữ liệu nguyên thủy khác.

Thuộc tính bộ đệm

Một bộ đệm có bốn thuộc tính chính. Đó là khả năng, giới hạn, vị trí và nhãn hiệu.

Dung lượng là lượng dữ liệu/byte tối đa có thể được lưu trữ trong bộ đệm. Không thể thay đổi dung lượng của bộ đệm . Khi bộ đệm đầy, nó phải được xóa trước khi ghi thêm vào đó.

Ở chế độ ghi, giới hạn của bộ đệm giống như dung lượng của nó, cho biết lượng dữ liệu tối đa có thể được ghi vào bộ đệm. Ở chế độ đọc, giới hạn của bộ đệm đề cập đến lượng dữ liệu tối đa có thể được đọc từ bộ đệm.

Vị trí cho biết vị trí hiện tại của con trỏ trong bộ đệm. Ban đầu, nó được đặt thành 0 khi bộ đệm được tạo. Nói cách khác, nó là chỉ mục của phần tử tiếp theo được đọc hoặc viết.

Dấu được sử dụng để lưu vị trí con trỏ . Khi chúng ta thao tác với bộ đệm, vị trí con trỏ thay đổi liên tục, nhưng chúng ta luôn có thể đưa nó về vị trí đã đánh dấu trước đó.

Phương pháp làm việc với bộ đệm

Bây giờ hãy xem tập hợp các phương thức chính cho phép chúng ta làm việc với bộ đệm (khối bộ nhớ) để đọc và ghi dữ liệu đến và từ các kênh.

  1. phân bổ (int dung lượng) - phương pháp này được sử dụng để phân bổ bộ đệm mới với dung lượng được chỉ định. Phương thứcphân bổ() đưa ra một IllegalArgumentException nếu dung lượng đã truyền là một số nguyên âm.

  2. capacity() trả về dung lượng của bộ đệm hiện tại .

  3. position() trả về vị trí con trỏ hiện tại. Thao tác đọc ghi di chuyển con trỏ đến cuối vùng đệm. Giá trị trả về luôn nhỏ hơn hoặc bằng giới hạn.

  4. limit() trả về giới hạn của bộ đệm hiện tại.

  5. mark() được sử dụng để đánh dấu (lưu) vị trí con trỏ hiện tại.

  6. reset() đưa con trỏ về vị trí đã đánh dấu (đã lưu) trước đó.

  7. clear() đặt vị trí thành 0 và đặt giới hạn cho dung lượng. Phương pháp này không xóa dữ liệu trong bộ đệm. Nó chỉ khởi tạo lại vị trí, giới hạn và đánh dấu.

  8. flip() chuyển bộ đệm từ chế độ ghi sang chế độ đọc. Nó cũng đặt giới hạn cho vị trí hiện tại và sau đó đặt vị trí trở lại bằng không.

  9. read() — Phương thức read của kênh được sử dụng để ghi dữ liệu từ kênh vào bộ đệm, trong khi phương thức put() của bộ đệm được sử dụng để ghi dữ liệu vào bộ đệm.

  10. write() — Phương thức ghi của kênh được sử dụng để ghi dữ liệu từ bộ đệm vào kênh, trong khi phương thức get() của bộ đệm được sử dụng để đọc dữ liệu từ bộ đệm.

  11. tua lại() tua lại bộ đệm. Phương pháp này được sử dụng khi bạn cần đọc lại bộ đệm — nó đặt vị trí thành 0 và không thay đổi giới hạn.

Và bây giờ là một vài lời về các kênh.

Các triển khai kênh quan trọng nhất trong Java NIO là các lớp sau:

  1. FileChannel — Một kênh để đọc và ghi dữ liệu từ/đến một tệp.

  2. DatagramChannel — Lớp này đọc và ghi dữ liệu qua mạng thông qua UDP (Giao thức gói dữ liệu người dùng).

  3. SocketChannel — Một kênh để đọc và ghi dữ liệu qua mạng thông qua TCP (Giao thức điều khiển truyền dẫn).

  4. ServerSocketChannel — Một kênh để đọc và ghi dữ liệu qua các kết nối TCP, giống như một máy chủ web. Một SocketChannel được tạo cho mỗi kết nối đến.

Luyện tập

Đã đến lúc viết một vài dòng mã. Đầu tiên, hãy đọc tệp và hiển thị nội dung của nó trên bảng điều khiển, sau đó viết một số chuỗi vào tệp.

Đoạn mã chứa rất nhiều bình luận — tôi hy vọng chúng sẽ giúp bạn hiểu mọi thứ hoạt động như thế nào:


// Create a RandomAccessFile object, passing in the file path
// and a string that says the file will be opened for reading and writing
try (RandomAccessFile randomAccessFile = new RandomAccessFile("text.txt", "rw");
    // Get an instance of the FileChannel class
    FileChannel channel = randomAccessFile.getChannel();
) {
// Our file is small, so we'll read it in one go   
// Create a buffer of the required size based on the size of our channel
   ByteBuffer byteBuffer = ByteBuffer.allocate((int) channel.size());
   // Read data will be put into a StringBuilder
   StringBuilder builder = new StringBuilder();
   // Write data from the channel to the buffer
   channel.read(byteBuffer);
   // Switch the buffer from write mode to read mode
   byteBuffer.flip();
   // In a loop, write data from the buffer to the StringBuilder
   while (byteBuffer.hasRemaining()) {
       builder.append((char) byteBuffer.get());
   }
   // Display the contents of the StringBuilder on the console
   System.out.println(builder);
 
   // Now let's continue our program and write data from a string to the file
   // Create a string with arbitrary text
   String someText = "Hello, Amigo!!!!!";
   // Create a new buffer for writing,
   // but let the channel remain the same, because we're going to the same file
   // In other words, we can use one channel for both reading and writing to a file
   // Create a buffer specifically for our string — convert the string into an array and get its length
   ByteBuffer byteBuffer2 = ByteBuffer.allocate(someText.getBytes().length);
   // Write our string to the buffer
   byteBuffer2.put(someText.getBytes());
   // Switch the buffer from write mode to read mode
   // so that the channel can read from the buffer and write our string to the file
   byteBuffer2.flip();
   // The channel reads the information from the buffer and writes it to our file
   channel.write(byteBuffer2);
} catch (FileNotFoundException e) {
   e.printStackTrace();
} catch (IOException e) {
   e.printStackTrace();
}

Hãy dùng thử API NIO — bạn sẽ thích nó!