В современной Java с файлами работают через пакет java.nio.file: Path описывает путь, а Files — набор статических методов, которые по этому пути читают, пишут, копируют, удаляют и обходят файлы. Старый класс java.io.File остался для совместимости, но в новом коде его обычно заменяют на Path + Files.
Содержание
- Четыре термина, которые путают
- Минимальный рабочий пример
- Чтение: какой метод выбрать
- try-with-resources: зачем закрывать файлы
- Обход каталогов: list и walk
- Копирование, перемещение, удаление
- Типовые исключения: код -> результат -> исправление
- Если имя файла приходит от пользователя
- java.io.File и Files: что выбрать
- Если не получилось
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — рабочий пример целиком, затем чтение и запись, try-with-resources, обход каталогов, типовые исключения и безопасная работа с путями. Все примеры проверены на Java 25 (Temurin 25.0.4, Linux) 24.09.2026 запуском java Имя.java; большинство методов доступно с Java 11.
Четыре термина, которые путают
| Термин | Что это | Чего не делает |
|---|---|---|
Path |
Путь: строка вида data/notes.txt, разобранная на части |
Не открывает файл и не проверяет, что он существует |
Files |
Утилитный класс с операциями над путями | Не хранит состояние, объекты не создаются |
Поток (InputStream, BufferedReader) |
Открытый файл, из которого читают по частям | Сам не закрывается — нужен close() |
java.io.File |
Старый класс пути и операций (до Java 7) | Часто сообщает об ошибке через false без причины |
Главное различие: Path.of("x.txt") ничего не делает с диском. Диск трогают только методы Files и открытые потоки.
Минимальный рабочий пример
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;
public class FilesDemo {
public static void main(String[] args) throws IOException {
Path dir = Path.of("data");
Files.createDirectories(dir); // не падает, если каталог уже есть
Path file = dir.resolve("notes.txt"); // data/notes.txt
Files.writeString(file, "первая строка\n", StandardCharsets.UTF_8);
Files.writeString(file, "вторая строка\n", StandardCharsets.UTF_8,
StandardOpenOption.APPEND); // дописать в конец
List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
System.out.println("Строк: " + lines.size());
System.out.println("Первая: " + lines.get(0));
System.out.println("Размер, байт: " + Files.size(file));
System.out.println("Полный путь: " + file.toAbsolutePath());
}
}
Вывод (полный путь зависит от каталога запуска):
Строк: 2
Первая: первая строка
Размер, байт: 52
Полный путь: /w/data/notes.txt
Что здесь происходит:
resolveсклеивает путь с учетом разделителя ОС, поэтомуfile.separatorвручную подставлять не нужно.- Первый
writeStringбез опций создает файл или перезаписывает его целиком (по умолчанию действуютCREATE,TRUNCATE_EXISTING,WRITE). Поэтому повторный запуск снова дает 2 строки, а не 4. APPENDдописывает в конец. 52 байта — это 28 символов: 24 кириллические буквы по 2 байта в UTF-8 плюс 2 пробела и 2 перевода строки по 1 байту.- Относительный путь
dataотсчитывается от рабочего каталога процесса, а не от папки с.java-файлом.
Чтение: какой метод выбрать
| Задача | Метод | Граница |
|---|---|---|
| Небольшой текст целиком | Files.readString(path) (Java 11+) |
Весь файл в памяти |
| Список строк | Files.readAllLines(path) |
Весь файл в памяти |
| Большой файл построчно | Files.newBufferedReader(path) или Files.lines(path) |
Нужно закрыть через try-with-resources |
| Бинарные данные | Files.readAllBytes(path) / Files.newInputStream(path) |
Первый вариант — все в памяти |
Методы Files без явной кодировки читают и пишут в UTF-8 независимо от настроек системы. У FileReader/FileWriter без аргумента кодировки иначе: там берется кодировка по умолчанию, которая стала UTF-8 только с Java 18. Надежнее всегда передавать StandardCharsets.UTF_8 явно.
try-with-resources: зачем закрывать файлы
readString и writeString открывают и закрывают файл сами. А newBufferedReader, newBufferedWriter, Files.lines, Files.walk и Files.list возвращают объект, который держит файл открытым, пока вы его не закроете. Конструкция try (...) закрывает ресурс при любом выходе из блока, в том числе по исключению.
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
public class Big {
public static void main(String[] args) throws IOException {
Path log = Path.of("app.log");
try (BufferedWriter w = Files.newBufferedWriter(log, StandardCharsets.UTF_8)) {
for (int i = 1; i <= 100_000; i++) {
w.write(i % 1000 == 0 ? "ERROR request " + i : "INFO request " + i);
w.newLine();
}
}
int errors = 0;
try (BufferedReader r = Files.newBufferedReader(log, StandardCharsets.UTF_8)) {
String line;
while ((line = r.readLine()) != null) {
if (line.startsWith("ERROR")) errors++;
}
}
System.out.println("ERROR-строк: " + errors);
System.out.println("Размер: " + Files.size(log) + " байт");
}
}
ERROR-строк: 100
Размер: 1888995 байт
Файл почти в 2 МБ читается по строке, в памяти одновременно одна строка. Тот же подсчет короче через поток: try (Stream<String> s = Files.lines(log)) { s.filter(l -> l.startsWith("ERROR")).count(); } — и здесь try тоже обязателен.
Без закрытия ошибка проявляется не сразу. При сотнях незакрытых файлов процесс упирается в лимит дескрипторов ОС, в Windows открытый файл обычно нельзя удалить или переместить, а у BufferedWriter без close() или flush() хвост данных может так и не попасть на диск.
Обход каталогов: list и walk
Files.list отдает только первый уровень каталога, Files.walk — все вложенные файлы и папки рекурсивно. Оба возвращают Stream<Path>, который нужно закрыть.
import java.io.IOException;
import java.nio.file.*;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;
public class WalkDemo {
public static void main(String[] args) throws IOException {
Path root = Path.of("project");
Files.createDirectories(root.resolve("src/util"));
Files.writeString(root.resolve("README.txt"), "readme");
Files.writeString(root.resolve("src/Main.java"), "class Main {}");
Files.writeString(root.resolve("src/util/Helper.java"), "class Helper {}");
try (Stream<Path> top = Files.list(root)) { // только первый уровень
top.map(p -> p.getFileName().toString()).sorted()
.forEach(n -> System.out.println("list: " + n));
}
try (Stream<Path> all = Files.walk(root)) { // рекурсивно
List<Path> javaFiles = all
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.sorted()
.toList();
javaFiles.forEach(p -> System.out.println("walk: " + root.relativize(p)));
}
try (Stream<Path> all = Files.walk(root)) { // удалить дерево: сначала файлы, потом каталоги
for (Path p : all.sorted(Comparator.reverseOrder()).toList()) {
Files.delete(p);
}
}
System.out.println("exists: " + Files.exists(root));
}
}
list: README.txt
list: src
walk: src/Main.java
walk: src/util/Helper.java
exists: false
Порядок элементов в list и walk файловая система не гарантирует, поэтому в примере стоит sorted(). Обратная сортировка при удалении ставит вложенные пути раньше родительских. Files.walk по умолчанию не заходит по символическим ссылкам в каталоги; с опцией FileVisitOption.FOLLOW_LINKS заходит. Метод toList() у потока появился в Java 16, на более старых версиях используйте collect(Collectors.toList()).
Копирование, перемещение, удаление
Files.copy(src, dst)падает, еслиdstсуществует; перезапись — опциейStandardCopyOption.REPLACE_EXISTING. Каталог копируется без содержимого, для дерева нужен обход.Files.move(src, dst)переименовывает или переносит; опцияATOMIC_MOVEпросит атомарную операцию и бросаетAtomicMoveNotSupportedException, если ФС ее не умеет (например, перенос между разными дисками).Files.deleteбросает исключение, если файла нет или каталог не пуст;Files.deleteIfExistsвернетfalseдля отсутствующего файла.
Практический прием — атомарное сохранение: пишем во временный файл рядом с целью и заменяем цель одним move. Читатель файла видит либо старую, либо новую версию, но не наполовину записанную.
import java.io.IOException;
import java.nio.file.*;
public class AtomicSave {
static void saveAtomically(Path target, String content) throws IOException {
Path dir = target.toAbsolutePath().getParent();
Path tmp = Files.createTempFile(dir, "save-", ".tmp"); // в том же каталоге, что и цель
try {
Files.writeString(tmp, content);
Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);
} finally {
Files.deleteIfExists(tmp); // если move не случился
}
}
public static void main(String[] args) throws IOException {
Path cfg = Path.of("settings.json");
saveAtomically(cfg, "{\"theme\": \"dark\"}");
System.out.println(Files.readString(cfg)); // {"theme": "dark"}
}
}
Граница приема: при ATOMIC_MOVE поведение для уже существующей цели документация оставляет на усмотрение реализации. На Linux в одной ФС цель заменяется, на других системах это стоит проверить. createTempFile на POSIX-системах создает файл с правами rw------- (проверено в прогоне), то есть другие пользователи его не прочитают.
Типовые исключения: код -> результат -> исправление
В java.nio.file ошибки приходят как подклассы IOException с понятным именем. Прогон на Java 25:
Files.createFile(Path.of("once.txt")); // второй вызов
Files.delete(Path.of("box")); // в box есть подкаталог
Files.readString(Path.of("missing.txt"));
Files.readString(Path.of("win1251.txt")); // файл в кодировке windows-1251
java.nio.file.FileAlreadyExistsException: once.txt
java.nio.file.DirectoryNotEmptyException: box
java.nio.file.NoSuchFileException: missing.txt
java.nio.charset.MalformedInputException: Input length = 1
Исправления по порядку: для «создать файл, если его нет» — перехватить FileAlreadyExistsException (сам createFile атомарен) или писать через Files.writeString(path, text, StandardOpenOption.CREATE, StandardOpenOption.APPEND); проверка Files.exists перед созданием ненадежна, потому что между проверкой и созданием файл может появиться, а Files.createDirectories подходит только для каталогов и на существующий файл с тем же именем тоже бросает FileAlreadyExistsException; для каталога — удаление дерева обходом, как выше; для отсутствующего файла — вывести path.toAbsolutePath() и сверить рабочий каталог; для кодировки — указать ее явно: Files.readString(path, Charset.forName("windows-1251")) вернула в прогоне «Привет».
Если имя файла приходит от пользователя
Имя из формы или HTTP-запроса нельзя просто передать в resolve: строка ../../etc/passwd выведет путь за пределы рабочего каталога (path traversal). Минимальная проверка — нормализовать путь и убедиться, что он остался внутри базового каталога.
import java.io.IOException;
import java.nio.file.*;
public class SafePath {
static Path resolveInside(Path base, String userName) throws IOException {
Path root = base.toRealPath(); // реальный путь базы без симлинков
Path target = root.resolve(userName).normalize(); // убираем ./ и ../
if (!target.startsWith(root)) {
throw new SecurityException("Путь вне каталога загрузок: " + userName);
}
return target;
}
public static void main(String[] args) throws IOException {
Path base = Files.createDirectories(Path.of("uploads"));
System.out.println(base.toRealPath().relativize(resolveInside(base, "report.txt")));
try {
resolveInside(base, "../../etc/passwd");
} catch (SecurityException e) {
System.out.println(e.getMessage());
}
}
}
report.txt
Путь вне каталога загрузок: ../../etc/passwd
Это базовая проверка, а не полная защита. Пустое имя дает сам базовый каталог (он проходит проверку, поэтому пустую строку стоит отклонять отдельно), а символ NUL в имени приводит к InvalidPathException — его тоже нужно обрабатывать как отказ. Path.startsWith сравнивает элементы пути, а не строки, поэтому uploads2 не пройдет как «внутри uploads». Но симлинк, созданный внутри uploads и ведущий наружу, эта проверка не ловит: для уже существующих файлов дополнительно сравнивайте target.toRealPath() с базой. В рабочем сервисе надежнее вообще не использовать имя пользователя как имя файла: генерировать свое (например, UUID), хранить исходное имя в базе, ограничивать размер и держать каталог загрузок вне раздаваемых веб-сервером папок.
java.io.File и Files: что выбрать
| Операция | java.io.File |
java.nio.file.Files |
|---|---|---|
| Создать файл | createNewFile() -> boolean |
createFile(path) -> исключение с причиной |
| Удалить | delete() -> false без объяснения |
delete(path) -> NoSuchFileException, DirectoryNotEmptyException |
| Прочитать текст | через FileReader + цикл |
readString, readAllLines, lines |
| Обход дерева | рекурсия по listFiles() |
walk, find, walkFileTree |
| Атрибуты, права, симлинки | почти нет | getPosixFilePermissions, isSymbolicLink, readAttributes |
Если библиотека требует File, переход в обе стороны — одна строка: file.toPath() и path.toFile().
Если не получилось
NoSuchFileException, хотя файл «рядом» — программа запущена из другого каталога. НапечатайтеPath.of("").toAbsolutePath()и путь к файлу черезtoAbsolutePath().- Вместо текста «кракозябры» или
MalformedInputException— файл не в UTF-8. Узнайте исходную кодировку и передайте ее в метод чтения. - Файл не удаляется на Windows — где-то остался открытый поток; оберните все
new...,lines,walk,listв try-with-resources. AccessDeniedException— нет прав у пользователя, от которого запущен процесс, или файл занят другим процессом.- Записанные данные пропали — вызван
writeString/writeбезAPPEND, и файл перезаписался.
Выводы
Path— только путь,Files— операции над ним;java.io.Fileнужен в основном для совместимости со старым кодом.- Для небольших файлов хватает
readString/writeString, для больших —newBufferedReader/newBufferedWriterилиFiles.lines. - Все, что возвращает поток или
Stream(lines,walk,list,new...), закрывайте через try-with-resources. - Кодировку передавайте явно, а режим записи выбирайте сознательно: по умолчанию файл перезаписывается.
- Имя файла от пользователя нормализуйте и проверяйте, что путь остался внутри базового каталога.
Где применяется / связь с практикой
Освойте тему на практике
Работа с файлами встречается почти в любом Java-проекте: чтение конфигураций и CSV, логи, выгрузки отчетов, загрузка файлов на сервер, тестовые данные во временных каталогах. Эти задачи опираются на базу языка — исключения, коллекции, Stream API и работу с ресурсами. Системно пройти ее с практикой и ревью кода можно на курсе «Java разработчик. Базовый уровень», а отдельные темы разобрать на открытых уроках Otus.
FAQ
Чем Path.of отличается от Paths.get?
Результат одинаковый. Path.of появился в Java 11, Paths.get — более старый вариант с Java 7; в новом коде обычно пишут Path.of.
Как прочитать файл из ресурсов внутри jar?
Файл внутри jar — не файл на диске, поэтому Path.of к нему не подходит. Используйте getClass().getResourceAsStream("/name.txt") в try-with-resources.
Нужно ли проверять Files.exists перед чтением?
Обычно нет: между проверкой и чтением файл может исчезнуть. Надежнее сразу читать и обрабатывать NoSuchFileException.



