Класс File в Java
Работа с путями файловой системы в Java через устаревший класс java.io.File — exists, isFile, isDirectory, listFiles.
java.io.File — это исходный тип «эта строка является путём» из Java 1.0. Сам класс не выполняет никакого ввода-вывода — он не открывает, не читает и не записывает данные — он просто называет местоположение в файловой системе и предлагает несколько методов для запроса ОС об этом местоположении и выполнения одиночных операций над ним (exists, isDirectory, delete, renameTo, listFiles).
java.nio.file.Path (Java 7) — это современная замена, которую следует использовать в новом коде, но File встречается в каждой кодовой базе старше ~2012 года, и многие старые API по-прежнему принимают и возвращают его. Эта глава описывает, что он делает, где находятся границы и как он взаимодействует с Path.
Конструирование
File оборачивает строку пути. Четыре конструктора покрывают распространённые случаи:
File a = new File("data/users.txt"); // relative to the JVM's working directory
File b = new File("/var/log/app.log"); // absolute
File c = new File("/tmp", "session.txt"); // parent + child
File d = new File(new File("/tmp"), "session.txt"); // parent File + childКонструктор не выполняет проверку — передача бессмысленного пути создаёт File без ошибок; только при вызове exists(), delete() и т.д. к ОС происходит обращение.
Используйте двухаргументный конструктор для «родитель + имя» вместо конкатенации строк. Он выбирает правильный разделитель (/ в Unix, \ в Windows) и позволяет избежать ошибки, при которой родительский путь может или не может заканчиваться разделителем:
File good = new File(parentDir, "data.txt"); // separator handled for you
File bad = new File(parentDir + "/data.txt"); // brittle: depends on parentDir's exact stringЗапрос файловой системы
File предоставляет широкий набор запросов, возвращающих boolean и long. Наиболее распространённые:
File f = new File("data/users.txt");
f.exists(); // does the path point to anything?
f.isFile(); // is it a regular file?
f.isDirectory(); // is it a directory?
f.isHidden(); // hidden by OS convention (leading dot on Unix, hidden attr on Windows)
f.length(); // size in bytes (0 for a directory)
f.lastModified(); // epoch millis; 0 if it doesn't exist or can't be queried
f.canRead(); // permission check from the JVM's point of view
f.canWrite();
f.canExecute();Каждый из этих вызовов обращается к ОС. По отдельности они недороги, но не бесплатны — вызов exists(), затем isDirectory(), затем length() — это три системных вызова. Если нужно получить несколько атрибутов одного файла, Files.readAttributes(path, BasicFileAttributes.class) (следующая часть) выполняет один системный вызов вместо трёх.
Представления пути
File предоставляет несколько способов взглянуть на одну и ту же базовую строку:
File f = new File("data/../data/users.txt");
f.getName(); // "users.txt" — last component
f.getParent(); // "data/../data" — String, or null at the root
f.getParentFile(); // File for the parent, or null
f.getPath(); // "data/../data/users.txt" — what you constructed
f.getAbsolutePath(); // resolved against working dir, NOT canonicalised
f.getCanonicalPath(); // resolved, normalised, symlinks followed — can throw IOExceptiongetAbsolutePath и getCanonicalPath — наиболее путаемая пара в классе:
getAbsolutePath— добавляет текущий рабочий каталог JVM, если путь относительный. Возвращает строку с сегментами.., которые по-прежнему в ней присутствуют.getCanonicalPath— то же, что absolute, затем разрешает..и., затем следует по символическим ссылкам. Может обращаться к диску и выбрасыватьIOException.
Для проверок, важных с точки зрения безопасности (находится ли этот путь, предоставленный пользователем, внутри разрешённого каталога?), getCanonicalPath — единственный безопасный вариант — иначе относительный путь вроде safe-dir/../../../etc/passwd обходит проверку startsWith("safe-dir").
Перечисление содержимого каталога
Четыре варианта, две пары:
File dir = new File("/tmp");
String[] names = dir.list(); // child names, no metadata
File[] children = dir.listFiles(); // child File objects
String[] txt = dir.list((d, name) -> name.endsWith(".txt")); // FilenameFilter
File[] files = dir.listFiles(File::isFile); // FileFilterFilenameFilter и FileFilter — оба функциональные интерфейсы с одним методом (словарь из части 12), поэтому лямбда или ссылка на метод работают напрямую. Разница: FilenameFilter получает родительский каталог и простое имя; FileFilter получает сконструированный дочерний File. Используйте FileFilter, если нужно вызвать isDirectory() или length() для принятия решения; используйте FilenameFilter, если достаточно сопоставления по имени.
Все четыре метода возвращают null, если путь не является каталогом — они не выбрасывают исключений. Это классический источник NPE:
for (File child : dir.listFiles()) { ... } // NPE if dir is not a directory!
File[] children = dir.listFiles();
if (children != null) for (File c : children) { ... } // correctСовременный Files.list(path) возвращает пустой Stream<Path> для несуществующего каталога или выбрасывает понятное исключение NotDirectoryException. API File просто возвращает null и позволяет вам упасть.
Создание, удаление, переименование
File предоставляет несколько методов мутации:
f.createNewFile(); // creates an empty file; returns boolean; throws IOException on real failure
f.mkdir(); // creates this directory; parent must exist
f.mkdirs(); // creates this directory and any missing parents
f.delete(); // deletes this file or empty directory; returns boolean
f.renameTo(other); // OS-specific behaviour; returns booleanПовторяющаяся тема — возвращаемые значения boolean, которые не объясняют причину — является главной причиной существования Files. f.delete() возвращает false, если файл не существовал, если не хватало прав, если это был непустой каталог или если другой процесс держал его открытым в Windows. По возвращаемому значению невозможно определить причину. Соответствующий Files.delete(path) выбрасывает конкретное исключение (NoSuchFileException, AccessDeniedException, DirectoryNotEmptyException) и является API для полноценной обработки ошибок.
renameTo — самый проблематичный: он может завершиться неудачно, не выбросив никакого исключения, а режимы отказа (переименование между томами, цель существует, права, блокировка) зависят от ОС. Files.move(src, dst, REPLACE_EXISTING) — современная замена, которая сообщает о причине неудачи.
Мост к Path
Каждый File знает свой Path и наоборот:
File f = new File("data/users.txt");
Path p = f.toPath(); // bridge to java.nio.file
File g = p.toFile(); // bridge backОни взаимодействуют с небольшими издержками. Когда вы работаете с устаревшим API, возвращающим File, правильным решением обычно является f.toPath(), а затем вызов Files.* на нём. Новый код должен начинаться с Path.of(...) и конвертироваться в File только в точке вызова устаревшего метода.
Рабочий пример: построение дерева и обход его с помощью File
Программа ниже создаёт небольшое дерево каталогов в системном временном каталоге, наполняет его несколькими файлами, затем запрашивает каждую запись с помощью File. Она демонстрирует лямбды FilenameFilter и FileFilter, ловушку возврата null, разрешение канонического пути и проблему отсутствия информации об ошибке при delete(). Все созданные объекты очищаются с помощью deleteOnExit.
Что можно вынести из выполнения:
a.getCanonicalPath()вывел нормализованный абсолютный путь без сегментов...getAbsolutePath()не нормализует — для проверки безопасности канонический путь — это та версия, которую нужно сравнивать с разрешённым префиксом.- Форма
FilenameFilter(d, name) -> name.endsWith(".txt")— это лямбда с двумя аргументами;File::isFile— ссылка на метод для одноаргументногоFileFilter. Оба являются функциональными интерфейсами, тот же словарь, что и в части 12 —Fileбыл «готов к лямбдам» задолго до того, как лямбды появились. notDir.listFiles()вернулnull, потому чтоdata.csvне является каталогом. Циклforполучил бы NPE, если бы мы пропустили проверку на null.Files.list(path)выбрасывает понятное исключение в том же случае.ghost.delete(),a.delete()иsub.delete()— все вернулиboolean. Первые два легко интерпретировать; третий вернулfalse, потому что каталог не был пустым, и API не дал нам ничего, чтобы отличить «не был пустым» от «не было прав». Это пробел, который закрываетFiles.delete(path).root.toPath()— это мост вjava.nio.file. Как только у вас естьPath, применяется весь остаток части 13 —Files.readString,Files.lines,Files.walk, всеstatic-вспомогательные методы.
Что дальше
Следующая глава, Создание файлов в Java, рассматривает три способа создать новый файл или каталог — устаревшие File.createNewFile и mkdir(s), а также современные Files.createFile, Files.createDirectory и Files.createDirectories — и какой из них выбирать для какой задачи.