Перейти к основному содержимому

Сравните содержимое двух файлов в Java

· 5 мин. чтения

1. Обзор

В этом руководстве мы рассмотрим различные подходы к определению одинаковости содержимого двух файлов. Мы будем использовать основные библиотеки ввода-вывода Java Stream для чтения содержимого файлов и реализации базовых сравнений.

В завершение мы рассмотрим поддержку, предоставляемую в 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

Метод Files::mismatch , добавленный в Java 12, сравнивает содержимое двух файлов . Он возвращает -1L, если файлы идентичны, в противном случае возвращает позицию в байтах первого несоответствия.

Этот метод внутренне считывает фрагменты данных из входных потоков файлов и использует Arrays::mismatch , представленный в Java 9, для их сравнения .

Как и в нашем первом примере, для файлов разного размера, но для которых содержимое маленького файла идентично соответствующему содержимому большего файла, он возвращает размер (в байтах) меньшего файла.

Примеры использования этого метода см. в нашей статье, посвященной новым возможностям Java 12 .

5. Использование файлов с отображением памяти

Файл с отображением памяти — это объект ядра, который отображает байты из файла на диске в адресное пространство памяти компьютера. Память кучи обходится, поскольку код Java манипулирует содержимым отображаемых в память файлов, как если бы мы напрямую обращались к памяти.

Для больших файлов чтение и запись данных из отображаемых в память файлов выполняется намного быстрее, чем при использовании стандартной библиотеки ввода-вывода Java . Важно, чтобы компьютер имел достаточный объем памяти для выполнения задания, чтобы предотвратить перегрузку.

Давайте напишем очень простой пример, который показывает, как сравнить содержимое двух файлов, используя файлы с отображением памяти:

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 для сравнения в памяти байтов всего файла за один проход.

6. Использование ввода-вывода Apache Commons

Методы IOUtils::contentEquals и IOUtils::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 .