Java 大檔案IO操作效率對比【我說說 你瞅瞅】

·Bei發表於2024-08-14

Java 檔案IO操作效率對比

注:本文只做時間消耗層面對比,記憶體佔用層面需要特別關注!

引數說明

檔案總大小:2,111,993,850 位元組(2.11 GB)

static String defaultFilePath = "/tmp/data-24081412.json";

緩衝區大小:8192 位元組

static int defaultByteLength = 1024 * 8;

示例介紹

透過以下幾種方式讀取資料檔案,並連續進行 10 次測試:

  1. FileInputStream + byte[] 檔案位元組輸入流 + 位元組陣列讀取方式
  2. FileInputStream + Scanner 檔案位元組輸入流 + Scanner 讀取方式
  3. FileReader + char[] 檔案字元輸入流 + 字元陣列方式
  4. BufferedReader 緩衝字元流方式
  5. FileChannel 檔案輸入輸出管道流「NIO」

比對結果

「5. FileChannel」 > 「1. FileInputStream + byte[]」> 「3. FileReader + char[]」>= 「4. BufferedReader」 > 「2. FileInputStream + Scanner」

注: 在操作檔案時,會將檔案區分為大檔案、小檔案、文字檔案、二進位制檔案等,根據不同的檔案需要選擇合適的讀取方式。通常大檔案推薦使用 「FileChannel」效率會更高,小檔案採用 IO 或 NIO 都可以,文字檔案採用「BufferedReader」或者「FileChannel」判斷換行符。

示例程式碼

4.1. FileInputStream + byte[] 檔案位元組輸入流 + 位元組陣列讀取方式

/**
 * FileInputStream + byte[] 方式
 * 等同於 BufferedInputStream 位元組輸入緩衝流
 * 檔案位元組輸入流 + 位元組陣列讀取方式
 * 適用於:二進位制檔案或非文字檔案
 */
public void testFileInputStreamWithBytes() {
    long startTime = new Date().getTime();
    // 使用 try 包裝
    try (FileInputStream fileInputStream = new FileInputStream(defaultFilePath)){
        byte[] reads = new byte[defaultByteLength];
        int readCount;
        while ((readCount = fileInputStream.read(reads)) != -1) {
            // TODO 處理資料
        }
    } catch (IOException e) {
        System.out.printf("讀取檔案異常[ %s ]%n", e.getMessage());
    }

    System.out.printf("FileInputStream + byte[] 方式 讀取檔案共使用 %d 毫秒%n", new Date().getTime() - startTime);
}

10 次測試結果如下

FileInputStream + byte[] 方式 讀取檔案共使用 884 毫秒
FileInputStream + byte[] 方式 讀取檔案共使用 331 毫秒
FileInputStream + byte[] 方式 讀取檔案共使用 319 毫秒
FileInputStream + byte[] 方式 讀取檔案共使用 420 毫秒
FileInputStream + byte[] 方式 讀取檔案共使用 333 毫秒
FileInputStream + byte[] 方式 讀取檔案共使用 321 毫秒
FileInputStream + byte[] 方式 讀取檔案共使用 327 毫秒
FileInputStream + byte[] 方式 讀取檔案共使用 339 毫秒
FileInputStream + byte[] 方式 讀取檔案共使用 328 毫秒
FileInputStream + byte[] 方式 讀取檔案共使用 398 毫秒

4.2. FileInputStream + Scanner 檔案位元組輸入流 + Scanner 讀取方式

/**
 * FileInputStream + Scanner 方式
 * 檔案位元組輸入流 + Scanner 讀取文字方式
 * 適用於:文字檔案
 */
public void testFileInputStreamWithScanner() {
    long startTime = new Date().getTime();
    // 使用 try 包裝
    try (FileInputStream fileInputStream = new FileInputStream(defaultFilePath)){
        Scanner scanner = new Scanner(fileInputStream);
        while (scanner.hasNext()) {
            scanner.nextLine();
            // TODO 處理資料
        }
    } catch (IOException e) {
        System.out.printf("讀取檔案異常[ %s ]%n", e.getMessage());
    }

    System.out.printf("FileInputStream + Scanner 方式 讀取檔案共使用 %d 毫秒%n", new Date().getTime() - startTime);
}

