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, H2, Linux, Hibernate, Collections, Stream API, многопоточность, чат-боты, нейросети, файлы, devops, Docker, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.