Java 7 Update 40 で修正されたバグ

2013年9月10日に出た Java 7 Update 40 は Limited Update (セキュリティ修正を含まないバグ修正と新機能のリリース) でした。
そのリリースノートを見てみると、いくつか面白い(?)バグがあったので、調べてみました。

EnumMap: クローン時のバグ修正

EnumMap の entrySet を取得した後、クローンして再び entrySet を取得すると、クローンする前のオブジェクトに対する entrySet が返ってくるというバグが修正されていました。
Bug ID: JDK-7164256 EnumMap clone doesn't clear the entrySet keeping a reference to the original Map

どういう問題?

以下のコードを Java 7 Update 25 で実行すると…。

import java.util.EnumMap;
import java.util.Map.Entry;

public class TestEnumMap {
    public enum Test {
        ONE, TWO
    }

    public static void main(String[] args) {
        EnumMap<Test, String> map1 = new EnumMap<Test, String>(Test.class);
        map1.put(Test.ONE, "1");
        map1.put(Test.TWO, "2");

        // 現在の状態を表示(クローン前)
        for (Entry<Test, String> entry : map1.entrySet()) {
            System.out.println(entry);
        }

        System.out.println("---------------");

        EnumMap<Test, String> map2 = map1.clone();
        map2.remove(Test.ONE);    // ここで、要素を一つ削除!

        // 現在の状態を表示(クローン後)
        for (Entry<Test, String> entry : map2.entrySet()) {
            System.out.println(entry);
        }
    }
}

下記の実行結果となりました。
クローン後の結果に削除したはずのオブジェクト(Test.ONE)が含まれていました。(バグレポートより引用)

ONE=1
TWO=2
                            • -
ONE=1 TWO=2


クローンするときに、entrySet の内部キャッシュを消していなかったのが原因のようです。
jdk7u/jdk7u/jdk: changeset 6155:80fefc247e3d

Java 7 Update 40 で実行したところ、以下のように正しい結果が表示されました。

ONE=1
TWO=2
                            • -
TWO=2

Integer: キャッシュ上限指定のバグ修正

Integer は、デフォルトで -128 〜 +127 までの間の Integer オブジェクトをキャッシュしています。
この上限は、システムプロパティで変更できるのですが、その時に Integer.MAX_VALUE (=2,147,483,519) を指定すると Integer クラスの初期化時に NegativeArraySizeException が起きるというバグが修正されました。
Bug ID: JDK-7103957 NegativeArraySizeException while initializing class IntegerCache

どういう問題?

「-Djava.lang.Integer.IntegerCache.high=2147483519」というVMオプションを指定すると発生するそうです。
原因はキャッシュサイズの上限を決める際のオーバフローで、修正は -1 を追加しただけというとてもシンプルなもの。
jdk7u/jdk7u/jdk: changeset 5863:9cf3e367b99b


ちなみに、修正されたものの OracleJava VM だとこのように VMオプションを指定した場合は OutOfMemoryError になりました*1
これについて、詳しくはこちらの記事(Java の配列要素数の上限 - 地平線に行く)をご参照ください。

File: createTempFile メソッドで無限ループする問題のバグ修正

Windows 上で java.io.File.createTempFile メソッドを呼び出した場合、引数によってはメソッド内で無限ループが発生するというバグが修正されていました。
Bug ID: JDK-8011950 java.io.File.createTempFile enters infinite loop when passed invalid data

どういう問題?

以下を Java 7 Update 25 で実行すると、いつまでたっても終了しません。

public class BugRepo {
    public static void main(String[] args) throws Exception {
        java.io.File.createTempFile(" ///../// ", " ///..///.. ");
    }
}

また、Windows の予約されたデバイス*2で始まる文字列を指定した場合(File.createTempFile("LPT1.package.zip", ".temp")、など)も同様の動きになるそうです。*3


原因は、テンポラリーファイルの作成方法。
テンポラリーファイル作成時は、「引数に指定されたプリフィックス+ランダムな番号」をファイル名とし、その名前でテンポラリーファイルを作成して、作成に失敗した場合はランダムな番号を変更してリトライするという方法をとっていました。
しかし、上記のようなプリフィックスを指定されていた場合、常にファイルの作成に失敗してしまうため、無限ループになっていたようです。

