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 就會發生作用。
沒有留言:
張貼留言