10 мая 2022
Тэги: Collections, Java, Stream API, руководство, файлы.
В предыдущей статье Как сохранить текстовый файл мы научились записывать текстовый файл больших размеров. А в этот раз давайте научимся его читать построчно и каждую строку как-то обрабатывать. Например, будем искать наибольшую длину строки. Поскольку размер файла больше 100 МБ, его чтение происходит с небольшой задержкой.
Как обычно, рассмотрим несколько вариантов решения этой задачи, постепенно улучшая производительность.
В пакете nio есть удобный статичный метод Files.readAllLines(). Он возвращает список строк, т.е. считывает содержимое файла полностью в память.
Из этого списка мы получаем стрим, затем преобразуем его в список длин строк с помощью метода mapToInt() и находим среди значений наибольшее. Для кейсов, когда список пустой, максимум не может быть найден, поэтому возвращаем по умолчанию 0. Для удобства, в блоке catch оборачиваем исключение в RuntimeException, чтобы не прописывать исключения в сигнатуре нашего метода.
Как вы уже догадываетесь, читать большой файл в память не самая лучшая затея. Особенно если вы обрабатываете каждую строку отдельно. Поэтому чтение файла размером в 111 МБ на моём ноуте занимает порядка 1100 миллисекунд, т.е. чуть больше секунды.
Рассмотрим более эффективную реализацию, где мы обойдёмся без создания промежуточной коллекции со всеми строками, а сразу будем получать стрим и по очереди обрабатывать строки из него. Воспользуемся методом Files.lines(). Этот вариант более предпочтителен с точки зрения экономии памяти.
Обратите внимание, что стрим мы инициализируем в блоке try-with-resources, чтобы Java автоматически освобождала ресурсы при выходе из метода. В остальном реализация этого метода похожа на предыдущую: получаем список длин строк и ищем среди них максимальную.
Данная реализация с тем же файлом работает почти в 2 раза быстрее, примерно за 600 миллисекунд.
Если отказаться от пакета nio и воспользоваться классической связкой FileReader, обёрнутый в BufferedReader, то этот вариант даст ещё больший прирост производительности.
Ридеры инициализируем в начале блока try-with-resources, заводим необходимые переменные и в цикле, строка за строкой, считываем весь файл. Попутно проверяем длину текущей строки, и если она больше той, что мы нашли ранее, то выбираем её в качестве результата. После обработки всего файла возвращаем найденное значение.
Данная реализация обрабатывает файл целиком примерно за 530 миллисекунд. Но кода здесь писать чуть больше.
Вариант решения задачи с помощью Files.readAllLines() работает наименее эффективно, т.к. считывает все строки в память. Этот вариант имеет смысл использовать только если чтения всего файла целиком не избежать. Если же есть возможность обрабатывать файлы построчно, то лучше использовать Files.lines(), который сразу возвращает стрим.
А наибольшей эффективностью обладает классическая связка FileReader + BufferedReader, но она является блокирующей, в отличие от первых двух вариантов.
Kotlin, Java, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Linux, Hibernate, Collections, Stream API, многопоточность, файлы, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.