Статьи Утилиты Telegram YouTube RuTube Отзывы

Построчное чтение больших файлов

10 мая 2022

Тэги: Collections, Java, Stream API, руководство, файлы.

Содержание

  1. Чтение файла целиком
  2. Чтение файла через стрим
  3. Буферизация
  4. Выводы

В предыдущей статье Как сохранить текстовый файл мы научились записывать текстовый файл больших размеров. А в этот раз давайте научимся его читать построчно и каждую строку как-то обрабатывать. Например, будем искать наибольшую длину строки. Поскольку размер файла больше 100 МБ, его чтение происходит с небольшой задержкой.

Как обычно, рассмотрим несколько вариантов решения этой задачи, постепенно улучшая производительность.

Чтение файла целиком

В пакете nio есть удобный статичный метод Files.readAllLines(). Он возвращает список строк, т.е. считывает содержимое файла полностью в память.

public int getMaxLineLengthAllLines(File file) {
    try {
        return Files.readAllLines(Path.of(file.toURI()))
                .stream()
                .mapToInt(String::length)
                .max()
                .orElse(0);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

Из этого списка мы получаем стрим, затем преобразуем его в список длин строк с помощью метода mapToInt() и находим среди значений наибольшее. Для кейсов, когда список пустой, максимум не может быть найден, поэтому возвращаем по умолчанию 0. Для удобства, в блоке catch оборачиваем исключение в RuntimeException, чтобы не прописывать исключения в сигнатуре нашего метода.

Как вы уже догадываетесь, читать большой файл в память не самая лучшая затея. Особенно если вы обрабатываете каждую строку отдельно. Поэтому чтение файла размером в 111 МБ на моём ноуте занимает порядка 1100 миллисекунд, т.е. чуть больше секунды.

Чтение файла через стрим

Рассмотрим более эффективную реализацию, где мы обойдёмся без создания промежуточной коллекции со всеми строками, а сразу будем получать стрим и по очереди обрабатывать строки из него. Воспользуемся методом Files.lines(). Этот вариант более предпочтителен с точки зрения экономии памяти.

public int getMaxLineLengthStream(File file) {
    try (var lines = Files.lines(Paths.get(file.toURI()), StandardCharsets.UTF_8)) {
        return lines.mapToInt(String::length)
                .max()
                .orElse(0);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

Обратите внимание, что стрим мы инициализируем в блоке try-with-resources, чтобы Java автоматически освобождала ресурсы при выходе из метода. В остальном реализация этого метода похожа на предыдущую: получаем список длин строк и ищем среди них максимальную.

Данная реализация с тем же файлом работает почти в 2 раза быстрее, примерно за 600 миллисекунд.

Буферизация

Если отказаться от пакета nio и воспользоваться классической связкой FileReader, обёрнутый в BufferedReader, то этот вариант даст ещё больший прирост производительности.

public int getMaxLineLengthBuffered(File file) {
    try (
            var fr = new FileReader(file);
            var br = new BufferedReader(fr)
    ) {
        var line = br.readLine();
        var maxLineLength = 0;

        while (line != null) {
            if (maxLineLength < line.length()) {
                maxLineLength = line.length();
            }
            line = br.readLine();
        }
        return maxLineLength;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

Ридеры инициализируем в начале блока try-with-resources, заводим необходимые переменные и в цикле, строка за строкой, считываем весь файл. Попутно проверяем длину текущей строки, и если она больше той, что мы нашли ранее, то выбираем её в качестве результата. После обработки всего файла возвращаем найденное значение.

Данная реализация обрабатывает файл целиком примерно за 530 миллисекунд. Но кода здесь писать чуть больше.

Выводы

Вариант решения задачи с помощью Files.readAllLines() работает наименее эффективно, т.к. считывает все строки в память. Этот вариант имеет смысл использовать только если чтения всего файла целиком не избежать. Если же есть возможность обрабатывать файлы построчно, то лучше использовать Files.lines(), который сразу возвращает стрим.

А наибольшей эффективностью обладает классическая связка FileReader + BufferedReader, но она является блокирующей, в отличие от первых двух вариантов.


См. также


Комментарии

Добавить комментарий

×

devmark.ru