Статьи

Сравнение файлов в Java

Я создаю серию видеоуроков для PACKT о сетевом программировании на Java. Существует целый раздел о Java NIO. Одним из примеров программы является копирование файла через необработанное сокетное соединение с клиента на сервер. Клиент читает файл с диска, а сервер сохраняет байты по мере их поступления на диск. Поскольку это демонстрационная версия, сервер и клиент работают на одном компьютере, и файл копируется из одного каталога в один и тот же каталог, но с другим именем. Доказательством того, что пудинг съест это: файлы нужно сравнивать.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
package packt.java9.network.niodemo;
 
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Random;
 
public class SampleMaker {
    public static void main(String[] args) throws IOException {
        byte[] buffer = new byte[1024 * 1024 * 10];
        try (FileOutputStream fos = new FileOutputStream("sample.txt")) {
            Random random = new Random();
            for (int i = 0; i < 16; i++) {
                random.nextBytes(buffer);
                fos.write(buffer);
            }
        }
    }
}

Использовать IntelliJ для сравнения файлов довольно просто, но, поскольку файлы являются двоичными и большими, такой подход не совсем оптимален. Я решил написать короткую программу, которая будет не только сигнализировать, что файлы разные, но и где разница. Код очень прост:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package packt.java9.network.niodemo;
 
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
 
public class SampleCompare {
    public static void main(String[] args) throws IOException {
        long start = System.nanoTime();
        BufferedInputStream fis1 = new BufferedInputStream(new FileInputStream("sample.txt"));
        BufferedInputStream fis2 = new BufferedInputStream(new FileInputStream("sample-copy.txt"));
        int b1 = 0, b2 = 0, pos = 1;
        while (b1 != -1 && b2 != -1) {
            if (b1 != b2) {
                System.out.println("Files differ at position " + pos);
            }
            pos++;
            b1 = fis1.read();
            b2 = fis2.read();
        }
        if (b1 != b2) {
            System.out.println("Files have different length");
        } else {
            System.out.println("Files are identical, you can delete one of them.");
        }
        fis1.close();
        fis2.close();
        long end = System.nanoTime();
        System.out.print("Execution time: " + (end - start)/1000000 + "ms");
    }
}

Время сравнения двух 160-мегабайтных файлов составляет около 6 секунд на моем Mac Book, оборудованном твердотельным накопителем, и оно значительно не улучшится, если я укажу большой, скажем, 10 МБ буфер в качестве второго аргумента для конструктора BufferedInputStream . (С другой стороны, если мы не используем BufferedInputStream тогда время примерно в десять раз больше.) Это приемлемо, но если я просто diff sample.txt sample-copy.txt из командной строки, то ответ значительно быстрее, а не 6 секунд. Это может быть много вещей, таких как время запуска Java, интерпретация кода в начале цикла while, пока JIT-компилятор не решит, что пора начинать работать. Однако я догадываюсь, что код тратит большую часть времени на чтение файла в память. Чтение байтов в буфер является сложным процессом. Это касается операционной системы, драйверов устройств, реализации JVM, и они перемещают байты из одного места в другое, и, наконец, мы сравниваем только байты, и ничего больше. Это можно сделать более простым способом. Мы можем попросить операционную систему сделать это для нас и пропустить большинство действий времени выполнения Java, файловых буферов и других проблем.

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

Файлы, отображенные в память, считываются в память операционной системой, а байты доступны программе Java. Память выделяется операционной системой, и она не потребляет кучу памяти. Если код Java изменяет содержимое отображенной памяти, то операционная система записывает изменение на диск оптимизированным способом, когда считает, что это необходимо. Это, однако, не означает, что данные будут потеряны в случае сбоя JVM. Когда код Java изменяет память файла, отображенного в памяти, он изменяет память, которая принадлежит операционной системе и доступна и действительна после остановки JVM. Нет гарантии и 100% защиты от перебоев в подаче электроэнергии и аппаратных сбоев, но это очень низкий уровень. Если кто-то боится этого, тогда защита должна быть на аппаратном уровне, так что Java все равно не имеет ничего общего. С файлами, отображенными в память, мы можем быть уверены, что данные сохраняются на диск с определенной, очень высокой вероятностью, которая может быть увеличена только отказоустойчивым оборудованием, кластерами, источниками бесперебойного питания и так далее. Это не Java. Если вам действительно нужно что-то сделать из Java для записи данных на диск, вы можете вызвать метод MappedByteBuffer.force() который попросит операционную систему записать изменения на диск. Вызов этого слишком часто и излишне может помешать производительности, хотя. (Простой, потому что он записывает данные на диск и возвращает только тогда, когда операционная система говорит, что данные были записаны.)

Чтение и запись данных с использованием отображенных в память файлов обычно происходит намного быстрее в случае больших файлов. Для обеспечения надлежащей производительности у машины должна быть значительная память, в противном случае в памяти сохраняется только часть файла, а количество ошибок страниц возрастает. Одна из хороших вещей заключается в том, что если один и тот же файл отображается в памяти двумя или более различными процессами, то используется одна и та же область памяти. Таким образом, процессы могут даже общаться друг с другом.

Приложение сравнения, использующее отображенные в память файлы, выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package packt.java9.network.niodemo;
 
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
 
public class MapCompare {
    public static void main(String[] args) throws IOException {
        long start = System.nanoTime();
        FileChannel ch1 = new RandomAccessFile("sample.txt", "r").getChannel();
        FileChannel ch2 = new RandomAccessFile("sample-copy.txt", "r").getChannel();
        if (ch1.size() != ch2.size()) {
            System.out.println("Files have different length");
            return;
        }
        long size = ch1.size();
        ByteBuffer m1 = ch1.map(FileChannel.MapMode.READ_ONLY, 0L, size);
        ByteBuffer m2 = ch2.map(FileChannel.MapMode.READ_ONLY, 0L, size);
        for (int pos = 0; pos < size; pos++) {
            if (m1.get(pos) != m2.get(pos)) {
                System.out.println("Files differ at position " + pos);
                return;
            }
        }
        System.out.println("Files are identical, you can delete one of them.");
        long end = System.nanoTime();
        System.out.print("Execution time: " + (end - start) / 1000000 + "ms");
    }
}

Чтобы отобразить в памяти файлы, мы должны сначала открыть их, используя класс RandomAccessFile и запросить канал у этого объекта. Канал может использоваться для создания MappedByteBuffer , который представляет область памяти, в которую загружается содержимое файла. Карта методов в этом примере отображает файл в режиме только для чтения, от начала файла до конца файла. Мы пытаемся отобразить весь файл. Это работает, только если файл не превышает 2 ГБ. Начальная позиция long но размер отображаемой области ограничен размером Integer .

Обычно это … О да, время сравнения файлов со случайным контентом размером 160 МБ составляет около 1 секунды.

Опубликовано на Java Code Geeks с разрешения Питера Верхаса, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Сравнение файлов в Java

Мнения, высказанные участниками Java Code Geeks, являются их собственными.