倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

これからの「Java I/O」の話をしようwww (9) : Files クラスのメソッド 〜ディレクトリの階層を走査〜

Java nio の API を見ていくシリーズ(目次)。 今回は、Files クラスに定義されている、ディレクトリ階層を走査するメソッドとそれに関連するクラス群を見ていきます。 java.nio では、ディレクトリ階層を走査す方法として、デザイン・パターンの1つである Visitor パターンを実装するためのインターフェース、クラス、メソッド等が導入されています。

参考

ディレクトリ階層の走査に関係するクラス、メソッド

まずはディレクトリ階層の走査を行う際に使用するインターフェース、クラスを見ていきましょう。 おそらく、必ず使うことになるのは

  • Files#walkFileTree() メソッド
  • FileVisitor インターフェース
  • SimpleFileVisitor クラス(このクラスは必ずしも使う必要はないけど、普通は使う)
  • FileVisitResult 定数

です。 あと、

  • FileVisitOption 定数

は必須ではありませんが、簡単に触れておきます。

Files クラスのメソッド
ディレクトリ階層の走査を実行する起点となるメソッドは Files クラスに定義されている、以下の(オーバーロードされた)2つのメソッドです:

package java.nio.file;

public final class Files{
    ...
    public static Path walkFileTree(Path start, FileVisitor<? super Path> visitor);
    public static Path walkFileTree(Path start,
                                    Set<FileVisitOption> options,
                                    int maxDepth,
                                    FileVisitor<? super Path> visitor);
}

ディレクトリの走査には、少なくとも「起点となるディレクト(へのパス)」とファイル、ディレクトリに対する「処理を実装した FileVisitor オブジェクト」が必要です。 2番目のメソッドでは、それに加えて「走査のオプション」と「走査の深さの最大値」も指定できます。

FileVisitor インターフェース
次はファイル、ディレクトリに対する処理を実装する「ビジター (Visitor)」にあたる FileVisitor インターフェース。 この型には4つのメソッドが定義されています:

package java.nio.file;

public class interface FileVisitor<T>{

    // ファイルに対する処理
    FileVisitResult visitFile(T file, BasicFileAttributes attrs);

    // 例外が投げられた場合の処理
    FileVisitResult visitFileFailed(T file, IOException exc);

    // ディレクトリ走査の前処理
    FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs);

    // ディレクトリ走査の後処理
    FileVisitResult postVisitDirectory(T dir, IOException exc);
}

FileVisitor に宣言されているメソッドを見ると、どんな処理が記述できるのかは分かると思います。 ディレクトリに対する処理は、目的の処理によってディレクトリ下のファイルを走査する前後どちらに行うかが違ってくるので、preVisitDirectory() / postVisitDirectory() メソッドの2つが宣言されています。 postVisitDirectory() メソッドで属性の引数がなくなっているのが何故かよく分かりませんけど*1。 また例外処理を行う visitFileFailed() メソッドは、ドキュメントに「ファイルの属性が読み取れなかったり、ファイルが開けないディレクトリだったり、その他の理由で実行される」*2とあるので、引数のファイルがディレクトリの場合もありえそうです。

メソッドの返り値 FileVisitResult 定数については FileVisitResult 定数の箇所参照。

ところで、前回の DirectoryStream インターフェースもそうですが、なぜこのインターフェースが型パラメータ付きで定義されているのかが不明。 Path 型との継承関係は指定されてないので、ファイルやディレクトリ(へのパス)の独自実装に対しても流用できなくはないけど、BasicFileAttributes や FileVisitResult は普通に宣言されてるので、独自実装を組み込める自由度なんて大して有難味がないよなぁ・・・

あと、OS に依存するファイル属性を取得するには、BasicFileAttributes オブジェクトをキャストする必要があるようですね。 これは、BasicFileAttributes に Map のような、名前をキーにして属性値を取得するメソッドが定義されてれば必要ないと思うんだけど、どうでしょう?

