倭マン's BLOG

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

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

Java nio の API を見ていくシリーズ(目次)。 今回は Files クラスに定義されている、ディレクトリの内容を走査するメソッドを見ていきます。 java.io.File クラスでいうところの list() / listFiles() メソッドに対応するメソッドです。 このメソッドに関連して、java.nio.file パッケージ内の

  • DirectoryStream インターフェース
  • DirectoryStream.Filter インターフェース

も見ていきます。

ディレクトリ内容を走査するメソッド、クラス群

Files クラス
まずは Files クラスに定義されている、ディレクトリ内容を走査するメソッドを見てみましょう:

package java.nio.file;

public final class Files{
    ...
    public static DirectoryStream<Path>
    newDirectoryStream(Path dir)

    public static DirectoryStream<Path>
    newDirectoryStream(Path dir, String glob)

    public static DirectoryStream<Path>
    newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter)
}

名前は newDirectoryStream で、3つのシグニチャオーバーロードされてます。 返り値はどれも DirectoryStream<Path> 型です。

DirectoryStream インターフェース
上記、Files#newDirectoryStream() メソッドの返り値に現れている DirectoryStream インターフェースは Closeable インターフェースと Iterable<T> インターフェースを拡張している以外にメソッドなどは定義されていません:

package java.nio.file;

public interface DirectoryStream<T> extends Closeable, Iterable<T>{

    // フィルター用の内部インターフェース
    static interface Filter<T>{
        boolean accept(T entry);
    }
}

ただし、java.io.FilenameFilter / java.io.FileFilter に相当するフィルター用のインターフェース DirectoryStream.Filter インターフェースが内部インターフェースとして定義されています。 この DirectoryStream.Filter インターフェースは、上記の Files#newDirectoryStream() メソッドの3つ目のオーバーロードに引数として渡して使用します。

この DirectoryStream は「ストリーム」なので、使用後は必ずクローズ処理を行う必要があります。 Java 7 では try-with-resources 構文を用いるとよいでしょう。

DirectoryStream インターフェースの定義で1つ疑問なのは、なぜ型パラメータを用いて定義されているのか?というところ。 サンプル・コードでも見ますが、この型パラメータには Path インターフェースを指定する以外に使いようがないと思うんだけど・・・ 何か、内部で別の使い方してたり、将来 Path 以外にも指定できる型を導入したりするつもりなんでしょうかね? とりあえず、今のところは毎回 <Path> を書かないといけないので面倒スグル。

サンプル・コード

ではいくつかサンプル・コードを見ていきましょう。

サンプルその1
まずは一番簡単(だと思われる)コード。 カレント・ディレクトリにあるファイル、ディレクトリを取得して名前を表示します:

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

public class PrintFiles {

    public static void main(String... args){
        // try-with-resources 構文
        try(DirectoryStream<Path> dir = Files.newDirectoryStream(Paths.get("."))){
            for(Path file : dir){    // ディレクトリ内のファイル(ディレクトリを含む)を列挙
                System.out.println(file.getFileName());
            }

        }catch(IOException | DirectoryIteratorException ex){
            // DirectoryIteratorException は実行時例外なのでキャッチしなくてもよか
            ex.printStackTrace();
        }
    }
}
  • このサンプルでは、Paths.get(".") でカレント・ディレクトリの Path オブジェクトを取得して、それを Files#newDirectoryStream() メソッドに渡しています。 カレント・ディレクトリ内のファイル、ディレクトリをリストアップするには、さらに for 文で列挙します(DirectoryStream<Path> は Iterable<Path> を実装)。
  • DirectoryStream では、指定したディレクトリの直下にあるファイル、ディレクトリが返されるだけで、ディレクトリ階層を走査したりはしません。 ディレクトリ階層を走査する処理は次回に。
  • DirectoryStream は「ストリーム」なのでクローズ処理を行う必要があります。 ここでは try-with-resources 構文を使って暗にその処理を行っています。
  • サンプルでは DirectoryIteratorExcception 例外をキャッチしてますが、この例外は実行時例外(RuntimeException)なので必ずしもキャッチする必要はありません。

サンプルその2
次は、その1を少しイジってディレクトリ階層を走査してファイル名を表示するようにしてみましょう:

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

public class PrintFiles2 {

    public static void main(String... args){
        printFiles(Paths.get("."));
    }

    public static void printFiles(Path dir){
        try(DirectoryStream<Path> stream = Files.newDirectoryStream(dir)){
            for(Path path : stream){

                // path がファイルかディレクトリかで条件分岐
                if(Files.isRegularFile(path))
                    System.out.println(path.getFileName());
                else if(Files.isDirectory(path))
                    printFiles(path);
            }

        }catch(IOException | DirectoryIteratorException ex){
            ex.printStackTrace();
        }
    }
}

ま、あんまり難しくないですね。 ただ、java.nio ではディレクトリ階層を走査する別の方法があります。 それは次回に。

サンプルその3
次は Path オブジェクトと文字列を引数にとる Files#newDirectoryStream() メソッドを使ってみましょう。 次のサンプルはカレント・ディレクトリにある「Java」ファイルを表示します:

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

public class PrintJavaFiles {

    public static void main(String... args){
        // newDirectoryStream() の第2引数に文字列 "*.java" を渡して Java ファイルを指定
        try(DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get("."), "*.java")){
            for(Path path : stream){
                System.out.println(path.getFileName());
            }

        }catch(IOException | DirectoryIteratorException ex){
            ex.printStackTrace();
        }
    }
}
  • Java」ファイルかどうかは、ファイル名の最後に「.java」がつくかどうかで判定しています。 これは Files#newDirectoryStream() メソッドの第2引数に渡している「"*.java"」によって指定しています。 複数の拡張子を指定したい場合は「"*.{java,class,jar}"」のようにします。
  • ディレクトリ階層の走査は行っていません。 というか、この方法ではディレクトリも「.java」という名前でないといけないので上手くいきません。

DirectoryStream.Filter オブジェクトを引数にとる Files#newDirectoryStream() メソッドも使い方は同様です。 ただ、第2引数が DirectoryStream.Filter<? super Path> のようになってますが、型パラメータが Path 以外になることがあるのかどうかが不明。

ディレクトリの内容をストリームとして取得するのでパフォーマンスが上がったりメモリ消費量が抑えられたりするのかも知れませんが、try-with-resouces 構文を使ってもコードがそこそこ長くなってしまうのが哀しい・・・ さらに、DirectoryStream インターフェースには意味不明な型パラメータ指定も書かないといけないので、これも助長感を醸し出してるように思うんだが。 まぁ、あんまり積極的に使おうとは思わない API かな。

【追記】 Java8 で Files クラスに追加されたディレクトリ走査メソッド list

ディレクトリを走査するのに、DirectoryStream ではなく Java8 で追加された Stream API を使ったメソッドが追加されてます。 使い方は DirectoryStream を使う場合とほとんど同じで、やはり try-with-resources 文を使います。

import java.nio.file.*;
import java.util.stream.Stream;

public class FilesStreamTest {

    public static void main(String... args)throws Exception {
        // Files#list(Path)
        try(Stream<Path> paths = Files.list(Paths.get("."))){
            paths.map(Path::toAbsolutePath).forEach(System.out::println);
        }
    }
}

DirectoryStream を使う newDirectoryStream() メソッドの場合は引数としてフィルターを指定しましたが、list() メソッドの場合は Stream API を使えばもっと柔軟に要素を扱うことができます。 なんか、DirectoryStream ってもういらなくない?(内部では使ってるようだけど)