倭マン's BLOG

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

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

Java nio の API を見ていくシリーズ(目次)。 そう言えば途中で終わってたなぁというシリーズ、ちょっと再開。 今回は Java8 で Stream API が導入された影響で Files クラスに追加された Stream 関連のメソッドのうち、ディレクトリ階層を走査するものを見ていきます。

ディレクトリ階層を走査するメソッドとしては、前回見た walkFileTree() メソッドというのがありましたが、今回扱う find() メソッド、walk() メソッドというのも目的は同じです。 walkFileTree() メソッドでは FileVisitor クラスのサブクラスを実装する必要がありましたが、find(), walk() では基本的に Stream API であれこれ走査・操作します。

find(), walk() メソッド

find() メソッド、walk() メソッドのシグニチャは以下のようになっています:

package java.nio.file;

public final Files{

    //***** find *****
    public static Stream<Path>
    find(Path start,
         int maxDepth,
         BiPredicate<Path,BasicFileAttributes> matcher,
         FileVisitOption... options) throws IOException;

    //***** walk *****
    public static Stream<Path>
    walk(Path start, FileVisitOption... options) throws IOException;

    public static Stream<Path>
    walk(Path start, int maxDepth, FileVisitOption... options) throws IOException;
}
  • find() メソッドと 2つ目の walk() メソッドの引数にある int 値の maxDepth は走査する深さです。 最深部までディレクトリを走査したい場合は Integer.MAX_VALUE を指定します。
  • find() メソッドの3つ目の引数の BiPredicate オブジェクトはファイルのフィルタです。
  • 各メソッドの最後の可変長引数 FileVisitOption は、今のところリンクを辿るかどうかの FOLLOW_LINKS しかないので、特に説明の必要はないかと思います。

Files クラスのメソッドで Stream を返すメソッドを使うときは常にそうですが、Stream を使い終わった後に閉じる必要があるので try-with-resouces 文といっしょに使う必要があります。

サンプルコード

では、これらのメソッドを使うサンプルを書いてみましょう。 まぁ、ディレクトリ階層を走査するサンプルは前回出てきたので、それを find() と walk() で書き直してみましょう。 前回出てきたサンプルは Find, Copy, Delete でしたが、Find を find() メソッドで、Copy を walk() メソッドで書き直します。 Delete は今回保留。

ファイルを見つける Find
find() メソッドを使って、指定したディレクトリ下の Java ファイル(ファイル名が「.java」で終わるファイル)を探して表示してみましょう。 こんな感じ:

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

import java.util.function.BiPredicate;
import java.util.stream.Stream;

public class Find {

    public static void main(String... args)throws IOException{

        Path currentDir = Paths.get(".");

        // Java ファイルを判定するフィルタ(find() メソッドの3つ目の引数に渡す)
        BiPredicate<Path, BasicFileAttributes> isJavaFile = (path, atts) ->
                Files.isRegularFile(path) && path.getFileName().toString().endsWith(".java");

        try(Stream<Path> paths = Files.find(currentDir, Integer.MAX_VALUE, isJavaFile)){
            paths.map(Path::toAbsolutePath)
                    .map(p -> "[JAVA FILE FOUND] " + p)
                    .forEach(System.out::println);
        }
    }
}

ちょっとフィルタが長くなるので別途オブジェクトにしてラムダ式で定義しています。 別途定義してるせいで BiPredicate とか BasicFileAttributes とか書かないといけなくなってやたらと長くなってます。 本末転倒感が否めないw。 まぁ、普通に find() メソッドの引数にラムダ式を渡した方が無難かな。 Stream に対しての操作は、単に表示を絶対パスにして接頭辞「[JAVA FILE FOUND] 」を付けたいために行っている変換だけです。 簡単ですね。

まぁ、これで OK なんですが、Stream の有り難みがないので、find() メソッドにフィルタを渡さずに Stream の filter() メソッドを使ってみましょう:

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

import java.util.stream.Stream;

public class Find2 {

    public static void main(String... args)throws IOException{

        Path currentDir = Paths.get(".");

        try(Stream<Path> paths = Files.find(currentDir, Integer.MAX_VALUE, (path, atts) -> true)){
            paths.filter(p -> Files.isRegularFile(p))
                    .filter(p -> p.getFileName().toString().endsWith(".java"))
                    .map(Path::toAbsolutePath)
                    .map(p -> "[JAVA FILE FOUND] " + p)
                    .forEach(System.out::println);
        }
    }
}

find() メソッドの第3引数は使わないので、常に true を返すラムダ式を渡してます。 なんか、Stream オブジェクトで返すなら find() メソッドの第3引数のフィルタっていらない気がするんだけど。 パフォーマンスがいいとかあるのかな?

ディレクトリ・ツリーをコピーする Copy
次は指定したディレクトリをサブディレクトリやファイルを含めてコピーするサンプル。 実装の仕方はいろいろあるかと思いますが、ここではコピー元の Path からコピー元とコピー先の Path のペア (List) へ変換(2回の map で)してから、Files#copy() メソッドでコピーしています。 Files#copy() メソッドはファイルもディレクトリもコピーできますが、ディレクトリの場合は含まれるファイルなどはコピーされないのでした(されるならこんなサンプルいらねーし)。

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

import java.util.stream.Stream;

import static java.util.Arrays.asList;

public class Copy {

    public static void main(String... args)throws IOException{
        Path src = Paths.get("./src");    // コピー元
        Path dest = Paths.get("./target/sample");    // コピー先

        try(Stream<Path> paths = Files.walk(src)){
            paths.map(path -> asList(path, src.relativize(path)))
                    .map(pathPair -> asList(pathPair.get(0), dest.resolve(pathPair.get(1))))
                            // コピー元、コピー先の Path のペア(List)
                    .forEach(pathPair -> {
                        try {
                            Files.copy(pathPair.get(0), pathPair.get(1));
                            System.out.println("[COPY] " + pathPair.get(1));

                        }catch(IOException ex){
                            throw new RuntimeException(ex);
                        }
                    });

        }catch(RuntimeException ex){
            if(ex.getCause() instanceof IOException)
                throw (IOException)ex.getCause();
            else
                throw ex;
        }
    }
}

Stream API 使うときに一番面倒なのが例外の処理。 これのせいでやたらとコードが長くなりますね。 ここでは Stream#forEach() の中でファイルをコピーしようとしてるんですが、IOException が投げられるので実行時例外を生成してそれを投げてます。 で、Stream のメソッド・チェーンの外で実行時例外をキャッチして、原因が IOException ならそれを投げ直してます。 結構雑な例外の扱いをしてますが、真面目にやるならどうしたらいいんでしょうかね。 まぁ、Stream の中で例外投げられるのも気持ち悪いので、無理せず walkFileTree() メソッドと FileVisitor で実装した方がよさそう。

Javaによる関数型プログラミング ―Java 8ラムダ式とStream

Javaによる関数型プログラミング ―Java 8ラムダ式とStream