SimpleFileVisitor クラス
SimpleFileVisitor クラスは、FileVisitor インターフェースの実装クラスを簡単に作成できるようにするためのサポートクラスです:

package java.nio.file;

public class SimpleFileVisitor<T> implements FileVisitor<T>{
    ...
}

各メソッドの実装は基本的に何もしないようなものですが、FileVisitor の各メソッドに返り値が宣言されているので、FileVisitResult を返す(もしくは例外を投げる)処理は行われます。 各メソッドで返される値は以下の通りです:

メソッド 返り値
visitFile() FileVisitResult.CONTINUE を返す。
visitFileFailed() 引数の例外を投げ直す。
preVisitDirectory() FileVisitResult.CONTINUE を返す。
postVisitDirectory() 引数の例外が null なら FileVisitResult.CONTINUE を返す。
例外が null でないなら、その例外を投げ直す。

まぁ、そのままですね。

FileVisitResult 定数
FileVisitResult 定数(列挙型)は FileVisitor の各メソッドで返され、その後の走査の操作をする定数です。

package java.nio.file;

public enum FileVisitResult{
    CONTINUE,
    TERMINATE,    // 走査停止
    SKIP_SUBTREE,    // サブツリーをスキップ
    SKIP_SIBLINGS    // 兄弟要素をスキップ
}

これも名前のままなのであまり説明は必要なさそうですが、あえて注意するなら

  • preVisitDirectory() 以外で SKIP_SUTREE を返しても意味なし
  • ディレクトリ走査中に SKIP_SIBLINGS が返されても postVisitDirectory() メソッドは呼ばれる

といったことでしょうか。

FileVisitOption 定数
FileVisitOption 定数は、ディレクトリ階層の走査方法を指定する定数です。

package java.nio.file;

public enum FileVisitOption{
    FOLLOW_LINKS
}

以前に見た LinkOption と同じようなもんですな*3。 このオプションを指定してディレクトリ走査をすると、シンボリック・リンクのリンク先も辿って走査します。 この際、循環した走査にならないように気を付けましょう。

使い方は Files#walkFileTree() メソッド(引数4つの方)に Set の要素として渡します:

FileVisitor visitor = ...;
Files.walkFileTree(
    Paths.get("."),
    EnumSet.of(FileVisitOption.FOLLOW_LINKS),    // FileVisitOption の指定
    Integer.MAX_VALUE,
    visitor);

Java 7 では FileVisitOption は1つしかないので Set にするのが面倒だけど、これはさすがに仕方ないかな。

サンプル・コード

