确定两个文件是否存储相同的内容

81

你将如何编写一个 Java 函数 boolean sameContent(Path file1,Path file2),用于确定这两个给定的路径指向存储相同内容的文件?当然,首先我会检查文件大小是否相同。这是存储相同内容的必要条件。但是,我想听听你的方法。如果这两个文件存储在同一硬盘驱动器上(像在我大多数情况下一样),可能不是在两个流之间跳转太多次的最佳方式。


4
同一内容的大小可能会有所不同,这取决于几个因素。如果你真的想比较这些内容,一个简单的检查方法是对两个文件进行校验和,并进行比较。你可以使用 md5 对文件的字节数组进行校验。也可以直接比较这些字节数组。 - Rene M.
@ReneM。我很好奇,在什么情况下具有相同字节内容的两个文件会有不同的大小? - IntegrateThis
例如,块存储的块大小和使用的分区格式可能会导致完全相同内容的不同字节大小。 - Rene M.
10个回答

105

FileUtils.contentEquals 方法是 Apache Commons IO 库中的一种方法,其功能是比较两个文件的内容是否相同。有关该方法的API,请参见此处

可以尝试以下代码:

File file1 = new File("file1.txt");
File file2 = new File("file2.txt");
boolean isTwoEqual = FileUtils.contentEquals(file1, file2);

在进行比较之前,它会执行以下检查:

  • 两个文件都存在
  • 传递的两个文件必须是文件类型而不是目录。
  • 字节长度不能相同。
  • 两个是不同的文件而不是相同的文件。
  • 然后比较内容。

