Tại sao Java IO quá tệ?

API IO (Đầu vào & Đầu ra) là một API Java giúp các nhà phát triển dễ dàng làm việc với các luồng. Giả sử chúng ta nhận được một số dữ liệu (ví dụ: tên, tên đệm, họ) và chúng ta cần ghi dữ liệu đó vào một tệp — đã đến lúc sử dụng java.io .

Cấu trúc của thư viện java.io

Nhưng Java IO có những nhược điểm của nó, vì vậy hãy lần lượt nói về từng vấn đề:

  1. Chặn truy cập cho đầu vào/đầu ra. Vấn đề là khi nhà phát triển cố gắng đọc hoặc ghi nội dung nào đó vào tệp bằng Java IO , nó sẽ khóa tệp và chặn quyền truy cập vào tệp đó cho đến khi hoàn thành công việc.
  2. Không hỗ trợ hệ thống tập tin ảo.
  3. Không hỗ trợ cho các liên kết.
  4. Rất nhiều và rất nhiều ngoại lệ được kiểm tra.

Làm việc với các tệp luôn đòi hỏi phải làm việc với các ngoại lệ: ví dụ: cố gắng tạo một tệp mới đã tồn tại sẽ đưa ra IOException . Trong trường hợp này, ứng dụng sẽ tiếp tục chạy và người dùng sẽ được thông báo lý do không thể tạo tệp.


try {
	File.createTempFile("prefix", "");
} catch (IOException e) {
	// Handle the IOException
}

/**
 * Creates an empty file in the default temporary-file directory 
 * any exceptions will be ignored. This is typically used in finally blocks. 
 * @param prefix 
 * @param suffix 
 * @throws IOException - If a file could not be created
 */
public static File createTempFile(String prefix, String suffix) 
throws IOException {
...
}

Ở đây chúng ta thấy rằng phương thức createTempFile đưa ra một IOException khi không thể tạo tệp. Ngoại lệ này phải được xử lý thích hợp. Nếu chúng ta cố gắng gọi phương thức này bên ngoài khối try-catch , trình biên dịch sẽ tạo ra lỗi và đề xuất hai tùy chọn để sửa nó: bọc phương thức trong khối try-catch hoặc làm cho phương thức gọi File.createTempFile ném một IOException ( để nó có thể được xử lý ở mức cao hơn).

Đến Java NIO và cách so sánh với Java IO

Java NIO hoặc Java Non-Blocking I/O (hoặc đôi khi là Java New I/O) được thiết kế cho các hoạt động I/O hiệu suất cao.

Hãy so sánh các phương thức Java IO và những phương thức thay thế chúng.

Đầu tiên, hãy nói về cách làm việc với Java IO :

lớp InputStream


try(FileInputStream fin = new FileInputStream("C:/codegym/file.txt")){
    System.out.printf("File size: %d bytes \n", fin.available());
    int i=-1;
    while((i=fin.read())!=-1) {
        System.out.print((char)i);
    }   
} catch(IOException ex) {
    System.out.println(ex.getMessage());
}

Lớp FileInputStream dùng để đọc dữ liệu từ một tệp . Nó kế thừa lớp InputStream và do đó thực hiện tất cả các phương thức của nó. Nếu tệp không thể mở được, FileNotFoundException sẽ được ném ra.

lớp OutputStream


String text = "Hello world!"; // String to write
try(FileOutputStream fos = new FileOutputStream("C:/codegym/file.txt")){
    // Convert our string into bytes
    byte[] buffer = text.getBytes();
    fos.write(buffer, 0, buffer.length);
    System.out.println("The file has been written");
} catch(IOException ex) {
    System.out.println(ex.getMessage());
}

Lớp FileOutputStream để ghi byte vào tệp. Nó bắt nguồn từ lớp OutputStream .

Các lớp đọc và viết