10 次測試結果如下

沒有緩衝區,效能急劇下降!!
FileInputStream + Scanner 方式 讀取檔案共使用 16755 毫秒
FileInputStream + Scanner 方式 讀取檔案共使用 18744 毫秒
FileInputStream + Scanner 方式 讀取檔案共使用 17929 毫秒
FileInputStream + Scanner 方式 讀取檔案共使用 18640 毫秒
FileInputStream + Scanner 方式 讀取檔案共使用 18316 毫秒
FileInputStream + Scanner 方式 讀取檔案共使用 18015 毫秒
FileInputStream + Scanner 方式 讀取檔案共使用 18479 毫秒
FileInputStream + Scanner 方式 讀取檔案共使用 18755 毫秒
FileInputStream + Scanner 方式 讀取檔案共使用 18907 毫秒
FileInputStream + Scanner 方式 讀取檔案共使用 18783 毫秒

4.3. FileReader + char[] 檔案字元輸入流 + 字元陣列方式

/**
 * FileReader + char[] 方式
 * 等同於 BufferedReader 字元輸入緩衝流
 * 檔案字元輸入流 + 字元陣列方式
 * 適用於:字元檔案
 */
public void testFileReaderWithChars() {
    long startTime = new Date().getTime();
    // 使用 try 包裝
    try (FileReader fileReader = new FileReader(defaultFilePath)){
        char[] reads = new char[defaultByteLength];
        int readCount;
        while ((readCount = fileReader.read(reads)) != -1) {
            // TODO 處理資料
        }
    } catch (IOException e) {
        System.out.printf("讀取檔案異常[ %s ]%n", e.getMessage());
    }

    System.out.printf("FileReader + char[] 方式 讀取檔案共使用 %d 毫秒%n", new Date().getTime() - startTime);
}

10 次測試結果如下

FileReader + char[] 方式 讀取檔案共使用 922 毫秒
FileReader + char[] 方式 讀取檔案共使用 971 毫秒
FileReader + char[] 方式 讀取檔案共使用 842 毫秒
FileReader + char[] 方式 讀取檔案共使用 985 毫秒
FileReader + char[] 方式 讀取檔案共使用 868 毫秒
FileReader + char[] 方式 讀取檔案共使用 1207 毫秒
FileReader + char[] 方式 讀取檔案共使用 1031 毫秒
FileReader + char[] 方式 讀取檔案共使用 981 毫秒
FileReader + char[] 方式 讀取檔案共使用 1259 毫秒
FileReader + char[] 方式 讀取檔案共使用 1034 毫秒

4.4. BufferedReader 緩衝字元流方式

/**
 * BufferedReader 方式
 * 緩衝字元流方式
 * 適用於:字元檔案
 */
public void testBufferedReader() {
    long startTime = new Date().getTime();
    // 使用 try 包裝
    try (BufferedReader fileReader = new BufferedReader(new FileReader(defaultFilePath))){
        String line;
        while ((line = fileReader.readLine()) != null) {
            // TODO 處理資料
        }
    } catch (IOException e) {
        System.out.printf("讀取檔案異常[ %s ]%n", e.getMessage());
    }

    System.out.printf("BufferedReader 方式 讀取檔案共使用 %d 毫秒%n", new Date().getTime() - startTime);
}

10 次測試結果如下

BufferedReader 方式 讀取檔案共使用 1870 毫秒
BufferedReader 方式 讀取檔案共使用 1895 毫秒
BufferedReader 方式 讀取檔案共使用 1890 毫秒
BufferedReader 方式 讀取檔案共使用 1875 毫秒
BufferedReader 方式 讀取檔案共使用 1829 毫秒
BufferedReader 方式 讀取檔案共使用 2060 毫秒
BufferedReader 方式 讀取檔案共使用 1821 毫秒
BufferedReader 方式 讀取檔案共使用 1944 毫秒
BufferedReader 方式 讀取檔案共使用 1902 毫秒
BufferedReader 方式 讀取檔案共使用 1860 毫秒

