Ввод-вывод и файлы в Java
Ввод-вывод и файлы
★ Ввод-вывод (I/O) в Java строится на потоках — последовательная передача байтов или символов между программой и источником (файл, сеть, консоль). Современный API файловой системы — NIO.2 (java.nio.file, с Java 7).
Исключения при I/O: обработка исключений. Строки и кодировки: строки.
Два уровня API
| Уровень | Пакеты | Назначение |
|---|---|---|
| Классический I/O | java.io | InputStream, OutputStream, Reader, Writer |
| NIO / NIO.2 | java.nio, java.nio.file | Буферы, каналы, Path, Files |
В новом коде для файлов на диске начинайте с Path + Files, для сети и бинарных протоколов — потоки или NIO channels.
Path и Files (NIO.2)
Path — неизменяемый путь (замена устаревшему java.io.File для логики путей):
Path config = Path.of("app", "config", "settings.properties");
Path absolute = config.toAbsolutePath().normalize();
boolean exists = Files.exists(config);
boolean isDir = Files.isDirectory(config);
long size = Files.size(config);
Разбор:
Path.of("app", "config", "settings.properties")собирает путь платформенно-нейтрально, без ручной склейки строк через\или/.toAbsolutePath()превращает относительный путь в абсолютный, аnormalize()убирает лишние сегменты (.и..), чтобы путь был каноничнее для логов и проверок.Files.exists(config)проверяет наличие объекта по пути; это быстрый pre-check перед чтением или записью.Files.isDirectory(config)помогает отделить файл от каталога и заранее предотвратить ошибки вида "ожидали файл, получили директорию".Files.size(config)возвращает размер в байтах и часто используется для валидации входного файла перед обработкой.
Небольшой практический принцип — путь и его проверка (exists, isDirectory, normalize) живут рядом в коде, тогда ошибки формата и окружения всплывают раньше.
Чтение и запись целиком (малые и средние файлы):
Path file = Path.of("notes.txt");
List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
String text = Files.readString(file, StandardCharsets.UTF_8);
Files.writeString(file, "новая строка\n", StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
Разбор:
Files.readAllLines(...)загружает весь файл вList<String>, поэтому удобен для небольших файлов, где нужен построчный доступ в памяти.Files.readString(...)читает файл целиком одной строкой; полезно для шаблонов, JSON и конфигов небольшого размера.- Явный
StandardCharsets.UTF_8фиксирует кодировку и делает поведение одинаковым на Linux/Windows/macOS. Files.writeString(...)сCREATEсоздаёт файл при отсутствии, аAPPENDдописывает в конец, не перезаписывая старое содержимое.- Комбинация
CREATE + APPENDтипична для логов и "журналоподобных" текстовых файлов.
Поток строк (большие файлы, ленивая обработка):
try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) {
long count = lines.filter(l -> !l.isBlank()).count();
}
Разбор:
Files.lines(...)возвращает ленивыйStream<String>и читает файл постепенно, а не целиком в память.try-with-resourcesобязателен: закрывает underlying file handle, когда поток обработан.filter(l -> !l.isBlank())оставляет только непустые строки и убирает "шум" из пустых/пробельных строк.count()выполняет терминальную операцию: именно в этот момент запускается фактическое чтение данных.- Такой подход устойчивее для больших файлов и снижает риск
OutOfMemoryError.
Копирование и перемещение:
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.move(oldPath, newPath);
Files.delete(path);
Files.createDirectories(Path.of("data", "cache"));
Разбор:
Files.copy(...)копирует файл/каталог по пути, аREPLACE_EXISTINGразрешает перезапись существующей цели.Files.move(...)переносит или переименовывает путь; на одной файловой системе обычно это дешёвая операция.Files.delete(path)удаляет объект и бросает исключение, если путь не существует или удаление запрещено правами.Files.createDirectories(...)создаёт всю цепочку директорий и не падает, если часть каталогов уже есть.- Эти операции часто оборачивают в отдельный сервис с логированием пути и типа операции для удобной диагностики.
File и Path — что выбирать
java.io.File встречается в legacy-коде и сторонних библиотеках. В новом коде основной API для ФС:
Pathдля путей;Filesдля операций с файлами и каталогами;Fileтолько как мост при интеграции (path.toFile()/file.toPath()).
Байтовые и символьные потоки
| Тип | Классы | Единица |
|---|---|---|
| Байтовый | InputStream, OutputStream | byte |
| Символьный | Reader, Writer | char (кодировка) |
Символьные потоки оборачивают байтовые с указанием charset:
try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8);
Writer writer = Files.newBufferedWriter(out, StandardCharsets.UTF_8)) {
char[] buf = new char[8192];
int n;
while ((n = reader.read(buf)) != -1) {
writer.write(buf, 0, n);
}
}
Разбор:
newBufferedReader/newBufferedWriterдобавляют буферизацию и уменьшают количество системных вызовов чтения/записи.char[] buf = new char[8192]задаёт буфер фиксированного размера: компромисс между скоростью и памятью.reader.read(buf)возвращает число реально прочитанных символов, поэтому нужно писать ровноn, а не весь буфер.- Цикл
while (... != -1)— стандартный паттерн для чтения потока до конца. - Такой код безопасно копирует текст с сохранением выбранной кодировки.
Буферизация (BufferedInputStream, BufferedReader) снижает число системных вызовов.
Чтение больших файлов без OutOfMemoryError
Для логов и дампов действуйте потоково:
try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = br.readLine()) != null) {
process(line);
}
}
Разбор:
BufferedReaderчитает файл построчно и держит в памяти только текущую порцию данных.readLine()возвращаетnullв конце файла, это сигнал завершить цикл.- Вызов
process(line)выполняет бизнес-логику по мере чтения, без накопления всех строк. - Явная UTF-8 кодировка устраняет платформенные расхождения в обработке кириллицы и спецсимволов.
- Паттерн подходит для логов, CSV и любых длинных текстов, где важна стабильная память.
Преимущество перед readAllLines: память растет стабильно и не зависит линейно от размера файла.
try-with-resources
Ресурсы, реализующие AutoCloseable, закрываются автоматически:
try (InputStream in = Files.newInputStream(path);
OutputStream out = Files.newOutputStream(target)) {
in.transferTo(out); // Java 9+
}
Разбор:
newInputStream/newOutputStreamоткрывают байтовые потоки для бинарного копирования данных.in.transferTo(out)переносит все байты из входа в выход до EOF и упрощает ручной цикл чтения/записи.- Конструкция
try (...)гарантирует закрытие обоих потоков даже при исключении в середине операции. - Это хороший базовый шаблон для копирования файлов, выгрузок и загрузок.
- Для крупных файлов и сетевых сценариев остаётся важным обрабатывать
IOExceptionи логировать путь источника/цели.
При исключении в блоке сначала выбрасывается основное, затем подавленное из close() — удобно для диагностики.
Связь с try-with-resources для исключений.
Каталог ресурсов приложения
Важный практический момент: файлы рядом с исходниками и файлы в classpath читаются по-разному.
- На диске (
Path.of(...)) — для пользовательских данных и внешних конфигов. - Из classpath (
getResourceAsStream) — для файлов, упакованных в JAR.
Это убирает типичный класс ошибок "локально работает, в JAR не находит файл".
Сериализация объектов
ObjectOutputStream / ObjectInputStream сохраняют граф объектов в бинарный поток. Класс должен реализовать Serializable (или Externalizable).
В современных системах чаще используют JSON/XML/Protobuf (см. стандартные библиотеки), а не Java Serialization — из соображений безопасности и совместимости версий.
WatchService — реакция на изменения файлов
Для утилит и локальных демонов можно отслеживать изменения в каталоге:
WatchService ws = FileSystems.getDefault().newWatchService();
Path dir = Path.of("inbox");
dir.register(ws, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY);
Разбор:
newWatchService()создаёт подписку на события файловой системы конкретной платформы.Path.of("inbox")указывает каталог, за которым будет идти наблюдение.dir.register(...)подписывает каталог на события создания и изменения файлов.ENTRY_CREATEудобно для "подхвата" новых входящих файлов,ENTRY_MODIFY— для реакции на изменения существующих.- На практике цикл обработки событий должен учитывать
OVERFLOWи повторно валидировать состояние файлов на диске.
WatchService удобен для простых сценариев, но в production требует аккуратной обработки переполнения событий и перезапуска наблюдения.
Работа с каталогами
try (Stream<Path> walk = Files.walk(Path.of("logs"), 2)) {
walk.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".log"))
.forEach(System.out::println);
}
Разбор:
Files.walk(Path.of("logs"), 2)обходит дерево каталогов до глубины2, включая вложенные подпапки.Files::isRegularFileотбрасывает директории, симлинки и специальные файлы.- Фильтр
endsWith(".log")оставляет только лог-файлы для последующей обработки. forEach(System.out::println)здесь демонстрационный вывод; в реальном коде обычно вызывают парсер, архиватор или uploader.try-with-resourcesзакрывает внутренний поток обхода, чтобы не держать открытыми ресурсы файловой системы.
Files.list(dir) — только один уровень; walk — рекурсия с лимитом глубины.
Типичные ошибки
- Не указана кодировка —
FileReaderбез charset использует платформенную; на Windows часто не UTF-8. - Утечка дескрипторов — поток не закрыт (нет try-with-resources).
- Чтение гигантского файла в
readAllLines— рискOutOfMemoryError; используйтеFiles.linesили буфер. - Относительные пути — зависят от
user.dir; для конфигов лучше явный базовый каталог или переменные окружения. - Пути с пробелами —
Path.ofкорректен; не склеивайте строки вручную через\.
Мини-чек-лист для надежного I/O
- Charset указан явно (
UTF-8) на чтении и записи. - Внешние пути валидируются и нормализуются.
- Любой поток закрывается через try-with-resources.
- Для больших файлов применяется потоковая обработка.
- Ошибки I/O логируются с путем и операцией, а не только с текстом исключения.
I/O и многопоточность
Блокирующее чтение в одном потоке занимает поток ОС на всё время ожидания диска или сети. Для множества одновременных соединений смотрите асинхронность и NIO (Selector, неблокирующие каналы) или фреймворки уровня Spring WebFlux.
NIO Channels и Selector когда углубляться
Path и Files покрывают большинство задач работы с файлами на диске.
Неблокирующие Channel и Selector нужны, когда:
- много одновременных соединений;
- важно держать малое число потоков ОС;
- вы строите сетевой сервер, прокси или сервис с очень большим потоком входящих событий.
Базовые элементы:
ByteBuffer— буфер байтов с указателямиposition,limit,capacity.SocketChannelиServerSocketChannel— сетевые каналы.Selector— механизм, который позволяет одному потоку обслуживать много каналов.
Мини-схема:
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(8080));
server.register(selector, SelectionKey.OP_ACCEPT);
Когда задача типовая:
- чтение конфигов;
- работа с файлами;
- экспорт и импорт данных;
оставляйте код на Files и buffered I/O.
Переходите к Selector, когда нагрузочный тест показывает, что основное ограничение связано с числом одновременных подключений.
Дополнительно:
Связанные материалы
- Обработка исключений
- Строки в Java
- Stream API
- JVM, память и потоки
- Справочник по конфигурациям — кодировки и
java.io.tmpdir