Статьи

Java 7: копирование и перемещение файлов и каталогов

Этот пост является продолжением моей серии о пакете Java 7 java.nio.file, на этот раз посвященной копированию и перемещению файлов и полных деревьев каталогов. Если вы когда-либо были разочарованы отсутствием в Java методов копирования и перемещения, тогда читайте дальше, потому что облегчение не за горами. В покрытие включен очень полезный метод Files.walkFileTree. Однако прежде чем мы углубимся в основное содержание, потребуется некоторая справочная информация.

Объекты пути
Объекты пути представляют собой последовательность каталогов, которые могут включать или не включать файл. Существует три способа создания объекта Path:

  1. FileSystems.getDefault (). GetPath (сначала строка, строка… больше)
  2. Paths.get (String path, String… more), вспомогательный метод, который вызывает FileSystems.getDefault (). GetPath
  3. Вызов метода toPath для объекта java.io.File

С этого момента во всех наших примерах мы будем использовать метод Paths.get. Вот несколько примеров создания объектов Path:

1
2
3
4
//Path string would be "/foo"
Paths.get("/foo");
//Path string "/foo/bar"
Paths.get("/foo","bar");

Для манипулирования объектами Path есть методы Path.resolve и Path.relativize. Вот пример использования Path.resolve:

1
2
3
4
//This is our base path "/foo"
Path base = Paths.get("/foo");
//filePath is "/foo/bar/file.txt" while base still "/foo"
Path filePath = base.resolve("bar/file.txt");

Использование метода Path.resolve добавит указанный объект String или Path в конец вызывающего Path, если только указанная String или Path не представляет абсолютный путь, данный путь возвращается, например:

1
2
3
Path path = Paths.get("/foo");
//resolved Path string is "/usr/local"
Path resolved = path.resolve("/usr/local");

Path.relativize работает противоположным образом, возвращая новый относительный путь, который при разрешении относительно вызывающего Path приведет к той же строке Path. Вот пример:

1
2
3
4
5
6
7
8
// base Path string "/usr"
        Path base = Paths.get("/usr");
        // foo Path string "/usr/foo"
        Path foo = base.resolve("foo");
        // bar Path string "/usr/foo/bar"
        Path bar = foo.resolve("bar");
        // relative Path string "foo/bar"
        Path relative = base.relativize(bar);

Еще один полезный метод класса Path — это Path.getFileName, который возвращает имя самого дальнего элемента, представленного этим объектом Path, с именем, являющимся фактическим файлом или просто каталогом. Например:

1
2
3
4
5
6
7
//assume filePath constructed elsewhere as "/home/user/info.txt"
//returns Path with path string "info.txt"
filePath.getFileName()
 
//now assume dirPath constructed elsewhere as "/home/user/Downloads"
//returns Path with path string "Downloads"
dirPath.getFileName()

В следующем разделе мы рассмотрим, как мы можем использовать Path.resolve и Path.relativize вместе с классом Files для копирования и перемещения файлов.

Класс файлов

Класс Files состоит из статических методов, которые используют объекты Path для работы с файлами и каталогами. Хотя в классе Files более 50 методов, на данный момент мы только обсудим методы копирования и перемещения.

Копировать файл

Чтобы скопировать один файл в другой, вы должны использовать метод Files.copy (какие-либо догадки по названию?) — копировать (параметры Path source, Path target, CopyOption…) очень сжатые и без анонимных внутренних классов, мы уверены, что это Java ?. Аргументом являются перечисления, которые определяют, как файл должен быть скопирован. (На самом деле существует два разных класса Enum, LinkOption и StandardCopyOption, но оба реализуют интерфейс CopyOption) Вот список доступных параметров для Files.copy:

  1. LinkOption.NOFOLLOW_LINKS
  2. StandardCopyOption.COPY_ATTRIBUTES
  3. StandardCopyOption.REPLACE_EXISTING

Существует также перечисление StandardCopyOption.ATOMIC_MOVE, но если указан этот параметр, генерируется исключение UsupportedOperationException. Если параметры не указаны, по умолчанию выдается ошибка, если целевой файл существует или является символической ссылкой. Если объект пути является каталогом, то в целевой папке создается пустой каталог. (Подожди минутку! Разве во введении не сказано, что мы могли бы скопировать все содержимое каталога? Ответ все еще да, и он идет!) Вот пример копирования файла с другим объектом Path с использованием Path. Методы .resolve и Path.relativize:

