W3docs

Класс 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 IOException

getAbsolutePath и 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);                            // FileFilter

FilenameFilter и 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.

java— editable, runs on the server

Что можно вынести из выполнения:

  • 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 — и какой из них выбирать для какой задачи.

Практика

Практика
`dir.listFiles()` для объекта `File`, указывающего на обычный файл (не каталог), возвращает…
`dir.listFiles()` для объекта `File`, указывающего на обычный файл (не каталог), возвращает…
Was this page helpful?