.## 1. 概述

在这个教程中,我们将回顾几种方法来判断两个文件的内容是否相等。我们将使用Java核心流I/O库来读取文件内容,并实现基础比较。

最后,我们将讨论Apache Commons I/O提供的功能,用于检查两个文件的内容是否相等。

2. 字节对字节比较

让我们从一个简单的开始,逐字节地读取两个文件的内容进行比较。

为了加快文件的读取速度,我们将使用BufferedInputStream。如你所见,BufferedInputStream会将底层InputStream读取的大块字节存储到内部缓冲区。当客户端读取完缓冲区中的所有字节后,缓冲区会从流中读取另一块字节。

显然,使用BufferedInputStream比逐字节从底层流中读取要快得多

让我们编写一个使用BufferedInputStream比较两个文件的方法:

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语句确保在语句结束时关闭两个BufferedInputStream

通过while循环,我们逐个读取第一个文件的字节,并与第二个文件的相应字节进行比较。如果发现不匹配,我们返回不匹配的字节位置。否则,两个文件内容相同,方法返回-1L。

我们可以看到,如果两个文件大小不同,但较小文件的字节与较大文件的相应字节匹配,那么它将返回较小文件的字节数。

3. 行对行比较

对于文本文件,我们可以实现一个逐行比较它们之间内容的方法

让我们使用一个BufferedReader,它采用与InputStreamBuffer相同的策略,从文件中复制数据块到内部缓冲区以加速读取过程。

让我们看看我们的实现:

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

Java 12新增的Files::mismatch方法用于比较两个文件的内容。如果文件相同,它将返回-1L;否则,它将返回第一个不匹配的字节位置。

这个方法内部从文件的InputStreams读取数据块,并使用Java 9引入的Arrays::mismatch方法进行比较

与我们的第一个例子类似,对于大小不同但小文件的内容与大文件相应内容相同的文件,它将返回较小文件的字节数。

有关如何使用此方法的示例,请参阅我们的文章,涵盖了Java 12的新特性

5. 使用内存映射文件

内存映射文件是内核对象,它将磁盘文件的字节映射到计算机的内存地址空间。由于Java代码直接操作内存映射文件的内容,因此避免了堆内存的绕行。

对于大型文件,使用内存映射文件进行读写数据的速度远高于标准Java I/O库。重要的是,计算机必须有足够的内存来处理这项工作,以防止内存不足。

让我们编写一个简单的示例,展示如何使用内存映射文件比较两个文件的内容:

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

我们使用RandomAccessFile类打开文件并访问其相应的FileChannel以获取MappedByteBuffer。这是一个直接的字节数组缓冲区,它是文件的一个内存映射区域。在这个简单的实现中,我们使用它的equals方法一次遍历内存中的整个文件字节进行比较。

6. 使用Apache Commons I/O

IOUtils::contentEqualsIOUtils::contentEqualsIgnoreEOL方法用于比较两个文件的内容以确定它们是否相等。它们之间的区别在于,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. 总结

在这篇文章中,我们介绍了几种实现方法,用于比较两个文件的内容以检查它们是否相等。

源代码可以在GitHub上找到:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-12


« 上一篇: Java Weekly, 第399期
» 下一篇: ksqlDB入门介绍