4.5. FileChannel 檔案輸入輸出管道流「NIO」

/**
 * FileChannel 方式
 * 檔案輸入輸出管道流
 */
public void testFileChannel() {
    long startTime = new Date().getTime();
    // 使用 try 包裝
    try (FileChannel channel = FileChannel.open(Paths.get(defaultFilePath), StandardOpenOption.READ)){
        // 構造B yteBuffer 有兩個方法,ByteBuffer.allocate和 ByteBuffer.allocateDirect,兩個方法都是相同入參,含義卻不同。
        // ByteBuffer.allocate(capacity) 分配的是非直接緩衝區,非直接緩衝區的操作會在Java堆記憶體中進行,資料的讀寫會透過Java堆記憶體來傳遞。
        // ByteBuffer.allocateDirect(capacity) 分配的是直接緩衝區, 直接緩衝區的操作可以透過本地I/O傳遞,避免了在Java堆和本地堆之間的資料傳輸。
        ByteBuffer buf = ByteBuffer.allocate(defaultByteLength);
        while (channel.read(buf) != -1) {
            buf.flip();
            // TODO 處理資料
//                System.out.println(new String(buf.array()));
            buf.clear();
        }
    } catch (IOException e) {
        System.out.printf("讀取檔案異常[ %s ]%n", e.getMessage());
    }

    System.out.printf("FileChannel 方式 讀取檔案共使用 %d 毫秒%n", new Date().getTime() - startTime);
}

10 次測試結果如下

FileChannel 方式 讀取檔案共使用 314 毫秒
FileChannel 方式 讀取檔案共使用 293 毫秒
FileChannel 方式 讀取檔案共使用 332 毫秒
FileChannel 方式 讀取檔案共使用 296 毫秒
FileChannel 方式 讀取檔案共使用 285 毫秒
FileChannel 方式 讀取檔案共使用 290 毫秒
FileChannel 方式 讀取檔案共使用 283 毫秒
FileChannel 方式 讀取檔案共使用 282 毫秒
FileChannel 方式 讀取檔案共使用 298 毫秒
FileChannel 方式 讀取檔案共使用 280 毫秒

結語

在 Java 8 中,「FileChannel」是處理檔案 I/O 的高效方式,相比於傳統的 I/O流「FileInputStream」、「FileOutputStream」等更加靈活方便且效率更高。

程式碼附錄

package com.demo.io;

import org.junit.Test;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Date;
import java.util.Scanner;

/**
 * 檔案讀取
 * @date 2024-08-14 16:42:21
 */
public class FileReadTest {

    // 檔案路徑
    static String defaultFilePath = "/Users/changbeibei/Desktop/work/wfilep.log-24081412";

    // 8k
    static int defaultByteLength = 1024 * 8;

    /**
     * FileInputStream + byte[] 方式
     * 等同於 BufferedInputStream 位元組輸入緩衝流
     * 檔案位元組輸入流 + 位元組陣列讀取方式
     * 適用於:二進位制檔案或非文字檔案
     */
    @Test
    public void testFileInputStreamWithBytes() {
        long startTime = new Date().getTime();
        // 使用 try 包裝
        try (FileInputStream fileInputStream = new FileInputStream(defaultFilePath)){
            byte[] reads = new byte[defaultByteLength];
            int readCount;
            while ((readCount = fileInputStream.read(reads)) != -1) {
                // TODO 處理資料
            }
        } catch (IOException e) {
            System.out.printf("讀取檔案異常[ %s ]%n", e.getMessage());
        }

        System.out.printf("FileInputStream + byte[] 方式 讀取檔案共使用 %d 毫秒%n", new Date().getTime() - startTime);
    }

