2015年1月19日

如何在 Java 中計算出 2 + 1 = 4

My favourite Java puzzler 2 + 1 = 4 提供了一個很短的 Java 程式,可以在做整數計算 2+1 的時候,得到結果為 4 而不是 3。

文章是用問答題的方式問的,但我們資質駑鈍,偷懶直接看答案:

import java.lang.reflect.*;

public class Test {
    public static void main(String... args) throws Exception {
        Integer a = 2;
        Field valField = a.getClass().getDeclaredField("value");
        valField.setAccessible(true);
        valField.setInt(a, 3);

        Integer b = 2;
        Integer c = 1;

        System.out.println("b+c : " + (b + c)); // b+c : 4
    }
}

在程式的後半段,我們可看到 b + c 應該是 2 + 1 ,可是執行的結果卻得到 4 ,很明顯是前面那一半的部份,造成這樣的結果。

a.getClass().getDeclaredField

這一行是利用 Java 的 Reflection API 來取得 Integer 這個 class 的一個 private 欄位 value。

Field valField = a.getClass().getDeclaredField("value");

getField 跟 getDeclaredField 都是取得 class 裡面定義的 member 資料的 API,但 getField 只能取得 public fields,而 getDeclaredField 則不管 accessibility 是 public 或是 private,可以取得所有 fields。因此 a.getClass().getDeclaredField("value") 就是要取得 Integer 為 2 這個物件的 value 這個 field。

valField.setAccessible(true);
valField.setInt(a, 3);

接下來這兩行,則是將該 value 的欄位設定為可以修改的,並用 setInt 把 value 的值設定為 3。

新的 Integer 物件 2

我們在前面把物件 a 的 value 改成了 3,但是後半段卻是產生了一個新的 Interger 物件 2,就 Java 來說,只要是new 新的物件,那應該會不一樣才對啊!

但執行結果告訴我們,雖然有了一個新物件 b (Integer b = 2;),但很明顯地,在計算 b+c 的結果時,b 的數值被剛剛修改物件 a 的數值為 3 的時候,影響到了結果,也就是 b 物件就跟 a 物件是一樣的。

原因就在 Integer 的 source code 裡面

JDK 裡面的 Integer.java

JDK 裡面有 Integer 的 source code,我們節錄重要的部份:

public final class Integer extends Number implements Comparable<Integer> {

....

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

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

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

    private final int value;

    public Integer(int value) {
        this.value = value;
    }
...
}

Interger Class 裡面放了一個靜態的 Inner Class: IntegerCache,這裡面 cache 了 -128 到 127 這麼多整數的物件。

Integer b=2;

這樣的語法是 auto boxing 的功能,就是 compiler 可以將一些 syntax sugar 自動 unboxing。像上面這個整數物件賦值的語法,compiler 會自動翻譯成以下這樣的語法。

Integer b = Integer.valueOf(2);

另外再看看 valueOf 這個 method,在 IntegerCache 負責的數值區間中,其實是直接使用 IntegerCache 而不是產生一個新的 Integer 物件。

換句話說,當我們使用 -128 到 127 的 Interger 物件時,其實都是參考到同一個物件。

為什麼要有 IntegerCache

cache 的用途基本上唯一的目的就是為了效能考量,而記憶體通常都是 cache 的第一選擇,在實做 Integer 的工程師認定一般使用者最常用到 -128 到 127 這些整數的物件,所以就預先產生出來,放置到記憶體中。

另外因為 IntergerCache 是 JVM 中靜態的物件,這樣子處理,也會因為 -128 ~ 127 使用率的增加,而達到記憶體空間節省的效果,表面上多花了一些記憶體,存放著 256 個整數物件,實際上當程式運作時間越久,產生出來的這些 IntergerCache 就會發生作用。

Integers caching in Java