為什麼 Java IO 如此糟糕?

IO(輸入和輸出)API 是一種 Java API,它使開發人員可以輕鬆地使用流。假設我們收到了一些數據(例如,名字、中間名、姓氏),我們需要將其寫入文件——是時候使用 java.io

java.io 庫的結構

但是Java IO也有它的缺點,所以讓我們依次談談它們:

  1. 阻止對輸入/輸出的訪問。問題在於,當開發人員嘗試使用Java IO讀取文件或向文件寫入內容時,它會鎖定文件並阻止對其的訪問,直到工作完成。
  2. 不支持虛擬文件系統。
  3. 不支持鏈接。
  4. 很多很多檢查過的異常。

使用文件總是需要處理異常:例如,嘗試創建一個已經存在的新文件將拋出IOException。在這種情況下,應用程序應繼續運行,並且應通知用戶無法創建文件的原因。


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 {
...
}

在這裡我們看到createTempFile方法在無法創建文件時拋出IOException 。必須適當地處理此異常。如果我們嘗試在try-catch塊之外調用此方法,編譯器將生成錯誤並建議兩種修復方法:將方法包裝在try-catch塊中或使調用File.createTempFile的方法拋出IOException (所以它可以在更高級別處理)。

到達 Java NIO 以及它與 Java IO 的比較

Java NIO或 Java Non-Blocking I/O(有時稱為 Java New I/O)專為高性能 I/O 操作而設計。

讓我們比較Java IO方法和替代它們的方法。

首先,讓我們談談如何使用Java IO

輸入流類


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());
}

FileInputStream類用於從文件中讀取數據它繼承了InputStream類,因此實現了它的所有方法。如果無法打開文件,則會拋出FileNotFoundException 。

輸出流類


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());
}

用於將字節寫入文件的FileOutputStream類。它派生自OutputStream類。

讀者和作家類

FileReader類讓我們從流中讀取字符數據,FileWriter用於寫入字符流以下代碼顯示瞭如何寫入和讀取文件:


        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();
        }

現在讓我們談談Java NIO

渠道

與Java IO中使用的流不同,Channel是雙向接口,即既可以讀也可以寫。Java NIO通道支持阻塞和非阻塞模式下的異步數據流。


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();

這裡我們使用了FileChannel。我們使用文件通道從文件中讀取數據。文件通道對像只能通過調用文件對象的getChannel()方法來創建——沒有辦法直接創建文件通道對象。

除了FileChannel,我們還有其他通道實現:

  • FileChannel — 用於處理文件

  • DatagramChannel — 通過 UDP 連接工作的通道

  • SocketChannel — 通過 TCP 連接工作的通道

  • ServerSocketChannel包含一個SocketChannel並且類似於 Web 服務器的工作方式

請注意:FileChannel不能切換到非阻塞模式。Java NIO的非阻塞模式允許您請求從通道讀取數據並僅接收當前可用的數據(如果還沒有可用數據,則根本不接收任何數據)。也就是說,可以使用connect()方法將SelectableChannel及其實現置於非阻塞模式。

選擇器

Java NIO引入了創建線程的能力,該線程知道哪個通道已準備好寫入和讀取數據並可以處理該特定通道。此功能是使用Selector類實現的。

將通道連接到選擇器


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

因此,我們創建了Selector並將其連接到SelectableChannel

要與選擇器一起使用,通道必須處於非阻塞模式。這意味著您不能將FileChannel與選擇器一起使用,因為FileChannel不能置於非阻塞模式。但是套接字通道可以正常工作。

這裡提一下,在我們的示例中,SelectionKey是一組可以在通道上執行的操作。選擇鍵讓我們知道通道的狀態。

選擇鍵的類型

  • SelectionKey.OP_CONNECT表示準備好連接到服務器的通道。

  • SelectionKey.OP_ACCEPT是準備好接受傳入連接的通道。

  • SelectionKey.OP_READ表示準備好讀取數據的通道。

  • SelectionKey.OP_WRITE表示準備寫入數據的通道。

緩衝

