1. 概要

このチュートリアルでは、2つのファイルの内容が等しいかどうかを判断するために、さまざまなアプローチを確認します。 コアJavaストリームI/Oライブラリを使用して、ファイルの内容を読み取り、基本的な比較を実装します。

最後に、Apache Commons I / Oで提供されるサポートを確認して、2つのファイルのコンテンツが等しいかどうかを確認します。

2. バイトごとの比較

2つのファイルからバイトを読み取り、それらを順番に比較するという簡単なアプローチから始めましょう。

ファイルの読み取りを高速化するために、BufferedInputStreamを使用します。 後で説明するように、 BufferedInputStream は、基になるInputStreamから内部バッファーにバイトの大きなチャンクを読み取ります。 クライアントがチャンク内のすべてのバイトを読み取るとき、バッファーはストリームから別のバイトブロックを読み取ります。

明らかに、 BufferedInputStreamの使用は、基になるストリームから一度に1バイトを読み取るよりもはるかに高速です。

BufferedInputStreamを使用して2つのファイルを比較するメソッドを書いてみましょう。

public static long filesCompareByByte(Path path1, Path path2) throws IOException {
    try (BufferedInputStream fis1 = new BufferedInputStream(new FileInputStream(path1.toFile()));
         BufferedInputStream fis2 = new BufferedInputStream(new FileInputStream(path2.toFile()))) {
        
        int ch = 0;
        long pos = 1;
        while ((ch = fis1.read()) != -1) {
            if (ch != fis2.read()) {
                return pos;
            }
            pos++;
        }
        if (fis2.read() == -1) {
            return -1;
        }
        else {
            return pos;
        }
    }
}

try-with-resources ステートメントを使用して、ステートメントの最後で2つのBufferedInputStreamが閉じられるようにします。

while ループを使用して、最初のファイルの各バイトを読み取り、2番目のファイルの対応するバイトと比較します。 不一致が見つかった場合は、不一致のバイト位置を返します。 それ以外の場合、ファイルは同一であり、メソッドは-1Lを返します。

ファイルのサイズが異なるが、小さいファイルのバイトが大きいファイルの対応するバイトと一致する場合、小さいファイルのバイト単位のサイズが返されることがわかります。

3. 行ごとの比較

テキストファイルを比較するために、ファイルを1行ずつ読み取り、それらの間の同等性をチェックする実装を行うことができます

InputStreamBufferと同じ戦略を使用するBufferedReaderを使用して、データのチャンクをファイルから内部バッファーにコピーして、読み取りプロセスを高速化してみましょう。

実装を確認しましょう:

public static long filesCompareByLine(Path path1, Path path2) throws IOException {
    try (BufferedReader bf1 = Files.newBufferedReader(path1);
         BufferedReader bf2 = Files.newBufferedReader(path2)) {
        
        long lineNumber = 1;
        String line1 = "", line2 = "";
        while ((line1 = bf1.readLine()) != null) {
            line2 = bf2.readLine();
            if (line2 == null || !line1.equals(line2)) {
                return lineNumber;
            }
            lineNumber++;
        }
        if (bf2.readLine() == null) {
            return -1;
        }
        else {
            return lineNumber;
        }
    }
}

コードは、前の例と同様の戦略に従います。 while ループでは、バイトを読み取る代わりに、各ファイルの行を読み取り、等しいかどうかを確認します。 すべての行が両方のファイルで同一である場合は-1Lを返しますが、不一致がある場合は、最初の不一致が見つかった行番号を返します。

ファイルのサイズが異なっていても、小さいファイルが大きいファイルの対応する行と一致する場合は、小さいファイルの行数が返されます。

4. Files ::mismatchとの比較

Java12で追加されたメソッドFiles::mismatchは、2つのファイルの内容を比較します。 ファイルが同一の場合は-1Lを返し、それ以外の場合は最初の不一致の位置をバイト単位で返します。

このメソッドは、ファイルのInputStreamsからデータのチャンクを内部的に読み取り、Java9で導入されたArrays:: mismatchを使用して、それらを比較します

最初の例と同様に、サイズは異なるが、小さいファイルの内容が大きいファイルの対応する内容と同じであるファイルの場合、小さいファイルのサイズ(バイト単位)が返されます。