    /**
     * FileInputStream + Scanner 方式
     * 檔案位元組輸入流 + Scanner 讀取文字方式
     * 適用於:文字檔案
     */
    @Test
    public void testFileInputStreamWithScanner() {
        long startTime = new Date().getTime();
        // 使用 try 包裝
        try (FileInputStream fileInputStream = new FileInputStream(defaultFilePath)){
            Scanner scanner = new Scanner(fileInputStream);
            while (scanner.hasNext()) {
                scanner.nextLine();
                // TODO 處理資料
            }
        } catch (IOException e) {
            System.out.printf("讀取檔案異常[ %s ]%n", e.getMessage());
        }

        System.out.printf("FileInputStream + Scanner 方式 讀取檔案共使用 %d 毫秒%n", new Date().getTime() - startTime);
    }

    /**
     * FileReader + char[] 方式
     * 等同於 BufferedReader 字元輸入緩衝流
     * 檔案字元輸入流 + 字元陣列方式
     * 適用於:字元檔案
     */
    @Test
    public void testFileReaderWithChars() {
        long startTime = new Date().getTime();
        // 使用 try 包裝
        try (FileReader fileReader = new FileReader(defaultFilePath)){
            char[] reads = new char[defaultByteLength];
            int readCount;
            while ((readCount = fileReader.read(reads)) != -1) {
                // TODO 處理資料
            }
        } catch (IOException e) {
            System.out.printf("讀取檔案異常[ %s ]%n", e.getMessage());
        }

        System.out.printf("FileReader + char[] 方式 讀取檔案共使用 %d 毫秒%n", new Date().getTime() - startTime);
    }

    /**
     * BufferedReader 方式
     * 緩衝字元流方式
     * 適用於:字元檔案
     */
    @Test
    public void testBufferedReader() {
        long startTime = new Date().getTime();
        // 使用 try 包裝
        try (BufferedReader fileReader = new BufferedReader(new FileReader(defaultFilePath))){
            String line;
            while ((line = fileReader.readLine()) != null) {
                // TODO 處理資料
            }
        } catch (IOException e) {
            System.out.printf("讀取檔案異常[ %s ]%n", e.getMessage());
        }

        System.out.printf("BufferedReader 方式 讀取檔案共使用 %d 毫秒%n", new Date().getTime() - startTime);
    }

    /**
     * FileChannel 方式
     * 檔案輸入輸出管道流
     */
    @Test
    public void testFileChannel() {
        long startTime = new Date().getTime();
        // 使用 try 包裝
        try (FileChannel channel = FileChannel.open(Paths.get(defaultFilePath), StandardOpenOption.READ)){
            // 構造B yteBuffer 有兩個方法,ByteBuffer.allocate和 ByteBuffer.allocateDirect,兩個方法都是相同入參,含義卻不同。
            // ByteBuffer.allocate(capacity) 分配的是非直接緩衝區,非直接緩衝區的操作會在Java堆記憶體中進行,資料的讀寫會透過Java堆記憶體來傳遞。
            // ByteBuffer.allocateDirect(capacity) 分配的是直接緩衝區, 直接緩衝區的操作可以透過本地I/O傳遞,避免了在Java堆和本地堆之間的資料傳輸。
            ByteBuffer buf = ByteBuffer.allocate(defaultByteLength);
            while (channel.read(buf) != -1) {
                buf.flip();
                // TODO 處理資料
//                System.out.println(new String(buf.array()));
                buf.clear();
            }
        } catch (IOException e) {
            System.out.printf("讀取檔案異常[ %s ]%n", e.getMessage());
        }

        System.out.printf("FileChannel 方式 讀取檔案共使用 %d 毫秒%n", new Date().getTime() - startTime);
    }

    @Test
    public void testMain() {
        for (int i = 0; i < 10; i++) {
            System.out.printf("第 %d 次測試%n", i + 1);
            testFileInputStreamWithBytes();
            testFileInputStreamWithScanner();
            testFileReaderWithChars();
            testBufferedReader();
            testFileChannel();
        }
    }
}

相關文章