1
2
3
4
5
Path sourcePath ...
        Path basePath ...
        Path targetPath ...
 
        Files.copy(sourcePath, targetPath.resolve(basePath.relativize(sourcePath));

Переместить файл

Перемещение файла одинаково прямолинейно — перемещение (параметры «Путь к источнику», «Назначение пути», «Копирование»…)

Доступны перечисления StandardCopyOptions:

  1. StandardCopyOption.REPLACE_EXISTING
  2. StandardCopyOption.ATOMIC_MOVE

Если Files.move вызывается с помощью StandardCopyOption.COPY_ATTRIBUTES, генерируется исключение UnsupportedOperationException. Files.move можно вызывать в пустом каталоге или, если он не требует перемещения содержимого каталогов, например, переименования, вызов будет успешным, в противном случае он вызовет IOException (в следующем разделе мы увидим, как переместить непустые каталоги). По умолчанию выбрасывается исключение, если целевой файл уже существует. Если источником является символическая ссылка, то перемещается сама ссылка, а не цель ссылки. Вот пример Files.move, снова связывающий методы Path.relativize и Path.resolve:

1
2
3
4
5
Path sourcePath ...
        Path basePath ...
        Path targetPath ...
 
        Files.move(sourcePath, targetPath.resolve(basePath.relativize(sourcePath));

Копирование и перемещение каталогов

Один из наиболее интересных и полезных методов класса Files — это Files.walkFileTree. Метод walkFileTree выполняет первый обход дерева файлов по глубине. Есть две подписи:

  1. walkFileTree (Путь к началу, Задать параметры, int maxDepth, Посетитель FileVisitor)
  2. walkFileTree (начало пути, посетитель FileVisitor)

Вторая опция для Files.walkFileTree вызывает первую опцию с EnumSet.noneOf (FileVisitOption.class) и Integer.MAX_VALUE. На момент написания этой статьи существует только один вариант посещения файла — FOLLOW_LINKS. FileVisitor — это интерфейс, в котором определены четыре метода:

  1. preVisitDirectory (T dir, BasicFileAttributes attrs) вызывал каталог перед тем, как все входы пройдены.
  2. Метод visitFile (файл T, BasicFileAttributes attrs) вызвал файл в каталоге.
  3. postVisitDirectory (T dir, IOException exc) вызывается только после прохождения всех файлов и подкаталогов.
  4. Метод visitFileFailed (T file, IOException exc) вызывается для файлов, которые не могут быть посещены

Все методы возвращают одно из четырех возможных перечислений FileVisitResult:

  1. FileVistitResult.CONTINUE
  2. FileVistitResult.SKIP_SIBLINGS (продолжить без обхода одноуровневых элементов каталога или файла)
  3. FileVistitResult.SKIP_SUBTREE (продолжить без обхода содержимого каталога)
  4. FileVistitResult.TERMINATE

Чтобы упростить жизнь, существует стандартная реализация FileVisitor, SimpleFileVisitor (проверяет, что аргументы не равны NULL, и возвращает FileVisitResult.CONTINUE), которая может быть разделена на подклассы, вы можете переопределить только те методы, с которыми вам нужно работать. Давайте рассмотрим базовый пример для копирования всей структуры каталогов.

Копирование примера дерева каталогов

Давайте посмотрим на класс, который расширяет SimpleFileVisitor, используемый для копирования дерева каталогов (некоторые детали опущены для ясности):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public class CopyDirVisitor extends SimpleFileVisitor<Path> {
    private Path fromPath;
    private Path toPath;
    private StandardCopyOption copyOption = StandardCopyOption.REPLACE_EXISTING;
    ....
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
        Path targetPath = toPath.resolve(fromPath.relativize(dir));
        if(!Files.exists(targetPath)){
            Files.createDirectory(targetPath);
        }
        return FileVisitResult.CONTINUE;
    }
 
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        Files.copy(file, toPath.resolve(fromPath.relativize(file)), copyOption);
        return FileVisitResult.CONTINUE;
    }
}