數據被讀入緩衝區以供進一步處理。開發人員可以在緩衝區上來回移動,這讓我們在處理數據時更加靈活。同時,我們需要檢查緩衝區是否包含正確處理所需的數據量。此外,在將數據讀入緩衝區時,請確保不要破壞尚未處理的現有數據。


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

緩衝區的基本屬性:

基本屬性
容量 緩衝區大小,即數組的長度。
位置 處理數據的起始位置。
限制 經營限額。對於讀取操作,限制是可以讀取的數據量,但對於寫入操作,它是可用於寫入的容量或配額。
標記 調用reset()方法時位置參數將重置為的值的索引。

現在讓我們談談Java NIO.2中的新內容。

小路

Path表示文件系統中的路徑。它包含文件名和定義文件路徑的目錄列表。


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

Paths是一個非常簡單的類,只有一個靜態方法: get()。創建它只是為了從傳遞的字符串或 URI 中獲取Path對象。


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

文件

Files是一個實用類,可以讓我們直接獲取文件的大小、複製文件等。


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

文件系統

FileSystem提供文件系統的接口。文件系統就像一個工廠,用於創建各種對象(小路,路徑匹配器,文件). 它幫助我們訪問文件系統中的文件和其他對象。


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

性能測試

對於此測試,讓我們使用兩個文件。第一個是小文本文件,第二個是大視頻。

我們將創建一個文件並添加一些單詞和字符:

% 觸摸文本.txt

我們的文件一共佔用了42個字節的內存:

現在讓我們編寫代碼,將我們的文件從一個文件夾複製到另一個文件夾。讓我們在小文件和大文件上進行測試,以便比較IONIO以及NIO.2的速度。

用於復制的代碼,使用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();
        }
    }

這是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();
        }
    }

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));
}

讓我們從小文件開始。

Java IO的平均執行時間為 1 毫秒。通過多次運行測試,我們得到了 0 到 2 毫秒的結果。

以毫秒為單位的執行時間:1

Java NIO的執行時間要長得多。平均時間為 11 毫秒。結果介於 9 到 16 之間。這是因為Java IO 的工作方式與我們的操作系統不同。IO一個一個地移動和處理文件,但操作系統將數據一大塊發送。NIO表現不佳,因為它是面向緩衝區的,而不是像IO那樣面向流。

執行時間(以毫秒為單位):12

讓我們也為Java NIO.2運行我們的測試。與Java NIO相比, NIO.2改進了文件管理。這就是為什麼更新後的庫會產生如此不同的結果:

執行時間(以毫秒為單位):3

現在讓我們嘗試測試我們的大文件,一個 521 MB 的視頻。任務將完全相同:將文件複製到另一個文件夾。看!

Java IO的結果:

以毫秒為單位的執行時間:1866

這是Java NIO的結果:

執行時間(以毫秒為單位):205

Java NIO在第一次測試中處理文件的速度提高了 9 倍。重複測試顯示大致相同的結果。

我們還將嘗試在Java NIO.2上進行測試:

執行時間(以毫秒為單位):360

為什麼是這個結果?僅僅是因為我們比較它們之間的性能沒有多大意義,因為它們服務於不同的目的。NIO是更抽象的低級 I/O,而NIO.2則面向文件管理。

概括

我們可以有把握地說,由於在塊內部使用,Java NIO在處理文件時效率顯著提高。另一個優點是NIO庫分為兩部分:一部分用於處理文件,另一部分用於處理網絡。

Java NIO.2用於處理文件的新 API 提供了許多有用的功能:

  • 使用Path尋址更有用的文件系統,

  • 使用自定義文件系統提供程序顯著改進了對 ZIP 文件的處理,

  • 訪問特殊文件屬性,

  • 許多方便的方法,例如用一條語句讀取整個文件,用一條語句複製文件等。

這都是關於文件和文件系統的,而且都是相當高級的。

今天的現實是Java NIO大約占文件工作的 80-90%,儘管Java IO的份額仍然很大。

💡 PS 這些測試是在 MacBook Pro 14" 16/512 上運行的。測試結果可能因操作系統和工作站規格而異。