この方法の使用例については、Java12新機能に関する記事を参照してください。

5. メモリマップトファイルの使用

メモリマップトファイルは、ディスクファイルのバイトをコンピュータのメモリアドレス空間にマップするカーネルオブジェクトです。 Javaコードは、メモリに直接アクセスしているかのようにメモリマップトファイルの内容を操作するため、ヒープメモリは回避されます。

大きなファイルの場合、メモリマップトファイルからのデータの読み取りと書き込みは、標準のJava I/Oライブラリを使用するよりもはるかに高速です。 スラッシングを防ぐために、コンピュータにジョブを処理するのに十分な量のメモリがあることが重要です。

メモリマップトファイルを使用して2つのファイルの内容を比較する方法を示す非常に簡単な例を書いてみましょう。

public static boolean compareByMemoryMappedFiles(Path path1, Path path2) throws IOException {
    try (RandomAccessFile randomAccessFile1 = new RandomAccessFile(path1.toFile(), "r"); 
         RandomAccessFile randomAccessFile2 = new RandomAccessFile(path2.toFile(), "r")) {
        
        FileChannel ch1 = randomAccessFile1.getChannel();
        FileChannel ch2 = randomAccessFile2.getChannel();
        if (ch1.size() != ch2.size()) {
            return false;
        }
        long size = ch1.size();
        MappedByteBuffer m1 = ch1.map(FileChannel.MapMode.READ_ONLY, 0L, size);
        MappedByteBuffer m2 = ch2.map(FileChannel.MapMode.READ_ONLY, 0L, size);

        return m1.equals(m2);
    }
}

このメソッドは、ファイルの内容が同一である場合は true を返し、そうでない場合はfalseを返します。

RamdomAccessFile クラスを使用してファイルを開き、それぞれの FileChannel にアクセスして、MappedByteBufferを取得します。 これは、ファイルのメモリマップ領域であるダイレクトバイトバッファです。 この単純な実装では、 equals メソッドを使用して、1回のパスでファイル全体のバイトをメモリ内で比較します。

6. Apache Commons I/Oの使用

メソッドIOUtils::contentEqualsおよびIOUtils::contentEqualsIgnoreEOLは、2つのファイルの内容を比較して同等性を判断します。 それらの違いは、 contentEqualsIgnoreEOLが改行(\ n)とキャリッジリターン(\ r)を無視することです。 これの動機は、オペレーティングシステムがこれらの制御文字のさまざまな組み合わせを使用して新しい行を定義することによるものです。

等しいかどうかを確認する簡単な例を見てみましょう。

@Test
public void whenFilesIdentical_thenReturnTrue() throws IOException {
    Path path1 = Files.createTempFile("file1Test", ".txt");
    Path path2 = Files.createTempFile("file2Test", ".txt");

    InputStream inputStream1 = new FileInputStream(path1.toFile());
    InputStream inputStream2 = new FileInputStream(path2.toFile());

    Files.writeString(path1, "testing line 1" + System.lineSeparator() + "line 2");
    Files.writeString(path2, "testing line 1" + System.lineSeparator() + "line 2");

    assertTrue(IOUtils.contentEquals(inputStream1, inputStream2));
}

改行制御文字を無視したいが、それ以外の場合は内容が等しいかどうかを確認したい場合:

@Test
public void whenFilesIdenticalIgnoreEOF_thenReturnTrue() throws IOException {
    Path path1 = Files.createTempFile("file1Test", ".txt");
    Path path2 = Files.createTempFile("file2Test", ".txt");

    Files.writeString(path1, "testing line 1 \n line 2");
    Files.writeString(path2, "testing line 1 \r\n line 2");

    Reader reader1 = new BufferedReader(new FileReader(path1.toFile()));
    Reader reader2 = new BufferedReader(new FileReader(path2.toFile()));

    assertTrue(IOUtils.contentEqualsIgnoreEOL(reader1, reader2));
}

7. 結論

この記事では、2つのファイルの内容の比較を実装して、等しいかどうかを確認するいくつかの方法について説明しました。

ソースコードはGitHubにあります。