В строке 9 каждый каталог будет создан в целевом объекте «toPath», так как каждый каталог из источника «fromPath» пересекается. Здесь мы видим мощь объекта Path в отношении работы с каталогами и файлами. По мере того, как код перемещается все глубже в структуру каталогов, правильные объекты Path создаются просто из вызова релятивизированных и разрешенных объектов fromPath и toPath соответственно. Ни в коем случае нам не нужно знать, где мы находимся в дереве каталогов, и в результате не требуются громоздкие манипуляции с StringBuilder для создания правильных путей. В строке 17 мы видим метод Files.copy, используемый для копирования файла из исходного каталога в целевой каталог. Далее приведен простой пример удаления всего дерева каталогов.

Удаление примера дерева каталогов

В этом примере SimpleFileVisitor был разделен на подклассы для удаления структуры каталогов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class DeleteDirVisitor  extends SimpleFileVisitor<Path> {
 
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        Files.delete(file);
        return FileVisitResult.CONTINUE;
    }
 
    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
        if(exc == null){
            Files.delete(dir);
            return FileVisitResult.CONTINUE;
        }
        throw exc;
    }
}

Как видите, удаление — очень простая операция. Просто удалите каждый файл, как вы найдете их, а затем удалите каталог при выходе.

Объединение Files.walkFileTree с Google Guava

Предыдущие два примера, хотя и полезные, были очень «ванильными». Давайте рассмотрим еще два примера, которые немного более креативны, поскольку объединяют интерфейсы Google Gauva Function и Predicate.

01
02
03
04
05
06
07
08
09
10
11
12
public class FunctionVisitor extends SimpleFileVisitor<Path> {
    Function<Path,FileVisitResult> pathFunction;
 
    public FunctionVisitor(Function<Path, FileVisitResult> pathFunction) {
        this.pathFunction = pathFunction;
    }
 
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        return pathFunction.apply(file);
    }
}

В этом очень простом примере мы создадим подкласс SimpleFileVisitor, чтобы получить объект Function в качестве параметра конструктора и, по мере обхода структуры каталогов, применять функцию к каждому файлу.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class CopyPredicateVisitor extends SimpleFileVisitor<Path> {
    private Path fromPath;
    private Path toPath;
    private Predicate<Path> copyPredicate;
 
    public CopyPredicateVisitor(Path fromPath, Path toPath, Predicate<Path> copyPredicate) {
        this.fromPath = fromPath;
        this.toPath = toPath;
        this.copyPredicate = copyPredicate;
    }
 
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
        if (copyPredicate.apply(dir)) {
            Path targetPath = toPath.resolve(fromPath.relativize(dir));
            if (!Files.exists(targetPath)) {
                Files.createDirectory(targetPath);
            }
            return FileVisitResult.CONTINUE;
        }
        return FileVisitResult.SKIP_SUBTREE;
    }
 
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        Files.copy(file, toPath.resolve(fromPath.relativize(file)));
        return FileVisitResult.CONTINUE;
    }
}

В этом примере CopyPredicateVisitor принимает объект Predicate и, основываясь на возвращенном логическом значении, части структуры каталога не копируются. Я хотел бы отметить, что предыдущие два примера, за исключением полезности, работают в модульных тестах для исходного кода, поставляемого с этим постом.

DirUtils

Опираясь на все, что мы уже рассмотрели, я не мог удержаться от возможности создать служебный класс DirUtils в качестве абстракции для работы с каталогами, который предоставляет следующие методы:

01
02
03
04
05
06
07
08
09
10
//deletes all files but leaves the directory tree in place
 DirUtils.clean(Path sourcePath);
 //completely removes a directory tree
 DirUtils.delete(Path sourcePath);
 //replicates a directory tree
 DirUtils.copy(Path sourcePath, Path targetPath);
 //not a true move but performs a copy then a delete of a directory tree
 DirUtils.move(Path sourcePath, Path targetPath);
 //apply the function to all files visited
 DirUtils.apply(Path sourcePath,Path targetPath, Function function);

Хотя я бы не сказал, что он готов к производству, было весело писать.

Вывод

Это завершает новую функцию копирования и перемещения, предоставляемую пакетом java.nio.file. Лично я считаю, что это очень полезно и избавит меня от боли при работе с файлами в Java. Намного больше, о чем можно поговорить, работая с символическими ссылками, методами потокового копирования, DirectoryStreams и т. Д., Так что не забудьте остановиться. Спасибо за ваше время. Как всегда комментарии и предложения приветствуются.

Ссылка: Что нового в Java 7: копирование и перемещение файлов и каталогов от нашего партнера JCG