Чтение файлов в Java
Чтение текстовых и двоичных файлов в Java с помощью FileReader, BufferedReader, Scanner, Files.readString и потоков.
Существует пять распространённых способов прочитать текстовый файл в Java, и правильный выбор почти полностью зависит от размера файла и того, что вы хотите сделать с содержимым. В этой главе рассматриваются все пять — от простейшего до наиболее гибкого:
Files.readString(path)— весь файл в виде однойString.Files.readAllLines(path)— весь файл в видеList<String>.Files.readAllBytes(path)— весь файл в видеbyte[].Files.lines(path)— файл в виде ленивогоStream<String>.BufferedReader/Scanner— классические декораторы с полным контролем.
Выбирайте наименьший инструмент, подходящий для задачи. Чтение 4 ГБ лога с помощью Files.readString приводит к OutOfMemoryError; чтение 12-строчного конфигурационного файла через BufferedReader с циклом while — это шесть строк кода там, где хватило бы одной.
Files.readString(path) — весь файл за один вызов
String text = Files.readString(Path.of("config.json"), StandardCharsets.UTF_8);Добавлен в Java 11. Возвращает весь файл в виде String. По умолчанию использует UTF-8 начиная с Java 18 (тем не менее Charset по-прежнему рекомендуется задавать явно, даже с новым значением по умолчанию). Выбрасывает IOException, если файл не существует или не может быть прочитан; выбрасывает OutOfMemoryError, если файл больше кучи.
Используйте когда: файл достаточно мал — конфигурационные файлы, JSON-данные, MDX-главы, всё, что можно прочитать в одном окне редактора. Неформальное правило: меньше нескольких мегабайт.
Files.readAllLines(path) — список строк
List<String> lines = Files.readAllLines(Path.of("hosts.txt"), StandardCharsets.UTF_8);Возвращает неизменяемый List<String> строк файла с удалёнными разделителями строк. Профиль памяти аналогичен readString плюс накладные расходы List — также хранит весь файл в памяти.
Используйте когда: вам нужна индексация по номеру строки, сортировка файла или передача строк в цикл for (String line : lines) без настройки потоков.
Files.readAllBytes(path) — необработанные байты
byte[] raw = Files.readAllBytes(Path.of("photo.png"));Байтовый эквивалент. Без Charset, так как декодирование не происходит. Используйте для бинарных файлов (изображений, архивов, исполняемых файлов) или когда нужно вычислить хэш или передать байты в ByteArrayInputStream.
Files.lines(path) — ленивый поток
try (Stream<String> lines = Files.lines(Path.of("app.log"), StandardCharsets.UTF_8)) {
long errors = lines.filter(l -> l.contains("ERROR")).count();
}Это единственный встроенный инструмент чтения, масштабируемый для файлов произвольного размера. Stream<String> является ленивым — строки читаются по требованию, а не все сразу — и напрямую сочетается с лексикой потоковых конвейеров (filter, map, count, toList).
Два обязательных требования:
try-with-resources обязателен. Поток владеет открытым дескриптором файла; безtry-with-resources файл остаётся открытым до сборки мусора, и вы исчерпаете файловые дескрипторы на нагруженном сервере.- Не переиспользуйте поток после терминальной операции. Потоки одноразовые.
Используйте когда: файл слишком велик для readAllLines, или вы хотите, чтобы построчное преобразование сочеталось с остальным потоковым конвейером.
BufferedReader.readLine() — классический подход
BufferedReader — это рабочая лошадка, которую оборачивают современные вспомогательные методы. Он буферизует базовые операции чтения в блок фиксированного размера в памяти, так что readLine() не производит один системный вызов на каждый символ.
try (BufferedReader in = Files.newBufferedReader(Path.of("hosts.txt"), StandardCharsets.UTF_8)) {
String line;
while ((line = in.readLine()) != null) {
System.out.println(line);
}
}Files.newBufferedReader(path) — это современная фабрика; классический вариант — new BufferedReader(new FileReader("hosts.txt")) (который использует кодировку платформы на JDK старше 18 — задавайте UTF-8 с помощью трёхаргументной перегрузки). Контракт readLine():
- Возвращает следующую строку без разделителя (
\n,\rили\r\n). - Возвращает
nullв конце файла. Условие цикла(line = readLine()) != null— устоявшаяся идиома.
BufferedReader также является производителем Stream<String>: reader.lines() возвращает Stream<String>, основанный на ридере. Именно так реализован Files.lines под капотом.
Scanner — посимвольный разбор
Scanner читает текст токенами — словами, целыми числами, числами с плавающей точкой, строками, даже совпадениями регулярных выражений — и является правильным инструментом для чтения структурированного ввода, где единицы не являются целыми строками.
try (Scanner sc = new Scanner(Files.newBufferedReader(Path.of("nums.txt")))) {
while (sc.hasNextInt()) {
int n = sc.nextInt();
System.out.println(n * n);
}
}Scanner медленнее BufferedReader, так как выполняет разбор; он выделяет короткие строки и запускает регулярные выражения. Для построчной обработки предпочтите BufferedReader. Для типизированных токенов из небольшого файла (числа, слова, CSV-подобный ввод) Scanner избавляет от слоя разбора.
Полная глава о Scanner находится далее в этой части — здесь рассмотрен вариант чтения файла.
FileReader — необработанный символьный ридер
try (FileReader in = new FileReader("notes.txt", StandardCharsets.UTF_8)) {
int c;
while ((c = in.read()) != -1) {
System.out.print((char) c);
}
}FileReader читает символы непосредственно из файла — без буферизации, без учёта строк, без сделанных за вас выборов декодирования (вы передаёте Charset или принимаете умолчание платформы на JDK до версии 18). Это слой, поверх которого построены остальные инструменты. В прикладном коде его почти никогда не используют напрямую — его оборачивают в BufferedReader.
Тем не менее он полезен, когда нужно прочитать несколько сотен символов и остановиться — небольшие поиски, где стоимость настройки буфера перевешивает стоимость вызова.
Что выбрать
| Сценарий | Выбор |
|---|---|
Небольшой файл нужен как единая String | Files.readString |
Небольшой файл нужен как List<String> | Files.readAllLines |
| Бинарный файл (изображение, архив) | Files.readAllBytes |
| Любой файл с потоковым преобразованием | Files.lines (внутри try-with-resources) |
| Построчный цикл с полным контролем | Files.newBufferedReader + readLine |
| Типизированные токены (числа, слова, совпадения regex) | Scanner |
| По одному символу, крошечный файл | FileReader |
Правильный вариант по умолчанию для случая «просто хочу загрузить этот небольшой текстовый файл» — Files.readString. Правильный вариант по умолчанию для «обработать этот огромный лог без переполнения памяти» — Files.lines.
Практический пример: один файл, пять ридеров
Программа ниже записывает небольшой текстовый файл, а затем читает его пятью разными способами — readString, readAllLines, Files.lines с фильтрацией через Predicate<String> из словаря части 12, BufferedReader.readLine и Scanner для токенизированных целых чисел. Каждый блок выводит то, что получил, чтобы вы могли видеть формы рядом.
Что вынести из этого примера:
Files.readStringвернул весь файл в виде однойString— просто и именно то, что нужно для небольших конфигов и шаблонов. Для 4 ГБ лога это привело бы кOutOfMemoryError.Files.readAllLinesвернул индексируемыйList<String>со срезанными разделителями.lines.get(0)сработал, потому что список материализован в памяти; с потоком так сделать не получится.Files.lines(file)был открыт внутриtry-with-resources, так как поток владеет дескриптором файла. Конвейер.filter(isError).count()имеет ту же форму, что и в части 12 — изменился только источник.BufferedReader.readLine()вернулnullв конце файла. Циклforздесь остановился на трёх намеренно, но в продакшн-коде идиома —while ((line = in.readLine()) != null).Scannerпропустил строки, не начинающиеся с целого числа, затем читал токены с помощьюnextInt(), пока они не закончились. Тот жеScannerмог бы читать числа с плавающей точкой (nextDouble), совпадения регулярных выражений (findInLine) илиBigInteger— вот почему он стоит дороже на токен, чемBufferedReaderна строку.
Что дальше
Следующая глава, Запись файлов в Java, охватывает сторону записи тех же API — Files.writeString, Files.write, BufferedWriter, PrintWriter и флаги StandardOpenOption (APPEND, CREATE_NEW, TRUNCATE_EXISTING), которые определяют, как обрабатывается существующий файл.