// Java 7 Update 25 の File クラスより
    public static File createTempFile(String prefix, String suffix, File directory)
            throws IOException
    {
        (…中略…)
        File f;
        do {
            f = TempDirectory.generateFile(prefix, suffix, tmpdir);
            (…中略…)
        } while (!fs.createFileExclusively(f.getPath()));
        return f;
    }


修正されていたのは、2点。
まず、いきなりファイルを作るのではなく、存在チェックをしてファイルがないことを確認するようになっていました(すでにファイルが存在すればリトライ)。そのうえで、存在しないことを確認できたら、その名前でファイルを作ってみて、作成できなければエラーとするようになっていました。
また、ファイル名が Windows の予約されたデバイス名で始まるものだった場合、ファイル作成時にエラーとするようになっていました。*4

// Java 7 Update 40 の File クラスより
    public static File createTempFile(String prefix, String suffix, File directory)
            throws IOException
    {
        (…中略…)
        File f;
        try {
            do {
                f = TempDirectory.generateFile(prefix, suffix, tmpdir);
            } while (f.exists());
            if (!f.createNewFile())
                throw new IOException("Unable to create temporary file");
        } catch (SecurityException se) {
            (…中略…)
        }
        return f;
    }


これで、バグレポートにあるような引数を指定された場合でも無限ループすることなく、IOException で即座に終了するようになりました。

javac: 特定のプログラムがコンパイルできない問題の修正

for 文と try-with-resources 文を併用したプログラムをコンパイルすると、コンパイラが例外を出して停止する問題が修正されていました。
Bug ID: JDK-7178324 Crash when compiling for(i : x) try(AutoCloseable x = ...) {}

どういう問題?

以下を Java 7 Update 25 でコンパイルします。

import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.List;

class TestMe {
    public static void main(String[] args) throws Exception {
        List<File> files = new ArrayList<>();
        for (File f : files)
            try (FileInputStream is = new FileInputStream(f)) { }
    }
}

すると、コンパイラが下記の例外を吐いて停止します。

コンパイラで例外が発生しました(1.7.0_25)。Bug Paradeに同じバグが登録されていない
ことをご確認の上、Java Developer Connection(http://java.sun.com/webapps/bugrepor
t)でバグの登録をお願いいたします。レポートには、そのプログラムと下記の診断内容を
含めてください。ご協力ありがとうございます。
java.lang.NullPointerException
	at com.sun.tools.javac.tree.TreeInfo.endPos(TreeInfo.java:248)
	at com.sun.tools.javac.comp.Lower.visitIterableForeachLoop(Lower.java:3285)
	at com.sun.tools.javac.comp.Lower.visitForeachLoop(Lower.java:3141)
	at com.sun.tools.javac.tree.JCTree$JCEnhancedForLoop.accept(JCTree.java:907)
	at com.sun.tools.javac.tree.TreeTranslator.translate(TreeTranslator.java:58)
	at com.sun.tools.javac.comp.Lower.translate(Lower.java:2160)
…(以下省略)…


修正自体はとても簡単なもののようです。
ちなみに、下記のように for のあとに {} を付ければエラーにならいので、このエラーに遭遇する人はあまりなかったんじゃないかと思います。

        for (File f : files) {
            try (FileInputStream is = new FileInputStream(f)) { }
        }

まとめ

バージョンを重ねるにつれ安定感を増しているように思える Java も、思わぬところにバグが残っていたようです。
今回挙げた以外にも、こんなところにバグが!って思ったところが多数ありました。


Java 7 Update 40 では、合計で 621 ものチケットが改修されています。
ライブラリやコンパイラのバグだけでなく、VM のバグも多数直っているようなので、ざっと気になるところをチェックしておくと面白いかもしれません。
Java™ SE Development Kit 7 Update 40 Bug Fixes

*1:たとえメモリに空きがあったとしても、です。

*2:CON, PRN, AUX, NUL, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, LPT9

*3:テンポラリーファイルの名前を外部からのパラメータで決めるような作りにしていたら、攻撃に利用されそうですね。

*4:これは、File.createNewFile() 内の動作です。