3
为了增加价值,我发现 FileUtils.contentEqualsIgnoreEOL 可以为宽松的断言提供方便。 - CloudyTrees
这是一个很好的做法,但如果我想逐行匹配文件内容,然后从两个文件中打印匹配的名称和ID,则可以采用以下方法。 - whatthefish
我们可以用这个来进行图像或视频比较吗? - PJ2104
1
请注意,这可能不是最优的性能解决方案。commons-io(至少到v2.5)使用IOUtils,而IOUtils又会进行(逐字节读取)[https://github.com/apache/commons-io/blob/commons-io-2.5/src/main/java/org/apache/commons/io/IOUtils.java#L2563](至少在缓冲流上是这样的)。 如果差异很大且文件很大,则可能会影响性能。 - Skyr

38

如果您不想使用任何外部库,那么只需将文件读入字节数组并进行比较即可(在Java 7之前不起作用):

byte[] f1 = Files.readAllBytes(file1);
byte[] f2 = Files.readAllBytes(file2);

通过使用 Arrays.equals 方法进行比较。

如果文件很大,那么你应该使用 BufferedInputStream,并按照这里所述,将文件分块读取而不是将整个文件读入数组中。


2
我希望我的程序也能处理大文件。这可能会导致OutOfMemoryError错误 - 例如,如果无法分配所需大小的数组,例如文件大于2GB。编辑:抱歉,我刚看到您关于处理大文件的备注。 - principal-ideal-domain
2
是的。这就是为令我包含了一个链接到SO页面的原因,该页面提到使用BufferedInputStream并逐块读取文件,而不是整个文件。毫无意义地重复已经存在于SO中的答案。 - Chthonic Project

21
自Java 12以来,有一个名为Files.mismatch的方法,如果文件内容不存在不匹配,则返回-1。因此,该函数看起来应该如下:
private static boolean sameContent(Path file1, Path file2) throws IOException {
    return Files.mismatch(file1, file2) == -1;
}

它是否会将整个文件加载到内存中?我在谷歌上没有找到任何相关的参考资料。 - Akinn
2
据我所知 - 不会,它通过8kb大小的块读取两个文件。 - Nolequen

14
如果文件较小,您可以将两个文件都读入内存并比较字节数组。 如果文件不小,则可以依次计算其内容的哈希值(例如MD5或SHA-1),然后比较哈希值(但这仍然存在极小的出错机会),或者您可以比较它们的内容,但是为此您仍需要交替读取流。
以下是一个示例:
boolean sameContent(Path file1, Path file2) throws IOException {
    final long size = Files.size(file1);
    if (size != Files.size(file2))
        return false;

    if (size < 4096)
        return Arrays.equals(Files.readAllBytes(file1), Files.readAllBytes(file2));

    try (InputStream is1 = Files.newInputStream(file1);
         InputStream is2 = Files.newInputStream(file2)) {
        // Compare byte-by-byte.
        // Note that this can be sped up drastically by reading large chunks
        // (e.g. 16 KBs) but care must be taken as InputStream.read(byte[])
        // does not neccessarily read a whole array!
        int data;
        while ((data = is1.read()) != -1)
            if (data != is2.read())
                return false;
    }

    return true;
}

你不能只是将输入流包装在 BufferedInputStream 中吗?这样方法就可以像使用 read(byte[]) 一样高效,但没有那么复杂,对吧? - aioobe
@aioobe 是的,我们可以。我使用逐字节比较的原因是read(byte[])方法不能保证完全读取传递的字节数组(javadoc说“它最多读取bytes.length”)。如果基础流的源是文件,则当前实现将读取整个数组,但不能保证此操作。而且,正确处理非完整数组读取的代码会更加复杂,并且会吸引注意力,使我的代码片段所展示的原则不明显。 - icza
1
我理解这一点,但我的意思是,通过使用“BufferedInputStream”(同时仍然实现效率),您可以避免那种复杂性。 - aioobe
@aioobe 是的,你说得对。为其他读者提供一些背景:BufferedInputStream.read(byte[] b, int off, int len) 的 javadoc 确实说明它尝试读取整个数组。虽然 BufferedInputStream 没有覆盖 FilterInputStream.read(byte[] b),但是 FilterInputStream.read(byte[] b) 的 javadoc 表明实现会调用 read(b, 0, b.length),而在 BufferedInputStream 的情况下,这将调用 BufferedInputStream.read(byte[] b, int off, int len) 方法。 - icza
3
我仍然不确定我们是否在同一页面上。我认为只需将Files.newInputStream(file1)更改为new BufferedInputStream(Files.newInputStream(file1)),您的is1.read()调用将对应于简单的数组访问(在大多数情况下),整个read(byte [] ...)操作将在幕后处理。因此,我建议您通过在BufferedInputStreams中包装输入流并删除有关如何以牺牲额外复杂性来加速的注释来改进您的答案。 - aioobe

7

这个链接可以帮助您解决问题:

package test;

import java.io.File;
import java.io.IOException;

import org.apache.commons.io.FileUtils;

public class CompareFileContents {

    public static void main(String[] args) throws IOException {

        File file1 = new File("test1.txt");
        File file2 = new File("test2.txt");
        File file3 = new File("test3.txt");

        boolean compare1and2 = FileUtils.contentEquals(file1, file2);
        boolean compare2and3 = FileUtils.contentEquals(file2, file3);
        boolean compare1and3 = FileUtils.contentEquals(file1, file3);

        System.out.println("Are test1.txt and test2.txt the same? " + compare1and2);
        System.out.println("Are test2.txt and test3.txt the same? " + compare2and3);
        System.out.println("Are test1.txt and test3.txt the same? " + compare1and3);
    }
}

7

如果是单元测试,那么AssertJ提供了一个名为hasSameContentAs的方法。以下是一个示例:

Assertions.assertThat(file1).hasSameContentAs(file2)

1
现在这个函数名为hasSameTextualContentAs - Hamster
2
hasSameContentAsmethod has been deprecated, please use instead hasSameBinaryContentAs:`assertThat(file1).hasSameBinaryContentAs(file2);` - Anyul Rivas

2

我知道我在这个问题上来得很晚,但如果你想使用纯Java API和没有第三方依赖项,内存映射IO是一个非常简单的方法。只需要几个调用来打开文件,映射它们,然后使用ByteBuffer.equals(Object)比较文件。

如果您希望特定文件是大文件,那么这可能会为您提供最佳性能,因为您将大部分IO工作转移到操作系统和JVM的高度优化位(假设您正在使用良好的JVM)。

直接从FileChannel JavaDoc中引用:

对于大多数操作系统,将文件映射到内存中比通过通常的读取和写入方法读取或写入几十千字节的数据更加昂贵。从性能的角度来看,通常只有将相对较大的文件映射到内存中才值得。

import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;


public class MemoryMappedCompare {

    public static boolean areFilesIdenticalMemoryMapped(final Path a, final Path b) throws IOException {
        try (final FileChannel fca = FileChannel.open(a, StandardOpenOption.READ);
             final FileChannel fcb = FileChannel.open(b, StandardOpenOption.READ)) {
            final MappedByteBuffer mbba = fca.map(FileChannel.MapMode.READ_ONLY, 0, fca.size());
            final MappedByteBuffer mbbb = fcb.map(FileChannel.MapMode.READ_ONLY, 0, fcb.size());
            return mbba.equals(mbbb);
        }
    }
}


1
我刚刚发现map只能处理最大为Integer.MAX_VALUE字节的文件,约为2GB。 - localhost
最终我使用map()函数的位置和长度字段,以块的方式比较文件。 - localhost

1

它兼容JR6及以上版本,无需库文件,在运行时不会读取所有内容。

public static boolean sameFile(File a, File b) {
    if (a == null || b == null) {
        return false;
    }

    if (a.getAbsolutePath().equals(b.getAbsolutePath())) {
        return true;
    }

    if (!a.exists() || !b.exists()) {
        return false;
    }
    if (a.length() != b.length()) {
        return false;
    }
    boolean eq = true;
    
    FileChannel channelA;
    FileChannel channelB;
    try {
        channelA = new RandomAccessFile(a, "r").getChannel();
        channelB = new RandomAccessFile(b, "r").getChannel();

        long channelsSize = channelA.size();
        ByteBuffer buff1 = channelA.map(FileChannel.MapMode.READ_ONLY, 0, channelsSize);
        ByteBuffer buff2 = channelB.map(FileChannel.MapMode.READ_ONLY, 0, channelsSize);
        for (int i = 0; i < channelsSize; i++) {
            if (buff1.get(i) != buff2.get(i)) {
                eq = false;
                break;
            }
        }
    } catch (FileNotFoundException ex) {
        Logger.getLogger(HotUtils.class.getName()).log(Level.SEVERE, null, ex);
    } catch (IOException ex) {
        Logger.getLogger(HotUtils.class.getName()).log(Level.SEVERE, null, ex);
    }
    return eq; 
}

0

Rene M. 在评论中提到的校验和检查是最好的方法。


-3
package test;  

      import org.junit.jupiter.api.Test;

      import java.io.IOException;
      import java.nio.file.FileSystems;
      import java.nio.file.Files;
      import java.nio.file.Path;

import static org.junit.Assert.assertEquals;

public class CSVResultDIfference {

   @Test
   public void csvDifference() throws IOException {
       Path file_F = FileSystems.getDefault().getPath("C:\\Projekts\\csvTestX", "yolo2.csv");
       long size_F = Files.size(file_F);
       Path file_I = FileSystems.getDefault().getPath("C:\\Projekts\\csvTestZ", "yolo2.csv");
       long size_I = Files.size(file_I);
       assertEquals(size_F, size_I);

   }
}

这对我有用 :)


欢迎来到Stack Overflow!虽然这段代码片段可能解决了问题,但它并没有解释为什么或者如何回答这个问题。请在您的代码中包含解释,因为这真的有助于提高您的帖子质量。记住,您正在为未来的读者回答问题,而这些人可能不知道您的代码建议的原因。 - Samuel Philipp
3
你正在比较文件大小,而不是内容。 - Stefan Reich

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接