レタスのかわをぜんぶむく

ぜんぶむきます

Integerの比較で結果が安定しない

というのでハマった、という話を聞いたので
なぜそうなるのかを深堀りして確認した。

Integerの比較で結果が安定しない、なぜか?

gist.github.com

出力結果

true
false

短い回答

Integerは参照型なので、== を利用した比較では値の比較ではなく
それらが同一のインスタンスであるかどうかを判定する。
そのため、値の比較にはInteger.equalsメソッドを利用する必要がある。

長い回答

正しい比較ではないのは言語仕様からも明らかなものの
何故こうなるのかを調べるために内部での仕様を確認していく。

前提

利用したJava環境、参照したソースのバージョンはOracleJDK 13.0.1

書くまでも無いけれどInteger同士の値の比較をしたい訳なので intで比較したときと同様の結果になってほしい。
先にint同士で比較した望ましい結果を確認する。

gist.github.com

出力結果

true
true

当然、Integerの場合でもこのように動いてほしい。

実際に行われていること

ではint同士の比較と、Integer同士の比較を行う小さなコードを書いて
それぞれのバイトコードを比較して差分を確認する。

int同士の比較

intの比較をして出力するだけのクラス gist.github.com

javap -v -classpath classes IntCompare.class として上記のクラスを出力した結果 gist.github.com

Integer同士の比較

Integerの比較をして出力するだけのクラス gist.github.com

javap -v -classpath classes IntegerCompare.class として上記のクラスを出力した結果 gist.github.com

差分

基本データ型のintへの代入と、参照型のIntegerへの代入部分だけを抜き出して比較する

int

    Code:
      stack=3, locals=3, args_size=1
         0: bipush        100
         2: istore_1

Integer

    Code:
      stack=3, locals=3, args_size=1
         0: bipush        100
         2: invokestatic  #7                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         5: astore_1

bipush, isotre, astoreなどのオペコードについてはここを見てもらうとして
Integerへの代入の方では元ソースに記述の無いInteger.valueOfが呼び出されているのが確認できる。

これはJava1.5から導入されたオートボクシングという機能で
基本データ型の値を、対応する参照型の変数に変換することを容易にするというもの。
Java1.4まではプログラマが明示的にnew Integer(int型の値)などと記述する必要があった。

Integer.valueOfの実装

Integerの場合は代入時にInteger.valueOfが呼ばれて値を設定していることがわかったので
次にInteger.valueOfは内部でどういったことをしているのか確認する。

Integer.javaからInteger.valueOfの部分だけを抜粋する。

    /**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    @HotSpotIntrinsicCandidate
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

引数として与えられたintの値がIntegerCacheのlowからhighの間であれば
Integer.IntegerCache.cacheから返し、そうでなければ新しいインスタンスを生成している。

Integer.IntegerCache

次にInteger.valueOf内で利用されているIntegerCacheについて確認する。
同様にInteger.javaからInteger.IntegerCacheの部分だけを抜粋する。

    /**
     * Cache to support the object identity semantics of autoboxing for values between
     * -128 and 127 (inclusive) as required by JLS.
     *
     * The cache is initialized on first usage.  The size of the cache
     * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
     * During VM initialization, java.lang.Integer.IntegerCache.high property
     * may be set and saved in the private system properties in the
     * jdk.internal.misc.VM class.
     *
     * WARNING: The cache is archived with CDS and reloaded from the shared
     * archive at runtime. The archived cache (Integer[]) and Integer objects
     * reside in the closed archive heap regions. Care should be taken when
     * changing the implementation and the cache array should not be assigned
     * with new Integer object(s) after initialization.
     */

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer[] cache;
        static Integer[] archivedCache;

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    h = Math.max(parseInt(integerCacheHighPropValue), 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            // Load IntegerCache.archivedCache from archive, if possible
            VM.initializeFromArchive(IntegerCache.class);
            int size = (high - low) + 1;

            // Use the archived cache if it exists and is large enough
            if (archivedCache == null || size > archivedCache.length) {
                Integer[] c = new Integer[size];
                int j = low;
                for(int i = 0; i < c.length; i++) {
                    c[i] = new Integer(j++);
                }
                archivedCache = c;
            }
            cache = archivedCache;
            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

メソッドなどの実装はなく、全てstaticイニシャライザで処理が完結している。
CDSアーカイブされたデータが無ければある範囲の値を生成してキャッシュしていることがわかる。
(java.lang.Integer.IntegerCache.high の指定がなければ-128~127の範囲)

Integer.valueOfの実装と合わせて読むと、引数が特定の範囲に入っているケースであれば
毎回新しいインスタンスを生成するのではなく、キャッシュ済みのIntegerを返却している。

そのため、記事の頭にあるコードの場合では
値が100の場合はキャッシュ内から同一のインスタンスが返却されるためtrueとなり
値が200の場合は毎回新しいインスタンスが生成されて返却されるためfalseとなる。

最後に、境界値を含む検証コードを書いて挙動を確認する。

gist.github.com

出力結果

true
false
true
false

確かにコードで確認した通りの値域で想定された動作をしていることがわかる。

ちなみに、IntegerだけでなくByte, Character, Longでも同様のキャッシュ機構は存在しており
ByteとLong(-128~127), Character(0~127)の範囲でキャッシュしているが
Integerのようにキャッシュする範囲を変更することはできない。

キャッシュ値域内でも比較を失敗させる方法

オートボクシング時のvalueOfを回避すれば、キャッシュが利用されないため
(当然だが)Integer間の==での比較は正しく(?)失敗する。
例えば下記のように明示的に同じ値を持つ異なるインスタンスを作成すればよい。

    public static void main(String[] args) {
        Integer a = new Integer(100);
        Integer b = new Integer(100);

        System.out.println(a == b);
    }

出力結果

false

Booleanなどは2値しか無いので当然内部でキャッシュを持っているが
コンストラクタを利用すればtrueかつBoolean.TRUEと異なるインスタンスを作ることが出来る。

結論

Integer.valueOfは一定範囲の値をキャッシュしているため
オートボクシング(Integer.valueOf)経由でIntegerとなった値がその範囲に入った場合
==での比較にtrueを返してしまうケースがある。

普段の実装ではハマることも無いのでまあそうかという感じではあるものの
たまに何気なく触っているクラスの内部実装を見ると面白い。