一段代碼搞崩Java,坑都埋到胸了!
本文轉(zhuǎn)載自微信公眾號「小姐姐味道」,作者小姐姐養(yǎng)的狗。轉(zhuǎn)載本文請聯(lián)系小姐姐味道公眾號。
數(shù)字運算,是一門語言安身立命的根本。如果連1+1都變得不可信了,整個程序就會變得不可信。
考慮到這樣一段代碼:
- Integer a = 1;
- System.out.println(a);
- Integer b = 2;
- System.out.println( a.intValue() == b.intValue() );
- System.out.println(a.equals(b));
執(zhí)行的結(jié)果,竟然是:
- -996
- true
- true
這時候,你還敢繼續(xù)把代碼寫下去么?
為什么會這樣?
很簡單,我們使用反射改變了某些東西。
下面這段代碼,將會改變一些基本運算的執(zhí)行邏輯,理所當然屬于埋坑的范疇之一。我們還是先看一下它的行為。
- public class StaticBlock {
- static {
- try {
- Class<?> cls = Integer.class.getDeclaredClasses()[0];
- Field f = cls.getDeclaredField("cache");
- f.setAccessible(true);
- Integer[] cache = ((Integer[]) f.get(cls));
- for (int i = 0; i < cache.length; i++) {
- cache[i] = -996;
- }
- } catch (Exception e) {
- e.printStackTrace();
- //silence
- }
- }
- }
程序使用反射,修改了Integer中cache變量中的內(nèi)容,使得里面的數(shù)字,變成了一個固定的值。我們這里用的是-996,意思是永遠沒有996。
你只要想方設(shè)法把這段代碼給觸發(fā)了,Java的Integer包裝類,就算是廢了。
我們能這么做,關(guān)鍵就在于cache變量上。
數(shù)字緩存
Java 中有 8 種基本類型,鑒于 Java 面向?qū)ο蟮奶攸c,它們同樣有著對應的 8 個包裝類型,比如 int 和 Integer,包裝類型的值可以為 null,很多時候,它們都能夠相互賦值。
考慮到下面這段小小的代碼,它的運算就經(jīng)歷了多次裝箱拆箱。
- public Integer cal() {
- Integer a = 1000;
- int b = a * 10;
- return b;
- }
我們從字節(jié)碼層面看一下。
- public java.lang.Integer read();
- descriptor: ()Ljava/lang/Integer;
- flags: ACC_PUBLIC
- Code:
- stack=2, locals=3, args_size=1
- 0: sipush 1000
- 3: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
- 6: astore_1
- 7: aload_1
- 8: invokevirtual #3 // Method java/lang/Integer.intValue:()I
- 11: bipush 10
- 13: imul
- 14: istore_2
- 15: iload_2
- 16: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
- 19: areturn
可以看到這么簡單的運算,竟然涉及了valueOf、intValue等方法多次,說明它的計算過程效率是比單純的數(shù)字運算要低效的。
其中valueOf方法,用來將普通數(shù)字包裝成Integer,我們跟蹤到它的方法。
- public static Integer valueOf(int i) {
- if (i >= IntegerCache.low && i <= IntegerCache.high)
- return IntegerCache.cache[i + (-IntegerCache.low)];
- return new Integer(i);
- }
為了增加轉(zhuǎn)化的效率,Integer內(nèi)部,竟然緩存了i和Integer的對應關(guān)系!這樣在下次用的時候,就能夠直接進行定位。cache變量,就是用來存放這些中間信息的地方。如果我們通過反射改變了它,Integer就會有不正常的行為!
更多
IntegerCache,緩存了 low 和 high 之間的 Integer 對象,可以通過 -XX:AutoBoxCacheMax 來修改上限。
- String integerCacheHighPropValue =
- sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
有意思的是,Long也有這樣的Cache,但它的上下限是固定的,和Byte、Short是一樣的。
- static final Long cache[] = new Long[-(-128) + 127 + 1];S
Double和Float比較慘,只能直接new一個,做不了這種緩存。
綜合來看,Integer是比較特殊的。下面這段代碼,即使我們不做反射魔改,它的輸出依然是不確定的。
- Integer n1 = 123;
- Integer n2 = 123;
- Integer n3 = 128;
- Integer n4 = 128;
- System.out.println(n1 == n2);
- System.out.println(n3 == n4);
這是因為,正常情況,它會輸出true,false;而當我們使用AutoBoxCacheMax增加了它的上限,它就會輸出true,true。果然對象之間相互比較,還是得用equals才相對靠譜一點啊。
End
看著這個齊胸小坑,我的感情真的是難以言表。這段代碼整體看來,如果進行了正常的review,還是很容易看出問題的,但凡是總有萬一。
如果這段代碼被放到線上,哪怕是某個呆萌的同學不小心練手的時候提交到了倉庫中,后果都是毀滅性的。這段代碼目的比較直白,但如果我們把cache數(shù)組的修改邏輯,改的復雜一點,在某個特定的條件下才會觸發(fā)某單個變量值的修改,那才是要命的。
畢竟連sonar都掃描不出來,而且jdk中這樣的私有變量,還有一籮筐等著我們?nèi)ヌ剿髂?
作者簡介:小姐姐味道 (xjjdog),一個不允許程序員走彎路的公眾號。聚焦基礎(chǔ)架構(gòu)和Linux。十年架構(gòu),日百億流量,與你探討高并發(fā)世界,給你不一樣的味道。