FileSystemException: Too many open files in system を回避する

Java でファイルやディレクトリのリスティングをする際に再帰処理なんかを使うと、対象が多すぎる場合、`FileSystemException` がスローされて、`Too many open files in system` なんてメッセージを貰うことがある。
これの原因はそもそも、OS レベルの制約によるもので、Linux でプロセスが開けるファイルディスクリプタの上限に達した場合に発生するエラーが、JVM に伝播されて例外としてスローされるものになります。

Java 1.7 以降では、これを回避可能になっていたので、方法をメモ。
ただし、以下コードサンプルは Java 1.8 によるものなので、1.7 を使用する場合は適宜読み替えてください。

# 環境

- Mac OS X 10.10.3 Yosemite
- Java 1.8.0_45

```
$ mvn --version
Apache Maven 3.3.3 (7994120775791599e205a5524ec3e0dfe41d4a06; 2015-04-22T20:57:37+09:00)
Maven home: /usr/local/Cellar/maven/3.3.3/libexec
Java version: 1.8.0_45, vendor: Oracle Corporation
Java home: /Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "mac os x", version: "10.10.3", arch: "x86_64", family: "mac"
```

# 例外の発生するコード

まずは、ファイルディスクリプタの上限に達してしまうコード。
昔ながらの再帰処理によるリスティングです。

```java:FileRecursion.java
package com.yo1000.egg.filedescriptor;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class FileRecursion {
    public List<String> scan(Path directory) throws IOException {
        List<String> items = new ArrayList<>();

        Files.list(directory).forEach(path -> {
            items.add(path.toString());

            if (!path.toFile().isDirectory()) {
                return;
            }

            try {
                items.addAll(scan(path));
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });

        return items;
    }

    public static void main(String[] args) throws IOException {
        new FileRecursion().scan(Paths.get("../../")).forEach(s -> System.out.println(s));
    }
}
```

作業ディレクトリの2つ上の階層配下を、リスティングしています。
大抵の場合、ファイルが多すぎて、`FileSystemException` がスローされます。

# 解決したコード

解決編。
Java 1.7 で、JSR 203 (NIO.2 API) として追加された、`java.nio.file.FileVisitor` を使用したリスティングです。

```java:FileWalker.java
package com.yo1000.egg.filedescriptor;

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

public class FileWalker {
    public List<String> scan(Path directory) throws IOException {
        List<String> items = new ArrayList<>();

        Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                items.add(dir.toString());
                return super.preVisitDirectory(dir, attrs);
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                items.add(file.toString());
                return super.visitFile(file, attrs);
            }
        });

        return items;
    }

    public static void main(String[] args) throws IOException {
        new FileWalker().scan(Paths.get("../../")).forEach(s -> System.out.println(s));
    }
}
```

実行後しばらく待機していると、今度は例外をスローせず、無事に結果が返ってきました。
`Too many open files in system` が発生して、どうしても手詰まりなときには、NIO.2 API を使おう。

# 参考

今回使用したコード。
https://github.com/yo1000/egg.filedescriptor.git

詳しくはこのへんを参考に。
https://docs.oracle.com/javase/tutorial/essential/io/walk.html
http://www.oracle.com/webfolder/technetwork/jp/javamagazine/Java-SO12-Architect-ponge.pdf