Цикл статей «Учебник Java 8».
Следующая статья — «Java 8 многопоточность».
Предыдущая статья — «Java 8 сериализация».
Содержание
— Проверка существования файла или каталога
— Проверка прав доступа к файлу или каталогу
— Копирование файла или каталога
— Перемещение файла или каталога
— Чтение, запись и создание файлов
— — OpenOption
— — Наиболее часто используемые методы для небольших файлов
— — Буферизированный ввод и вывод в текстовые файлы
— — Небуферезированный ввод и вывод
— — Создание файлов
— Файлы с произвольным доступом
— — Перечисление корневых каталогов файловой системы
— — Создание временного каталога
— — Перечисление содержимого каталога
— Символические и другие ссылки
— — Создание символических ссылок
— — Определение символической ссылки
— — Интерфейс java.nio.file.FileVisitor
— — Запуск процесса обхода дерева файлов
— — Управление обходом дерева файлов
— — Примеры
Подписываемся на изменения в каталоге
java.nio.file.Path
Path представляет из себя путь в файловой системе. Он содержит имя файла и список каталогов, определяющих путь к файлу.
Экземпляры Path отражают путь в конкретной платформе (например /home/jho/foo для Linux или C:\home\jho\foo для Windows). Экземпляры Path зависят от платформы. Нельзя сравнивать Path из Linux с путём из Windows, даже если структура их каталогов одинаковая, и оба этих экземпляра указывают на один и тот же относительный файл.
Path может указывать на файл или каталог, которых не существует. Методы Path работают только с представлением пути. Они не проверяют существование пути.
Создавать экземпляры Path можно разными способами.
Можно использовать методы java.nio.file.Paths.get() :
1 2 |
public static Path get(String first, String... more) |
1 |
public static Path get(URI uri) |
Примеры:
1 2 3 |
Path p1 = Paths.get("/tmp/foo"); Path p2 = Paths.get(args[0]); Path p3 = Paths.get(URI.create("file:///Users/joe/FileTest.java")); |
Эти методы являются сокращённой формой для следующего кода:
1 |
Path p4 = java.nio.file.FileSystems.getDefault().getPath("/users/sally"); |
Можно рассматривать Path как класс, сохраняющий имена каталогов в пути и имя файла в виде последовательности. Наивысший (ближний к корневому) элемент находится по индексу 0, самый нижний элемент находится по индексу n-1 , где n — количество элементов в пути.
Получить конкретный элемент пути можно с помощью метода getName() :
1 2 3 |
String element0 = path.getName(0) // Для пути /homme/jho/foo (Linux) вернётся home // Для пути C:\home\joe\foo (Windows) вернётся home. |
Узнать количество элементов в пути можно с помощью метода getNameCount():
1 2 3 |
int nameCount = path.getNameCount(); // Для /home/jho/foo (Linux) вернёт 3 // Для C:\home\jho\foo (Windows) вернёт 3 |
Можно получить путь родительской директории с помощью метода getParent() :
1 2 3 4 |
Path parentPath = path.getParent(); // Для /home/jho/foo (Linux) вернётся /home/jho // Для C:\home\jho\foo Windows вернётся C:\home\jho // Вернёт null, если родительского элемента нет. |
Можно получить корень пути с помощью метода getRoot() :
1 2 3 4 |
Path rootPath = path.getRoot(); // Для /home/jho/foo (Linux) вернёт / // Для C:\home\jho\foo (Windos) вернёт C:\ // Вернёт null, если путь относительный и корня нет. |
С помощью метода toString() можно получить путь в виде строки:
1 2 3 |
String str1 = path.toString() // Вернёт строку "/home/jho/foo" для пути /home/jho/foo (Linux). // Вернёт строку "C:\home\jho\foo" для пути C:\home\jho\foo (Windows). |
Многие файловые системы используют символ точки «.» для обозначения текущего каталога и две точки «..» для обозначения родительского каталога. Например:
1 2 3 |
/home/./jho/foo /home/sandy/../jho/foo |
Метод normalize() удаляет все подобные элементы и приводит к нормализованному пути /home/jho/foo/. Этот метод не проверяет файловую систему. Это чисто синтетическая операция, работающая с элементами Path. Если sandy является символической ссылкой, то удаление sandy/.. может привести к тому, что Path больше не указывает на предыдущий файл.
Если вам нужно преобразовать Path к строке, с помощью которой можно открыть файл в браузере, то используйте метод toUri():
1 2 3 |
Path p1 = Paths.get("/home/logfile"); // Result is file:///home/logfile System.out.format("%s%n", p1.toUri()); |
Метод toAbsolutePath() преобразует путь к абсолютному. Способ преобразования зависит от системы. Если переданный путь уже является абсолютным, то возвращается тот же самый объект Path.
Метод toRealPath() возвращает реальный путь существующего файла. В качестве параметра в метод можно передать константу перечисления java.nio.file.LinkOption с единственным возможным значением NOFOLLOW_LINKS . Метод бросает исключение, если файл не существует, либо к нему нет доступа. Этот метод убирает все элементы «.» и «..» и возвращает всегда абсолютный путь.
1 2 3 4 5 6 7 8 9 |
try { Path fp = path.toRealPath(); } catch (NoSuchFileException x) { System.err.format("%s: no such" + " file or directory%n", path); // Logic for case when file doesn't exist. } catch (IOException x) { System.err.format("%s%n", x); // Logic for other sort of file error. } |
Можно объединять пути с помощью метода resolve() , в который передаётся часть пути, которую нужно добавить к исходному пути:
1 2 3 4 |
// Solaris Path p1 = Paths.get("/home/joe/foo"); // Result is /home/joe/foo/bar System.out.format("%s%n", p1.resolve("bar")); |
Или для Windows:
1 2 3 4 |
// Microsoft Windows Path p1 = Paths.get("C:\\home\\joe\\foo"); // Result is C:\home\joe\foo\bar System.out.format("%s%n", p1.resolve("bar")); |
С помощью метода relativize() можно создать относительный путь от одного пути к другому:
1 2 3 4 5 6 7 8 |
Path p1 = Paths.get("jho"); Path p2 = Paths.get("sandy"); // Так как нет другой информации, то считается, что // jho и sandy находятся в одном каталоге. // Result is ../sandy Path p1_to_p2 = p1.relativize(p2); // Result is ../jho Path p2_to_p1 = p2.relativize(p1); |
Слегка усложнённый пример:
1 2 3 4 5 6 |
Path p1 = Paths.get("home"); Path p3 = Paths.get("home/sandy/bar"); // Result is sandy/bar Path p1_to_p3 = p1.relativize(p3); // Result is ../.. Path p3_to_p1 = p3.relativize(p1); |
Относительный путь не может быть получен только в том случае, когда только один из путей имеет корневой элемент. Если оба пути имеют корневой элемент, но возможность построения относительного пути зависит от системы.
Path поддерживает метод equals(), что позволяет сравнивать пути. Так же есть методы startsWith(Path other) , startsWith(String other) , endsWith(Path other) , endsWith(String other) , позволяющие проверять начало и конец пути на совпадение с указанной строкой или частью пути. Path реализует интерфейс Comparable, что позволяет сортировать строки.
Что такое Glob?
Некоторые методы класса java.nio.file.Files принимают аргумент glob. Шаблон glob использует следующие правила:
Символ «*» обозначает любое количество символов (включая отсутствие символов).
Две снежинки «**» работают так же, как и одна, но переходят за границы каталогов.
Символ вопроса «?» обозначает ровно один символ.
Фигурные скобки указывают коллекцию паттернов. Например {sun,moon,starts} совпадает с "sun" , "moon" или "starts". {temp*, tmp*} совпадает со всеми строками, начинающимися с "temp" или "tmp".
Квадратные скобки позволяют указать набор символов либо диапазон символов:
[aeiou] обозначает любую строчную гласную.
[0-9] обозначает любую цифру
[A-Z] обозначает любую прописную букву.
[a-z,A-Z] обозначает любую строчную или прописную букву.
Внутри квадратных скобок «*», «?» и «/» обозначают самих себя.
java.nio.file.Files
Проверка существования файла или каталога
Проверить существование пути Path можно с помощью методов:
1 2 |
public static boolean exists(Path path, LinkOption... options) |
1 2 |
public static boolean notExists(Path path, LinkOption... options) |
Если передать константу LinkOption.NOFOLLOW_LINKS , то метод не будет проходить по символическим ссылкам. Если оба метода exists() и notExists() возвращают false , то существование файла не может быть проверено (например нет доступа).
Проверка прав доступа к файлу или каталогу
Проверка доступа к файлу осуществляется с помощью методов:
1 2 |
// Проверяет, что файл доступен для чтения public static boolean isReadable(Path path) |
1 2 |
// Проверяет, что файл доступен для записи public static boolean isWritable(Path path) |
1 2 |
// Проверяет, что файл доступен для выполнения public static boolean isExecutable(Path path) |
Один и тот же файл
Можно проверить, что два пути указывают на один и тот же файл на одной и той же файловой системе:
1 2 3 |
public static boolean isSameFile(Path path, Path path2) throws IOException |
Удаление файла или каталога
Метод delete(Path) удаляет файл или бросает исключение, если удалить файл не удалось. Можно удалять каталог, но только если он пустой.
1 2 3 4 5 6 7 8 9 10 |
try { Files.delete(path); } catch (NoSuchFileException x) { System.err.format("%s: no such" + " file or directory%n", path); } catch (DirectoryNotEmptyException x) { System.err.format("%s not empty%n", path); } catch (IOException x) { // File permission problems are caught here. System.err.println(x); } |
Метод
1 2 |
public static boolean deleteIfExists(Path path) throws IOExceptio |
тоже удаляет файл, но не бросает никаких исключений, если файл не существует. Это может быть полезно, если два потока удаляют файл и вы не хотите получать исключение из-за того, что другой поток уже удалил файл до этого.
Копирование файла или каталога
Можно копировать файл или каталог с помощью метода copy(Path, Path, CopyOption...) , но имейте в виду, что файлы внутри каталога не копируются этим методом. Метод принимает константы CopyOption:
StandardCopyOption.REPLACE_EXISTING заменять существующие файлы.
StandardCopyOption.COPY_ATTRIBUTES копирует атрибуты файла.
LinkOption.NOFOLLOW_LINKS не переходить по символическим ссылкам.
Перемещение файла или каталога
Можно переместить файл или каталог с помощью метода move(Path, Path, CopyOption...) . Можно перемещать пустые каталоги. Возможность перемещения каталогов с содержимым зависит от платформы. Метод принимает CopyOption:
- StandardCopyOption.REPLACE_EXISTING заменяет существующий файл, если он существует.
- StandardCopyOption.ATOMIC_MOVE попытка осуществить перемещение файла как единую атомарную операцию. Все остальные опции игнорируются.
Управление метаданными
Метаданные файлов это: размер, дата создания, дата последнего изменения, владелец, права доступа и прочее.
Метаданные файлов и каталогов часто называют атрибутами файлов.
Методы для работы с метаданными:
1 2 |
public static long size(Path path) throws IOException |
Возвращает размер файла в байтах.
1 2 |
public static boolean isDirectory(Path path, LinkOption... options) |
Проверяет, что path указывает на каталог. Можно указать LinkOption.NOFOLLOW_LINKS , чтобы метод не переходил по символическим ссылкам.
1 2 |
public static boolean isRegularFile(Path path, LinkOption... options) |
Возвращает true, если path указывает на обычный файл. Можно передать LinkOption.NOFOLLOW_LINKS, чтобы метод не переходил по символическим ссылкам.
1 |
public static boolean isSymbolicLink(Path path) |
Возвращает true , если path указывает на символическую ссылку.
1 2 |
public static boolean isHidden(Path path) throws IOException |
Возвращает true, если файл является скрытым. Для Linux файл является скрытым, если его имя начинается с точки. Для Windows файл является скрытым, если установлен соответствующий атрибут.
1 2 3 |
public static FileTime getLastModifiedTime(Path path, LinkOption... options) throws IOException |
Возвращает FileTime с датой последнего изменения файла. Можно передать LinkOption.NOFOLLOW_LINKS , чтобы метод не переходил по символическим ссылкам.
1 2 3 |
public static Path setLastModifiedTime(Path path, FileTime time) throws IOException |
Устанавливает дату последнего изменения файла. Смотрите описание класса FileTime в документации Oracle.
1 2 3 |
public static UserPrincipal getOwner(Path path, LinkOption... options) throws IOException |
Возвращает владельца файла. Можно использовать метод String getName() у возвращённого объекта, чтобы получить имя пользователя.
1 2 3 |
public static Path setOwner(Path path, UserPrincipal owner) throws IOException |
Меняет владельца файла.
Различные файловые системы имеют различные атрибуты файлов. Можно считывать группы атрибутов:
- BasicFileAttributeView — базовые атрибуты файлов, которые должны поддерживаться всеми реализациями файловых систем.
- DosFileAttributeView — расширяет стандартные атрибуты 4 битам (скрытый, архивный, только чтение, системный).
- PosixFileAttributeView — расширяет базовые атрибуты атрибутами системы Linux.
- FileOwnerAttributeView — поддерживается всеми файловыми системами, которые поддерживают владельцев файлов.
- AclFileAttributeView — права доступа к файлу, которые реализованы в Windows.
- UserDefinedFileAttributeView — пользовательские метаданные.
Получение конкретной группы атрибутов происходит с помощью метода
1 2 3 |
public static <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) |
Пример:
1 2 3 4 5 6 |
Path path = ... AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class); if (view != null) { List<AclEntry> acl = view.getAcl(); // ... } |
Работа с конкретными группами метаданных/атрибутов очень зависит от платформы. Вам вряд ли когда-нибудь придётся столкнуться с этим. Но если интересно, то рекомендую рассмотреть мой проект niofilecommander, который использует все группы атрибутов, описанные здесь.
Чтение, запись и создание файлов
OpenOption
Многие методы, описанные в этом разделе используют OpenOption в качестве своих параметров. В этом случае в метод можно передавать следующие значения:
- LinkOption.NOFOLLOW_LINKS не переходить по символическим ссылкам.
- StandardOpenOption.APPEND если файл открыт для записи, то байты будут добавляться в конец файла, а не в начало.
- StandardOpenOption.CREATE создавать новый файл, если его нет.
- StandardOpenOption.CREATE_NEW создавать новый файл. Если файл уже существует, то происходит ошибка.
- StandardOpenOption.DELETE_ON_CLOSE удалять файл при закрытии.
- StandardOpenOption.DSYNC каждое обновление содержимого файла синхронного пишется на устройство хранения (жёсткий диск).
- StandardOpenOption.READ открыть для чтения
- StandardOpenOption.SPARCE sparce file
- StandardOpenOption.SYNC каждое обновление содержимого файла или метаданных синхронно пишется на устройство чтения (жёсткий диск).
- StandardOpenOption.TRUNCATE_EXISTING если файл уже существует и открывается для записи, то его длина устанавливается в 0.
- StandardOpenOption.WRITE открывает файл на запись.
Наиболее часто используемые методы для небольших файлов
1 2 |
public static byte[] readAllBytes(Path path) throws IOException |
Читает содержимое файла и возвращает его в массиве байт.
1 2 3 |
public static List<String> readAllLines(Path path, Charset cs) throws IOException |
Читает содержимое текстового файла и возвращает его в виде списка строк.
1 2 3 4 |
public static Path write(Path path, byte[] bytes, OpenOption... options) throws IOException |
Записывает байты в файл.
1 2 3 4 5 |
public static Path write(Path path, Iterable<? extends CharSequence> lines, Charset cs, OpenOption... options) throws IOException |
Записывает строки в файл, преобразуя их в указанную кодировку.
Буферизированный ввод и вывод в текстовые файлы
1 2 3 |
public static BufferedReader newBufferedReader(Path path, Charset cs) throws IOException |
Возвращает BufferedReader, который может быть использован для чтения из текстового файла.
1 2 3 4 |
public static BufferedWriter newBufferedWriter(Path path, Charset cs, OpenOption... options) throws IOException |
Открывает или создаёт файл для записи. Возвращаемый BufferedWriter может быть использован для записи в файл. Если OpenOption не переданы, то используются опции: CREATE , TRUNCATE_EXISTING , WRITE.
Небуферезированный ввод и вывод
1 2 3 |
public static InputStream newInputStream(Path path, OpenOption... options) throws IOException |
Возвращает поток, который можно использовать для чтения байт из файла.
1 2 3 |
public static OutputStream newOutputStream(Path path, OpenOption... options) throws IOException |
Возвращает поток, который можно использовать для записи байт в файл. Если OpenOption не переданы, то используются опции: CREATE, TRUNCATE_EXISTING, WRITE.
Создание файлов
1 2 3 |
public static Path createFile(Path path, FileAttribute<?>... attrs) throws IOException |
Создаёт новый и пустой файл. Бросает исключение, если файл уже существует.
Создание временных файлов
1 2 3 4 5 |
public static Path createTempFile(Path dir, String prefix, String suffix, FileAttribute<?>... attrs) throws IOException |
Создаёт временный файл в указанном каталоге. Является только частью работы с временными файлами. Вы можете использовать метод File.deleteOnExit(), для того чтобы каталог удалялся автоматически.
1 2 3 4 |
public static Path createTempFile(String prefix, String suffix, FileAttribute<?>... attrs) throws IOException |
Создаёт временный файл в специальном каталоге для временных файлов. Является только частью работы с временными файлами. Вы можете использовать метод File.deleteOnExit(), для того чтобы каталог удалялся автоматически.
Файлы с произвольным доступом
Файл можно открыть одновременно для чтения и записи и с возможностью перемещения текущей позиции в любое место файла. Подобная функциональность реализуется с помощью интерфейса java.nio.channels.SeekableByteChannel.
Получить экземпляр класса, реализующего интерфейс SeekableByteChannel, можно с помощью статического метода из класса Files:
1 2 3 |
public static SeekableByteChannel newByteChannel(Path path, OpenOption... options) throws IOException |
либо:
1 2 3 4 |
public static SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException |
Пример использования:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
String s = "I was here!\n"; byte data[] = s.getBytes(); ByteBuffer out = ByteBuffer.wrap(data); ByteBuffer copy = ByteBuffer.allocate(12); try (FileChannel fc = (FileChannel.open(file, READ, WRITE))) { // Читаем первые 12 // байт из файла. int nread; do { nread = fc.read(copy); } while (nread != -1 && copy.hasRemaining()); // Пишем "I was here!" в начало файла. fc.position(0); while (out.hasRemaining()) fc.write(out); out.rewind(); // Перемещаемся в конец файла. Копируем первые 12 байт в // конец файла. Пишем "I was here!" снова. long length = fc.size(); fc.position(length-1); copy.flip(); while (copy.hasRemaining()) fc.write(copy); while (out.hasRemaining()) fc.write(out); } catch (IOException x) { System.out.println("I/O Exception: " + x); } |
Создание и чтение каталогов
Перечисление корневых каталогов файловой системы
Для этого используется специальный метод класса FileSystem :
1 |
public abstract Iterable<Path> getRootDirectories() |
Пример:
1 2 3 4 |
Iterable<Path> dirs = FileSystems.getDefault().getRootDirectories(); for (Path name: dirs) { System.err.println(name); } |
Создание каталога
Метод класса Files :
1 2 3 |
public static Path createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException |
Создаёт каталог.
1 2 3 |
public static Path createDirectories(Path dir, FileAttribute<?>... attrs) throws IOException |
Создаёт каталог вместе со всеми родительскими каталогами, которых ещё нет.
Создание временного каталога
1 2 3 4 |
public static Path createTempDirectory(Path dir, String prefix, FileAttribute<?>... attrs) throws IOException |
Создаёт временный каталог. Как и методы createTempFile является только частью работы с временными файлами. Вы можете использовать метод File.deleteOnExit(), для того чтобы каталог удалялся автоматически.
1 2 3 |
public static Path createTempDirectory(String prefix, FileAttribute<?>... attrs) throws IOException |
Создаёт временный каталог в специалном каталоге для временных файлов. Как и методы createTempFile является только частью работы с временными файлами. Вы можете использовать метод File.deleteOnExit(), для того чтобы каталог удалялся автоматически.
Перечисление содержимого каталога
1 2 |
public static DirectoryStream<Path> newDirectoryStream(Path dir) throws IOException |
Возвращает поток, который позволяет пройтись по всем элементам каталога.
Пример использования:
1 2 3 4 5 6 7 8 9 10 11 |
Path dir = ...; try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) { for (Path file: stream) { System.out.println(file.getFileName()); } } catch (IOException | DirectoryIteratorException x) { // IOException не может броситься во время итерации. // В этом куске кода оно может броситься только // методом newDirectoryStream. System.err.println(x); } |
1 2 3 |
public static DirectoryStream<Path> newDirectoryStream(Path dir, String glob) throws IOException |
Позволяет посмотреть элементы каталога, названия которых удовлетворяют переданному шаблону Glob.
Например, следующий код возвращает список файлов с расширениями «.class», «.java», «.jar»:
1 2 3 4 5 6 7 8 9 10 11 12 |
Path dir = ...; try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.{java,class,jar}")) { for (Path entry: stream) { System.out.println(entry.getFileName()); } } catch (IOException x) { // IOException никогда не бросится во время итерации. // В этом куске кода оно может броситься только // методом newDirectoryStream System.err.println(x); } |
Можно написать свой собственный фильтр для содержимого каталога. Например, этот фильтр будет возвращать только каталоги:
1 2 3 4 5 6 7 8 9 10 11 12 |
DirectoryStream.Filter<Path> filter = newDirectoryStream.Filter<Path>() { public boolean accept(Path file) throws IOException { try { return (Files.isDirectory(path)); } catch (IOException x) { // Failed to determine if it's a directory. System.err.println(x); return false; } } }; |
Фильтр применяется методом:
1 2 3 |
public static DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException |
Пример:
1 2 3 4 5 6 7 8 9 |
Path dir = ...; try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, filter)) { for (Path entry: stream) { System.out.println(entry.getFileName()); } } catch (IOException x) { System.err.println(x); } |
Символические и другие ссылки
Каждый метод класса Path автоматически обрабатывает символические ссылки, либо предоставляет опции, которые позволяют указать, каким образом их обрабатывать.
Ссылки бывают символическими и жёсткими. Жёсткие ссылки имеют больше ограничений, чем символические:
- Цель, на которую указывает жёсткая ссылка, должна существовать.
- Жёсткие ссылки обычно не могут указывать на каталоги.
- Жёсткие ссылки не могут указывать на другой раздел или том и не могут указывать на другую файловую систему.
- Жёсткие ссылки выглядят и ведут себя так, как обычный файл, поэтому их может быть трудно обнаружить.
- Жёсткая ссылка для всех целей и действий является той же сущностью, что и исходный файл. Они имеют те же самые права доступа, временные метки и т. д.
Из-за таких ограничений жёсткие ссылки не так часто используются, как символические ссылки.
Создание символических ссылок
Если ваша файловая система поддерживает символические ссылки, то вы можете создавать их с помощью метода класса Files:
1 2 3 4 |
public static Path createSymbolicLink(Path link, Path target, FileAttribute<?>... attrs) throws IOException |
Этот метод создаёт символическую ссылку по пути link, указывающую на target. Параметр attrs позволяет задать атрибуты ссылки.
Создание жёстких ссылок
1 2 3 |
public static Path createLink(Path link, Path existing) throws IOException |
Создаёт жёсткую ссылку по пути link, указывающую на existing.
Определение символической ссылки
1 |
public static boolean isSymbolicLink(Path path) |
Этот метод класса Files возвращает true, если файл является символической ссылкой.
Нахождение цели ссылки
1 2 |
public static Path readSymbolicLink(Path link) throws IOException |
Этот метод класса Files возвращает путь, на который указывает символическая ссылка. Если link не является ссылкой, то бросается исключение java.nio.file.NotLinkException.
Обход дерева файлов
Интерфейс java.nio.file.FileVisitor
Чтобы обходить дерево файлов нужно сперва написать класс, реализующий интерфейс FileVisitor. Этот интерфейс позволяет описать действия, которые будут происходить в основные моменты обхода дерева файлов: при посещении файла, перед доступом к каталогу, после доступа к каталогу и при возникновении ошибки. FileVisitor имеет четыре метода для этих ситуаций:
- preVisitDirectory — вызывается перед посещением элементов каталога.
- postVisitDirectory — вызывается после посещения всех элементов каталога.
- visitFile — вызывается при посещении файла. В метод передаются BasicFileAttributes.
- visitFileFailed — вызывается в случае, когда не удаётся получить доступ к файлу.
Если вам не нужны все эти четыре метода, то вы можете вместо реализации интерфейса FileVisitor унаследоваться от java.nio.file.SimpleFileVisitor. Этот класс реализует FileVisitor и посещает все файлы в дереве, бросая исключение IOException при возникновении ошибки. Вы можете отнаследоваться от этого класса и реализовать только те методы, которые вам нужны.
Пример реализации SimpleFileVisitor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import static java.nio.file.FileVisitResult.*; public static class PrintFiles extends SimpleFileVisitor<Path> { // Выводит в консоль информацию о // каждом типе файлов. @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attr) { if (attr.isSymbolicLink()) { System.out.format("Symbolic link: %s ", file); } else if (attr.isRegularFile()) { System.out.format("Regular file: %s ", file); } else { System.out.format("Other: %s ", file); } System.out.println("(" + attr.size() + "bytes)"); return CONTINUE; } // Пишет в консоль каждый посещаяемый каталог @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) { System.out.format("Directory: %s%n", dir); return CONTINUE; } // Если возникла какая-нибудь ошибка при доступе к файлу, // то выводим эту ошибку. // Если вы не переопределите этот метод, и возникнет // ошибка, то бросится исключение IOException @Override public FileVisitResult visitFileFailed(Path file, IOException exc) { System.err.println(exc); return CONTINUE; } } |
Запуск процесса обхода дерева файлов
1 2 3 |
public static Path walkFileTree(Path start, FileVisitor<? super Path> visitor) throws IOException |
или
1 2 3 4 5 |
public static Path walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth, FileVisitor<? super Path> visitor) throws IOException |
Второй метод позволяет задать максимальную глубину обхода и FileVisitOpion.FOLLOW_LINKS, что позволит заходить в символические ссылки.
Пример для нашего PrintFiles :
1 2 3 |
Path startingDir = ...; PrintFiles pf = new PrintFiles(); Files.walkFileTree(startingDir, pf); |
Размышления о FileVisitor
Дерево файлов обходится в глубину, но вы не можете предугадать порядок посещения подкаталогов.
Если ваша программа делает изменения в файловой системе, то вам нужно внимательно реализовывать ваш FileVisitor.
Например, если вы пишете рекурсивное удаление, то сначала вам нужно удалить файлы в каталоге, а затем удалят сам каталог. В этом случае удалять каталог нужно в методе postVisitDirectory.
Если вы пишете рекурсивное копирование, то вам сначала нужно создавать каталог в preVisitDirectory, а лишь потом копировать файлы в visitFiles. Если вам нужно скопировать атрибуты с исходного каталога, то это нужно делать после копирования каталога, то есть в postVisitDirectory.
Если вы пишете поиск файла, то осуществляйте сравнение в visitFile, если вам нужно находить не только файлы, но и каталоги, то используйте в добавок метод preVisitDirectory или postVisitDirectory.
Вам также нужно решить, как обрабатывать символические ссылки. Если вы удаляете файлы, то переходить по символическим ссылкам, скорее всего, не нужно. Если вы копируете дерево файлов, то вам потребуется переходить по ссылкам, скорее всего. По умолчанию метод walkFileTree не переходит по символическим ссылкам.
Метод visitFile вызывается для файлов. Если вы указали FOLLOW_LINKS, и ваше дерево файлов имеет циклическую ссылку на родительский каталог, то во время повторение каталога будет сообщено в visitFileFailed с помощью исключения FileSystemLoopException.
Управление обходом дерева файлов
Во время обхода может потребоваться не заходить внутрь какого-либо каталога либо даже прервать обход дерева. Методы FileVisitor возвращают java.nio.file.FileVisitResult, которое является перечислением со следующими константами:
- FileVisitResult.CONTINUE — указывает, что обход дерева файлов должен продолжаться. Если метод preVisitDirectory возвращает CONTINUE , то каталог посещаяется.
- FileVisitResult.TERMINATE — прерывает обход всего дерева файлов.
- FileVisitResult.SKIP_SUBTREE — Если preVisitDirectory возвращает это значение, то указанный каталог и его подкаталоги пропускаются.
- FileVisitResult.SKIP_SIBLINGS — Если preVisitDirectory возвращает это значение, то указанный каталог не посещается, и postVisitDirectory не вызывается. Если возвращается из postVisitDirectory, то остальные каталоги с тем же родительским каталогом пропускаются.
Примеры
Смотрите примеры в niofilecommander.
Поиск файлов
Каждая реализация файловой системы поставляет свою реализацию интерфейса java.nio.file.PathMatcher.
Вы можете получить экземпляр класса, реализующий этот интерфейс для файловой системы вот так:
1 2 3 |
String pattern = ...; PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern); |
Строка, которая передаётся в этот метод, указывает какие файлы искать. В этом примере используется синтаксис glob. Можно использовать регулярные выражения, тогда вместо "glob:" нужно писать "regex:".
В дальнейшем этот matcher можно использовать при обходе дерева файлов вот так:
1 2 3 |
if (matcher.matches(filename)) { System.out.println(filename); } |
Подписываемся на изменения в каталоге
Последовательность шагов, которую нужно проделать, для того чтобы следить за изменениями в каком-нибудь каталоге:
- Создаём экземпляр WatchService для файловой системы.
- Для каждого каталога, за которым нужно наблюдать, регистрируем наблюдателя (watcher). При регистрации каталога вы указываете события, о которых вам бы хотелось знать. Для каждого каталога вы получаете экземпляр класса WatchKey.
- Делаем бесконечный цикл на входящих событиях. При возникновении события WatchKey этого каталога получает сигнал и кладётся в очередь наблюдателя.
- Получаем WatchKey из очереди. Вы можете получить имя файла из этого ключа.
- Получаем каждое событие для ключа (их может быть несколько) и обрабатываем.
- Сбрасываем WatchKey и возвращаемся в ожиданию событий.
- Закрываем WatchServce. Он закрывается при выходе из потоки либо при вызове метода.
Экземпляры WatchKey потокобезопасны.
Пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import static java.nio.file.StandardWatchEventKinds.*; ... WatchService watcher = FileSystems.getDefault().newWatchService(); Path dir = ...; try { WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); } catch (IOException x) { System.err.println(x); } |
Обработка событий
Сначала мы получаем WatchKey с помощью одного из методов WatchService :
1 |
WatchKey poll() |
Удаляет из очереди и возвращает следующий WatchKey либо null , если очередь пуста.
1 2 3 |
WatchKey poll(long timeout, TimeUnit unit) throws InterruptedException |
Удаляет из очереди и возвращает следующий WatchKey. Ждёт, если нужно, указанное количество времени.
1 2 |
WatchKey take() throws InterruptedException |
Удаляет из очереди и возвращает следующий WatchKey. Если очередь пуста, то ждёт до тех пор, пока не появится хоть что-нибудь.
Затем обрабатываются все события из WatchKey, которые получаются при вызове его метода:
1 |
List<WatchEvent<?>> pollEvents() |
Тип события получается с помощью следующего метода WatchEvent :
1 |
WatchEvent.Kind<T> kind() |
Не зависимо от событий, на которые мы подписаны, мы может получить событие OVERFLOW.
Получаем имя файла из события с помощью метода context() , который возвращает относительный путь к файлу от каталога, на изменения в котором мы подписаны.
Вызываем reset() на WatchKey . Если он вернул false, то он больше не работает и из цикла можно выходить.
Пример обработки событий изменения содержимого каталога можно увидеть в niofilecommander, который обновляет свои панели при добавлении, переименовании и удалении файлов в них.
Цикл статей «Учебник Java 8».
Следующая статья — «Java 8 многопоточность».
Предыдущая статья — «Java 8 сериализация».
Здавствуйте!
Зачем нужны циклы?
while (out.hasRemaining())
fc.write(out);