Lớp FileReader cho phép chúng ta đọc dữ liệu ký tự từ các luồng và lớp FileWriter được sử dụng để ghi các luồng ký tự. Đoạn mã sau chỉ ra cách viết và đọc từ một tệp:


        String fileName = "c:/codegym/Example.txt";

        // Create a FileWriter object
        try (FileWriter writer = new FileWriter(fileName)) {

            // Write content to file
            writer.write("This is a simple example\nin which we\nwrite to a file\nand read from a file\n");
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Create a FileReader object
        try (FileReader fr = new FileReader(fileName)) {
            char[] a = new char[200]; // Number of characters to read
            fr.read(a); // Read content into an array
            for (char c : a) {
                System.out.print(c); // Display characters one by one
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

Bây giờ hãy nói về Java NIO :

Kênh

Không giống như các luồng được sử dụng trong Java IO , Kênh là giao diện hai chiều, nghĩa là nó có thể vừa đọc vừa ghi. Kênh Java NIO hỗ trợ luồng dữ liệu không đồng bộ ở cả chế độ chặn và không chặn.


RandomAccessFile aFile = new RandomAccessFile("C:/codegym/file.txt", "rw");
FileChannel inChannel = aFile.getChannel();

ByteBuffer buf = ByteBuffer.allocate(100);
int bytesRead = inChannel.read(buf);

while (bytesRead != -1) {
  System.out.println("Read: " + bytesRead);
  buf.flip();
	  while(buf.hasRemaining()) {
	      System.out.print((char) buf.get());
	  }
  buf.clear();
  bytesRead = inChannel.read(buf);
}
aFile.close();

Ở đây chúng tôi đã sử dụng FileChannel . Chúng tôi sử dụng một kênh tệp để đọc dữ liệu từ một tệp. Chỉ có thể tạo đối tượng kênh tệp bằng cách gọi phương thức getChannel() trên đối tượng tệp — không có cách nào để tạo trực tiếp đối tượng kênh tệp.

Ngoài FileChannel , chúng tôi có các triển khai kênh khác:

  • FileChannel — để làm việc với tệp

  • DatagramChannel - một kênh để làm việc qua kết nối UDP

  • SocketChannel — một kênh để làm việc qua kết nối TCP

  • ServerSocketChannel chứa một SocketChannel và tương tự như cách máy chủ web hoạt động

Xin lưu ý: Không thể chuyển FileChannel sang chế độ không chặn. Chế độ không chặn của Java NIO cho phép bạn yêu cầu đọc dữ liệu từ một kênh và chỉ nhận những gì hiện có (hoặc không có gì nếu chưa có dữ liệu). Điều đó nói rằng, SelectableChannel và các triển khai của nó có thể được đặt ở chế độ không chặn bằng phương thức connect() .

Bộ chọn

Java NIO đã giới thiệu khả năng tạo một luồng biết kênh nào sẵn sàng ghi và đọc dữ liệu và có thể xử lý kênh cụ thể đó. Khả năng này được thực hiện bằng cách sử dụng lớp Selector .

Kết nối các kênh với bộ chọn


Selector selector = Selector.open();
channel.configureBlocking(false); // Non-blocking mode
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

Vì vậy, chúng tôi tạo Bộ chọn của mình và kết nối nó với SelectableChannel .

Để được sử dụng với bộ chọn, kênh phải ở chế độ không chặn. Điều này có nghĩa là bạn không thể sử dụng FileChannel với bộ chọn, vì FileChannel không thể được đưa vào chế độ không chặn. Nhưng các kênh ổ cắm sẽ hoạt động tốt.

Ở đây, hãy đề cập rằng trong ví dụ của chúng tôi, SelectionKey là một tập hợp các thao tác có thể được thực hiện trên một kênh. Phím chọn cho chúng ta biết trạng thái của một kênh.

Các loại khóa lựa chọn

  • SelectionKey.OP_CONNECT biểu thị một kênh sẵn sàng kết nối với máy chủ.

  • SelectionKey.OP_ACCCEPT là một kênh sẵn sàng chấp nhận các kết nối đến.

  • SelectionKey.OP_READ biểu thị một kênh đã sẵn sàng để đọc dữ liệu.

  • SelectionKey.OP_WRITE biểu thị một kênh sẵn sàng ghi dữ liệu.

Đệm

Dữ liệu được đọc vào bộ đệm để xử lý thêm. Nhà phát triển có thể di chuyển qua lại trên bộ đệm, điều này giúp chúng tôi linh hoạt hơn một chút khi xử lý dữ liệu. Đồng thời, chúng ta cần kiểm tra xem bộ đệm có chứa lượng dữ liệu cần thiết để xử lý chính xác hay không. Ngoài ra, khi đọc dữ liệu vào bộ đệm, hãy chắc chắn rằng bạn không hủy dữ liệu hiện có chưa được xử lý.


ByteBuffer buf = ByteBuffer.allocate (2048); 
int bytesRead = channel.read(buf);
buf.flip(); // Change to read mode
while (buf.hasRemaining()) { 
	byte data = buf.get(); // There are methods for primitives 
}

buf.clear(); // Clear the buffer - now it can be reused

Các thuộc tính cơ bản của bộ đệm:

thuộc tính cơ bản
dung tích Kích thước bộ đệm, là độ dài của mảng.
chức vụ Vị trí bắt đầu để làm việc với dữ liệu.
giới hạn Giới hạn hoạt động. Đối với thao tác đọc, giới hạn là lượng dữ liệu có thể đọc, nhưng đối với thao tác ghi, đó là dung lượng hoặc hạn ngạch có sẵn để ghi.
đánh dấu Chỉ mục của giá trị mà tham số vị trí sẽ được đặt lại khi phương thức reset() được gọi.

Bây giờ hãy nói một chút về tính năng mới trong Java NIO.2 .

Con đường

Đường dẫn đại diện cho một đường dẫn trong hệ thống tệp. Nó chứa tên của một tập tin và một danh sách các thư mục xác định đường dẫn đến nó.


Path relative = Paths.get("Main.java");
System.out.println("File: " + relative);
// Get the file system
System.out.println(relative.getFileSystem());

Đường dẫn là một lớp rất đơn giản với một phương thức tĩnh duy nhất: get() . Nó được tạo chỉ để lấy đối tượng Đường dẫn từ chuỗi hoặc URI đã truyền.


Path path = Paths.get("c:\\data\\file.txt");

Các tập tin

Tệp là một lớp tiện ích cho phép chúng tôi lấy trực tiếp kích thước của tệp, sao chép tệp, v.v.


Path path = Paths.get("files/file.txt");
boolean pathExists = Files.exists(path);

Hệ thống tập tin

FileSystem cung cấp giao diện cho hệ thống tệp. Hệ thống tệp hoạt động giống như một nhà máy để tạo các đối tượng khác nhau (Con đường,PathMatch,Các tập tin). Nó giúp chúng tôi truy cập các tệp và các đối tượng khác trong hệ thống tệp.


try {
      FileSystem filesystem = FileSystems.getDefault();
      for (Path rootdir : filesystem.getRootDirectories()) {
          System.out.println(rootdir.toString());
      }
  } catch (Exception e) {
      e.printStackTrace();
  }

kiểm tra hiệu suất

Đối với thử nghiệm này, hãy lấy hai tệp. Đầu tiên là một tệp văn bản nhỏ và thứ hai là một video lớn.

Chúng tôi sẽ tạo một tệp và thêm một vài từ và ký tự:

% touch text.txt

Tệp của chúng tôi chiếm tổng cộng 42 byte trong bộ nhớ:

Bây giờ hãy viết mã sao chép tệp của chúng ta từ thư mục này sang thư mục khác. Hãy chạy thử trên file nhỏ và file lớn để so sánh tốc độ của IO với NIONIO.2 .

Mã để sao chép, được viết bằng Java IO :


public static void main(String[] args) {
        long currentMills = System.currentTimeMillis();
        long startMills = currentMills;
        File src = new File("/Users/IdeaProjects/testFolder/text.txt");
        File dst = new File("/Users/IdeaProjects/testFolder/text1.txt");
        copyFileByIO(src, dst);
        currentMills = System.currentTimeMillis();
        System.out.println("Execution time in milliseconds: " + (currentMills - startMills));
    }

    public static void copyFileByIO(File src, File dst) {
        try(InputStream inputStream = new FileInputStream(src);
            OutputStream outputStream = new FileOutputStream(dst)){

            byte[] buffer = new byte[1024];
            int length;
            // Read data into a byte array and then output to an OutputStream
            while((length = inputStream.read(buffer)) > 0) {
                outputStream.write(buffer, 0, length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Và đây là mã cho Java NIO :


public static void main(String[] args) {
        long currentMills = System.currentTimeMillis();
        long startMills = currentMills;

        File src = new File("/Users/IdeaProjects/testFolder/text.txt");
        File dst = new File("/Users/IdeaProjects/testFolder/text2.txt");
        // Code for copying using NIO
        copyFileByChannel(src, dst);
        currentMills = System.currentTimeMillis();
        System.out.println("Execution time in milliseconds: " + (currentMills - startMills));
    }

    public static void copyFileByChannel(File src, File dst) {
        // 1. Get a FileChannel for the source file and the target file
        try(FileChannel srcFileChannel  = new FileInputStream(src).getChannel();
            FileChannel dstFileChannel = new FileOutputStream(dst).getChannel()){
            // 2. Size of the current FileChannel
            long count = srcFileChannel.size();
            while(count > 0) {
                /**=============================================================
                 * 3. Write bytes from the source file's FileChannel to the target FileChannel
                 * 1. srcFileChannel.position(): the starting position in the source file, cannot be negative
                 * 2. count: the maximum number of bytes transferred, cannot be negative
                 * 3. dstFileChannel: the target file
                 *==============================================================*/
                long transferred = srcFileChannel.transferTo(srcFileChannel.position(),
                        count, dstFileChannel);
                // 4. After the transfer is complete, change the position of the original file to the new one
                srcFileChannel.position(srcFileChannel.position() + transferred);
                // 5. Calculate how many bytes are left to transfer
                count -= transferred;
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Mã cho Java NIO.2 :


public static void main(String[] args) {
  long currentMills = System.currentTimeMillis();
  long startMills = currentMills;

  Path sourceDirectory = Paths.get("/Users/IdeaProjects/testFolder/test.txt");
  Path targetDirectory = Paths.get("/Users/IdeaProjects/testFolder/test3.txt");
  Files.copy(sourceDirectory, targetDirectory);

  currentMills = System.currentTimeMillis();
  System.out.println("Execution time in milliseconds: " + (currentMills - startMills));
}

Hãy bắt đầu với tệp nhỏ.

Thời gian thực hiện cho Java IO trung bình là 1 mili giây. Bằng cách chạy thử nghiệm nhiều lần, chúng tôi nhận được kết quả từ 0 đến 2 mili giây.

Thời gian thực hiện tính bằng mili giây: 1

Thời gian thực thi Java NIO lâu hơn nhiều. Thời gian trung bình là 11 mili giây. Kết quả nằm trong khoảng từ 9 đến 16. Điều này là do Java IO hoạt động khác với hệ điều hành của chúng tôi. IO di chuyển và xử lý từng tệp một, nhưng hệ điều hành sẽ gửi dữ liệu trong một khối lớn. NIO hoạt động kém vì nó hướng vào bộ đệm, không hướng theo luồng như IO .

Thời gian thực hiện tính bằng mili giây: 12

Và hãy chạy thử nghiệm của chúng tôi cho Java NIO.2 . NIO.2 đã cải thiện việc quản lý tệp so với Java NIO . Đây là lý do tại sao thư viện được cập nhật tạo ra các kết quả khác nhau như vậy:

Thời gian thực hiện tính bằng mili giây: 3

Bây giờ, hãy thử kiểm tra tệp lớn của chúng tôi, một video 521 MB. Nhiệm vụ sẽ hoàn toàn giống nhau: sao chép tệp sang thư mục khác. Nhìn!

Kết quả cho Java IO :

Thời gian thực hiện tính bằng mili giây: 1866

Và đây là kết quả cho Java NIO :

Thời gian thực hiện tính bằng mili giây: 205

Java NIO đã xử lý tệp nhanh hơn 9 lần trong lần kiểm tra đầu tiên. Các thử nghiệm lặp đi lặp lại cho thấy kết quả gần giống nhau.

Và chúng tôi cũng sẽ thử kiểm tra trên Java NIO.2 :

Thời gian thực hiện tính bằng mili giây: 360

Tại sao lại có kết quả này? Đơn giản vì chúng tôi không có ý nghĩa gì khi so sánh hiệu suất giữa chúng, vì chúng phục vụ các mục đích khác nhau. NIO là I/O cấp thấp trừu tượng hơn, trong khi NIO.2 hướng tới quản lý tệp.

Bản tóm tắt

Chúng ta có thể nói rằng Java NIO hiệu quả hơn đáng kể khi làm việc với các tệp nhờ sử dụng bên trong các khối. Một điểm cộng nữa là thư viện NIO được chia thành hai phần: một phần để làm việc với tệp, phần còn lại để làm việc với mạng.

API mới của Java NIO.2 để làm việc với tệp cung cấp nhiều tính năng hữu ích:

  • địa chỉ hệ thống tệp hữu ích hơn nhiều bằng cách sử dụng Path ,

  • cải thiện đáng kể việc xử lý các tệp ZIP bằng nhà cung cấp hệ thống tệp tùy chỉnh,

  • truy cập vào các thuộc tính tập tin đặc biệt,

  • nhiều phương pháp thuận tiện, chẳng hạn như đọc toàn bộ tệp bằng một câu lệnh, sao chép tệp bằng một câu lệnh, v.v.

Đó là tất cả về tệp và hệ thống tệp, và tất cả đều ở mức khá cao.

Thực tế ngày nay là Java NIO chiếm khoảng 80-90% công việc với các tệp, mặc dù thị phần của Java IO vẫn còn đáng kể.

💡 Lưu ý: Các thử nghiệm này được chạy trên MacBook Pro 14" 16/512. Kết quả thử nghiệm có thể khác nhau tùy theo hệ điều hành và thông số kỹ thuật của máy trạm.