JavaのFileクラスは不変(immutable)クラスという点に関する注意点

長年Javaを書いてきた人間としてはちょっと情けないことに、先日、会社で自分の書いたコードが原因でちょっとしたバグを出してしまいました。きちんとテストファーストで単体試験は書いていたのですがテストが不十分でしたね。
バグの原因は、Fileクラスの仕様をちょっと勘違いして使っていたことが原因でした。FileクラスにはrenameTo()というメソッドがあって、このメソッドの呼び出しにより、操作が成功すればもともとFileクラスのオブジェクトに対応していたファイルの名前がファイルシステム上で変更されます。ここで、うっかり、Fileクラスが可変なクラスだと勘違いしてしまっていたのですが、実は、Java Docにも明記されている通り、Fileクラスは不変(immutable)なクラスであり、一度生成したら状態が決して変更されることがない設計となっています。これは、以下のテストケースを見ると確認できます。

    @Test
    public void legacyAPI() throws IOException {
        File srcFile = new File("temp.txt");
        File targetFile = new File("temp2.txt");

        if (!srcFile.createNewFile()) {
            throw new IOException();
        }
        
        if (!srcFile.renameTo(targetFile)) {
            throw new IOException();
        };
        
        assertThat(srcFile.getName(), is("temp.txt")); //srcFileの状態はrename前のまま。
        assertThat(targetFile.getName(), is("temp2.txt"));
        
        if (!targetFile.delete()) {
            throw new IOException();
        }
    }

ただ、言い訳ではありませんが、FileクラスのrenameTo()というメソッドは

boolean renameTo(File dest)

というシグネチャで定義されており、いかにも状態が変更されそうな雰囲気なため、Fileが不変ということを忘れていると、私のようにうっかりミスしてしまいそうです。実際、EclipseなどのIDEの入力補完に頼っていると間違いそうですね。おまけに、戻り値で成功か失敗かがbooleanで返ってくるのですが、どうしてIOExceptionを送出するようになっていないのでしょうか。以下のようになっていたら、間違いにくいと思うのですれどね。

File renameTo(File dest) throws IOException

このように例外を使わず、戻り値で成功失敗を判断させるというのはFileクラスの他のメソッドにも見られますが、必ず戻り値をチェックするようにする必要があります。このようになっている原因はC言語の雰囲気の影響を受けていることと、FileクラスがJDK1.0の時代からある相当古いクラスであるということもあるかもしれませんが注意が必要だと思います。
なお、Java7からはファイルシステムを扱う新しいAPIが導入されています。
http://download.java.net/jdk7/docs/api/java/nio/file/package-summary.html
2011-07-07
Fileクラスの代わりにPathクラスを使って特定のディレクトリやファイルが表現され、Filesクラスを使って実際のファイルの操作ができます。

    @Test
    public void java7API() throws IOException {
        Path srcPath = FileSystems.getDefault().getPath("temp.txt");
        Path targetPath = FileSystems.getDefault().getPath("temp2.txt");

        assertThat(srcPath.toString(), is("temp.txt"));
        
        Path createdPath = Files.createFile(srcPath);
        
        assertThat(srcPath, is(createdPath));
        Files.move(srcPath, targetPath);

        Files.delete(targetPath);
    }

Fileクラスは依然としてDeprecatedなわけではありませんが、Java7からはなるべくこちらのAPIを使うようにすべきでしょう。また、Java6までの環境では必要に応じてcommons-ioなどのライブラリーを使うとよいと思います。
なお、この問題は話題のGroovyを使うときにも同様に出くわしますので注意が必要です。