Класс Java NIO Path
Представляйте пути файловой системы в современном Java с помощью java.nio.file.Path и фабрики Paths.
Path — это современная замена java.io.File. Он представляет путь файловой системы — упорядоченную последовательность компонентов имён, опционально начинающуюся с / или C:\ — и ничего больше. Он не читает файл. Он не проверяет существование файла. Он ничего не блокирует. Операции с байтами на диске находятся в Files (следующая глава). Path — это существительное; Files — это глагол.
Если вы использовали java.io.File, разница двоякая: Path неизменяем (каждая операция возвращает новый Path), и он чётко разделяет «строку пути» и «то, что находится на диске по этому пути». Большинство современных API — Files, FileChannel, перегрузки BufferedReader.lines() — принимают Path, а не File. Новый код использует Path.
Создание Path
Path p = Path.of("logs", "2025", "app.log"); // joins components with the platform separator
Path q = Path.of("/etc/hosts"); // absolute Unix path
Path r = Paths.get("C:", "Users", "vaz"); // older factory — same behaviour
Path s = Path.of(URI.create("file:///etc/hosts")); // from a URIPath.of(...) — современная фабрика; Paths.get(...) — более старая и по-прежнему работает. Обе создают объект пути без обращения к файловой системе. Path.of("nope/nope/nope") выполняется успешно, даже если такой файл не существует.
Path.of объединяет аргументы переменной длины с помощью File.separator платформы — / на Unix, \ на Windows. Это делает строковый путь, который вы пишете, переносимым: Path.of("src", "main", "java") создаёт правильный путь на обеих платформах. Как только вы напишете Path.of("src/main/java") с жёстко заданными слэшами, вы случайно сделаете его специфичным для Unix.
Изучение пути
Методы доступа к компонентам для Path.of("/var/log/app/today.log"):
| Метод | Возвращает | Пример |
|---|---|---|
getFileName() | последний компонент как Path | today.log |
getParent() | всё, кроме последнего | /var/log/app |
getRoot() | корень, или null если относительный | / |
getNameCount() | количество компонентов имени | 4 |
getName(int i) | i-й компонент | getName(0) → var |
subpath(b, e) | компоненты имён [b, e) | subpath(1, 3) → log/app |
isAbsolute() | имеет ли путь корень | true |
toString() | отформатированная строка платформы | /var/log/app/today.log |
Эти методы чистые: они смотрят на внутренний список имён объекта пути и возвращают его срезы. Ни один из них не обращается к диску.
resolve, resolveSibling и ловушка абсолютного аргумента
resolve(other) означает «объединить this и other»:
Path base = Path.of("/var/log");
base.resolve("app.log"); // /var/log/app.log
base.resolve("app/today.log"); // /var/log/app/today.logЛовушка: если other является абсолютным, resolve возвращает other без изменений:
base.resolve("/etc/hosts"); // /etc/hosts -- base is discarded
base.resolve(Path.of("/etc/hosts")); // same: /etc/hostsЭто задокументированное поведение, и каждый Java-программист попадается на него однажды. Если вы принимаете имя файла из пользовательского ввода и разрешаете его относительно настроенного базового каталога, злоумышленник, который передаёт "/etc/passwd", получает свой абсолютный путь — выходя за пределы базы. Всегда проверяйте или нормализуйте внешний ввод перед resolve.
resolveSibling(other) заменяет последний компонент:
Path p = Path.of("/var/log/today.log");
p.resolveSibling("yesterday.log"); // /var/log/yesterday.logЭто getParent().resolve(other) со встроенной проверкой на null. Полезно для «записи вывода рядом со входом».
relativize: обратная операция
Зная базу и цель, base.relativize(target) возвращает относительный путь от base до target:
Path base = Path.of("/var/log");
Path target = Path.of("/var/log/app/today.log");
base.relativize(target); // app/today.log
target.relativize(base); // ../..Контракт: base.resolve(base.relativize(target)) эквивалентен target (с учётом normalize). Именно так вы превращаете абсолютную цель в короткую относительную ссылку внутри базового каталога — полезно для строк лога, записей архива и URL.
Оба пути должны быть одного типа (оба абсолютные или оба относительные) и оба должны принадлежать одной FileSystem. Смешение вызывает IllegalArgumentException.
normalize: удаление . и ..
Объекты Path допускают компоненты . и .. — Path.of("/var/log/../tmp") является допустимым Path. normalize() удаляет их синтаксически:
Path.of("/var/log/../tmp").normalize(); // /var/tmp
Path.of("./a/./b/../c").normalize(); // a/cСлово «синтаксически» важно: normalize работает на уровне строк. Он не спрашивает файловую систему, куда фактически указывает ... Если /var/log является символической ссылкой на /tmp/logs, то на диске /var/log/.. — это /tmp, а не /var. normalize() этого не знает — он просто удаляет ...
Когда вам нужен реальный путь на диске (с разрешёнными символическими ссылками и правильно интерпретированным ..), используйте toRealPath(), которая обращается к файловой системе:
Path real = Path.of("/var/log/../tmp").toRealPath(); // resolves symlinks, throws if the file doesn't existДля проверки равенства путей и сравнения строк используйте normalize(). Для «канонического имени файла на диске прямо сейчас» используйте toRealPath().
Равенство основано на строках
path1.equals(path2) сравнивает пути как строки (компонент за компонентом). Он не нормализует, не разрешает символические ссылки, не проверяет файловую систему:
Path.of("/var/log").equals(Path.of("/var/log/.")); // false -- one has a trailing '.' component
Path.of("/var/log/.").equals(Path.of("/var/log/.").normalize()); // false -- normalize() dropped the '.'
Path.of("/var/log").equals(Path.of("/var/log").normalize()); // true -- already normalized, no change
Path.of("/var/log").equals(Path.of("/var/log")); // trueЧтобы сравнить два пути как «указывают ли они на один и тот же файл», нормализуйте оба и сравните, или вызовите Files.isSameFile(p1, p2) (обращается к файловой системе, единственная полностью корректная проверка). Для сортировки и ключей HashSet строковое равенство — это то, что даёт Path; это подходит для большинства случаев, но не означает «один и тот же файл на диске».
Совместимость с File
Path и File взаимно конвертируются:
File f = Path.of("/etc/hosts").toFile();
Path p = new File("/etc/hosts").toPath();Это понадобится, когда старый API принимает File, а новый — Path (или наоборот). Не храните пути как File; конвертируйте на границе API и держите Path в своём коде.
Подробный пример: объединение, resolve, relativize, normalize
Программа ниже проходит через каждую операцию Path, представленную в этой главе, на конкретных путях. Вывод делает наглядным разницу между resolve и resolveSibling, между normalize и toRealPath, а также между абсолютным и относительным аргументом resolve.
Что нужно понять из выполнения:
Path.of(\"/var\", \"log\", \"app\", \"today.log\")создал/var/log/app/today.logна Unix и\\var\\log\\app\\today.logна Windows. Позволить фабрике с переменными аргументами объединять компоненты — это переносимый способ; жёстко заданные/или\во входной строке — непереносимый. Используйте фабрику.- Строка
resolve(\"/etc/hosts\")отбросилаbaseи вернула/etc/hosts. Это поведение с абсолютным аргументом — наиболее частый источник вопроса «но я же указал базовый каталог, почему запись идёт в/etc/hosts?». Всегда проверяйте имена файлов, поступающие от пользователей, передresolve. Защитная форма:base.resolve(other).normalize().startsWith(base)— и даже у неё есть тонкие проблемы при наличии символических ссылок. base.relativize(target)вернулapp/today.log. Конкатенация обратно сbase.resolve(...)дала исходную цель — тождество кругового обхода. Используйте это, когда пишете строку лога или запись архива, которой нужна короткая относительная форма длинного абсолютного пути.Path.of(\"/var/log/../tmp/./a/b/../c\").normalize()дал/var/tmp/a/c. Преобразование было чисто строковым: каждый.удалён, каждая параname/..удалена. Файловая система не запрашивалась.toRealPathв следующей строке запросил файловую систему — поэтому результат оказался каноническим именем реального файла на диске, с разрешёнными символическими ссылками. (На macOS вы увидите, что путь к временному каталогу начинается с/private/var/folders/..., а не/var/folders/...:toRealPathследовал реальной символической ссылке/var→/private/var, чегоnormalizeникогда не сделает.) ПосколькуtoRealPathпроверяет каждый компонент, промежуточный каталогsubв примере должен реально существовать — именно поэтому программа создаёт его заранее.- Две проверки
equals:Path.of(\"/var/log\")иPath.of(\"/var\", \"log\")были равны (одинаковая внутренняя последовательность имён, одинаковая строка);Path.of(\"/var/log\")иPath.of(\"/var/log/.\")— нет (у одного есть конечный компонент., у другого нет). Вывод:equals— это строковое сравнение. Для проверки «указывают ли эти два пути на один и тот же файл?» используйтеFiles.isSameFile(a, b)из следующей главы — это единственная проверка, которая обращается к файловой системе.
Что дальше
Path — это существительное. Следующая глава, Класс Java Files, охватывает глагол — Files, огромный утилитарный класс с однострочными операциями над файловой системой: readString, writeString, createDirectories, copy, move, delete, walk и остальные.