まぁ、あれこれ説明書きましたが、こういうのはサンプル見た方が分かりやすいのが常。 ってことでチュートリアルやドキュメントから簡単なサンプルを拝借)して(というよりもっと簡単にしてます見ていきましょう。 見ていくのは

こういう操作は Directories とかっていうユーティリティ・クラスを作って実装を提供しておいてくれても良さそうだけど、取り返しのつかない操作のためかそう言うのは提供される気配がないですな。

ファイルを見つける
ディレクトリ階層内から「.java」で終わるファイルを探すサンプル。 元ネタはFind.java。 ただし、下記のサンプルではマッチするディレクトリは探してません:

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

import java.nio.file.FileVisitResult;

public class Find {

    public static void main(String... args) throws IOException {
        execute(Paths.get("C:/workspace/java"), "*.java");
    }

    public static void execute(Path path, String pattern)
            throws IOException{
        Finder finder = new Finder(pattern);    // FileVisitor オブジェクト取得
        Files.walkFileTree(path, finder);    // 走査の開始
        finder.done();    // 結果の表示
    }

    /** ビジターの実装 */
    public static class Finder extends SimpleFileVisitor<Path> {

        private final PathMatcher matcher;
        private int numMatches = 0;

        Finder(String pattern) {
            this.matcher = FileSystems.getDefault().getPathMatcher("glob:"+pattern);
        }

        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes atts) {
            Path name = file.getFileName();
            if (name != null && this.matcher.matches(name)) {
                this.numMatches++;
                System.out.println(file);
            }
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult visitFileFailed(Path file, IOException exc) {
            System.err.println(exc);
            return FileVisitResult.CONTINUE;
        }

       /**
        * 結果を表示する
        * ディレクトリ階層走査後に手動で呼び出す
        */
        void done() {
            System.out.println("Matched: " + this.numMatches);
        }
    }
}

Finder クラスのフィールドなどに用いている java.nio.file.PathMatcher インターフェースは、java.io パッケージでいう FileFilter インターフェースみたいなもので、渡された Path オブジェクトが何らかの条件(これを実装する)にマッチしているかどうかを返します:

package java.nio.file;

public interface PathMatcher{
    boolean matches(Path path);
}

もちろん、独自の実装を自分で行っても構いませんが、拡張子のマッチ条件を指定したい場合(上記のサンプルでは、拡張子が「.java」のファイルを探す)は、以下のようにしてノゾミの PathMatcher オブジェクトを取得することができます:

PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.java")

glob:」を忘れずに。 他にもデフォルトで提供されている実装がありますが、詳しくは下記参照:

ディレクトリ・ツリーをコピーする
元ネタは FileVisitorJavaDoc より。 もっと凝ったコピーはこちらチュートリアルにあります:

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;

public class Copy{

    public static void main(String... args)
            throws IOException{
        Path src = Paths.get("C:/workspace/java");
        Path target = Paths.get("C:/sample/java");
        Files.createDirectories(target);
        execute(src, target);
    }

    public static void execute(Path src, Path target) throws IOException{
        FileVisitor<Path> visitor = new CopyVisitor(src, target);
         Files.walkFileTree(src, visitor);
    }

    /** ビジターの実装 */
    static class CopyVisitor extends SimpleFileVisitor<Path>{

        private final Path source;
        private final Path target;

        CopyVisitor(Path source, Path target){
            this.source = source;
            this.target = target;
        }

        @Override
        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes atts)
                throws IOException {
            Path targetDir = this.target.resolve(this.source.relativize(dir));
            try{
                Files.copy(dir, targetDir);
                System.out.println("[COPY DIR] "+targetDir);

            }catch(FileAlreadyExistsException ex){
                if(!Files.isDirectory(targetDir)) throw ex;
            }

            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes atts)
                throws IOException {
            Path targetFile = this.target.resolve(this.source.relativize(file));
            Files.copy(file, targetFile);
            System.out.println("[COPY FILE] "+targetFile);
            return FileVisitResult.CONTINUE;
        }
    }
}

コピーではディレクトリに対する処理は preVisitDirectory() に書く必要がありますね。

ディレクトリ・ツリーを削除する
こちらも FileVisitorJavaDoc より:

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class Delete {

    public static void main(String... args)throws IOException{
        delete(Paths.get("C:/sample/java"));
    }

    static void delete(Path target)throws IOException{
        Files.walkFileTree(target, new DeleteVisitor());
    }

    /** ビジターの実装 */
    static class DeleteVisitor extends SimpleFileVisitor<Path>{

        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes atts)
                throws IOException{
            Files.delete(file);
            System.out.println("[DELETE FILE] "+file);
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult postVisitDirectory(Path dir, IOException e)
                throws IOException{
            if (e != null)throw e;

            Files.delete(dir);
            System.out.println("[DELETE DIR] "+dir);
            return FileVisitResult.CONTINUE;
        }
    }
}

コピーのサンプルに対して、削除する場合はディレクトリに対する処理を postVisitDirectory() に書く必要があります。

*1:ファイルの属性が読み取れなくて例外が投げられた場合とかのためかな?

*2:「This method is invoked if the file's attributes could not be read, the file is a directory that could not be opened, and other reasons.」

*3:LinkOption では NOFOLLO_LINKS が定義されてたので真逆と言った方がいいかな。