我們知道Java是面向?qū)ο蟮恼Z(yǔ)言,號(hào)稱一切皆對(duì)象吃环,但是有8種原始數(shù)據(jù)類型(boolean也颤、byte 、short模叙、char歇拆、int鞋屈、float范咨、double、long)需要除排除在外厂庇。
在面試過(guò)程中經(jīng)常會(huì)遇到渠啊,考察原始數(shù)據(jù)類型和其包裝類語(yǔ)言特性的問(wèn)題。
本文就以原始數(shù)據(jù)類型int和其包裝類Integer為例進(jìn)行講解权旷,主要包含以下幾個(gè)方面的內(nèi)容:
1.Integer的不可變性
2.自動(dòng)裝箱和自動(dòng)拆箱(boxing/unboxing)
3.Integer的緩存值
4.使用Integer注意的事項(xiàng)
一.Integer的不可變性
A.什么是不可變對(duì)象替蛉?
如果一個(gè)對(duì)象,在它創(chuàng)建完成之后拄氯,不能再改變它的狀態(tài)躲查,那么這個(gè)對(duì)象就是不可變的。
前面講解的字符串String是不可變對(duì)象译柏,另外8種包裝類型也都是不可變對(duì)象镣煮,其中自然包括Integer。
B.Integer如何實(shí)現(xiàn)不可變
1.Integer類定義使用final修飾鄙麦,不能被繼承典唇,這樣Integer的所有方法就不能被重寫;
2.存儲(chǔ)數(shù)據(jù)的字段聲明為private final int value胯府,使用private final修飾介衔,value的值構(gòu)造函數(shù)初始化后不能被修改。
C.Integer不可變帶來(lái)的好處
1.Integer只有設(shè)計(jì)成不可變的對(duì)象骂因,才能為其建立緩存值(后面會(huì)相信講解)炎咖,以達(dá)到節(jié)約內(nèi)存的目的;
2.Integer是不可變的,必然是線程安全的乘盼,這樣同一個(gè)Integer對(duì)象就可以被多個(gè)線程安全地共享急迂,而且不需要任何同步操作;
3.Integer是不可變的蹦肴,可以保證信息的安全僚碎。比如我們使用Integer來(lái)保存服務(wù)器某個(gè)服務(wù)的端口,如果我們可以輕易地把Integer對(duì)象改變?yōu)槠渌麛?shù)值阴幌,這會(huì)給產(chǎn)品的可靠性帶來(lái)嚴(yán)重的問(wèn)題勺阐。
D.舉例說(shuō)明:
?上面代碼執(zhí)行結(jié)果:
?number和numberToIncrease兩個(gè)變量的內(nèi)存示意圖,如下圖所示:
1. 執(zhí)行number = 1的時(shí)候矛双,number執(zhí)行value = 1的Integer對(duì)象渊抽;
2. 調(diào)用increase方法,number作為實(shí)參傳給numberToIncrease议忽,此時(shí) number和numberToIncrease都指向了value = 1的Integer對(duì)象懒闷;
3. 執(zhí)行numberToIncrease = numberToIncrease + 1;numberToIncrease就指向了value = 2的Integer對(duì)象栈幸,但是number的指向不變愤估,還是原來(lái)value = 1的Integer對(duì)象。
最終速址,有了上面的執(zhí)行結(jié)果玩焰。
二.自動(dòng)裝箱和自動(dòng)拆箱(boxing/unboxing)
Java中的自動(dòng)裝箱和拆箱操作是通過(guò)語(yǔ)法糖實(shí)現(xiàn)的。什么是語(yǔ)法糖芍锚?
語(yǔ)法糖昔园,是指計(jì)算機(jī)語(yǔ)言中添加的某種語(yǔ)法,這種語(yǔ)法對(duì)語(yǔ)言的功能并沒(méi)有影響并炮,但是更方便程序員使用默刚。
通常來(lái)說(shuō)使用語(yǔ)法糖能夠增加程序的可讀性,從而減少程序代碼出錯(cuò)的機(jī)會(huì)逃魄。
在講解自動(dòng)裝箱和自動(dòng)拆箱操作之前荤西,先說(shuō)明一下裝箱和拆箱的概念:
裝箱:把Java原始數(shù)據(jù)類型(如:int)轉(zhuǎn)化為其對(duì)應(yīng)的包裝類型(如:Integer)的過(guò)程我們稱為裝箱操作;
拆箱:把Java包裝類型(如:Integer)轉(zhuǎn)化為其對(duì)應(yīng)的原始數(shù)據(jù)類型(如:int)的過(guò)程我們稱為拆箱操作嗅钻。
下面舉個(gè)例子皂冰,說(shuō)明一下裝箱和拆箱的操作,例1:
1. main方法的第一行养篓,代碼中的0是int類型秃流,當(dāng)賦值給Integer類型的sum變量時(shí),調(diào)用了Integer.valueOf將int類型轉(zhuǎn)換成Integer類型柳弄,這個(gè)過(guò)程就是裝箱舶胀;
2. main方法的第二行概说,sum要進(jìn)行加法操作時(shí),Integer類型無(wú)法直接進(jìn)行加法操作嚣伐,先執(zhí)行sum.intValue()變成int后糖赔,再進(jìn)行加法操作,而轉(zhuǎn)化為int的過(guò)程就是拆箱轩端;
3. main方法的第二行放典,當(dāng)需要把加1的結(jié)果,再賦值給sum的時(shí)基茵,再次調(diào)用Integer.valueOf進(jìn)行類型轉(zhuǎn)換奋构,又進(jìn)行了一次裝箱操作幅垮。
當(dāng)然平時(shí)我們很少會(huì)寫上面那種臃腫的代碼术浪,常用的寫法如下,例2:
?但是我們查看這兩個(gè)類main方法的字節(jié)碼朝群,結(jié)果完全一致根灯,如下所示:
從上面的字節(jié)碼可以看出:
雖然例2中的代碼中沒(méi)有顯式地去調(diào)用Integer.valueOf和Integer.intValue方法径缅。
但是編譯后的class文件中,卻有這兩個(gè)方法的隱式調(diào)用烙肺,而這個(gè)隱式調(diào)用過(guò)程就是自動(dòng)裝箱和自動(dòng)拆箱的操作纳猪。
自動(dòng)裝箱:當(dāng)一個(gè)Integer類型的值,需要變成int的時(shí)候(比如要進(jìn)行加法運(yùn)算)茬高,Java編譯器會(huì)加入Integer.intValue()的方法調(diào)用兆旬,將Integer類型自動(dòng)轉(zhuǎn)換成int;
自動(dòng)拆箱:當(dāng)一個(gè)int的值怎栽,需要變成Integer的時(shí)候(比如把int類型的值賦值給Integer),Java編譯器會(huì)加入一段Integer.valueOf(int i)的方法調(diào)用宿饱,把int類型自動(dòng)轉(zhuǎn)換為Integer類型熏瞄。
三.Integer的緩存值
關(guān)于Integer的值緩存,涉及到Java 5的一個(gè)改進(jìn)谬以。在Java 5之前强饮,構(gòu)建Integer對(duì)象的傳統(tǒng)方式是,直接調(diào)用其構(gòu)造函數(shù)創(chuàng)建出一個(gè)新的對(duì)象为黎。
但是根據(jù)實(shí)踐的結(jié)果邮丰,我們發(fā)現(xiàn)大部分int數(shù)值運(yùn)算的結(jié)果都集中在有限的、較小的數(shù)值范圍內(nèi)铭乾。
因此剪廉,在Java 5在Integer類上新增了一個(gè)valueOf的靜態(tài)工廠方法,在調(diào)用它的時(shí)候會(huì)利用一個(gè)緩存機(jī)制炕檩,最終帶來(lái)了明顯的性能改進(jìn)斗蒋。
我們先看看下面的代碼,猜測(cè)一下代碼執(zhí)行的結(jié)果,代碼如下:
代碼執(zhí)行結(jié)果如下:
當(dāng)沒(méi)有讀過(guò)Integer的源碼泉沾,看到上面的結(jié)果捞蚂,是不是會(huì)很驚訝。
為什么a和b賦值為1跷究,進(jìn)行==判斷返回true姓迅,而c和d賦值為128,進(jìn)行==判斷就返回false了呢俊马?
前面了解了自動(dòng)裝箱操作后队贱,我們知道,當(dāng)把int型的值潭袱,賦值為Integer類型的時(shí)候柱嫌,會(huì)調(diào)用Integer.valueOf進(jìn)行自動(dòng)裝箱操作,奧秘應(yīng)該就是在Integer.valueOf方法中屯换,源碼如下:
當(dāng)傳入的參數(shù)i大于等于IntegerCache.low并且小于等于IntegerCache.high時(shí)编丘,則從IntegerCache.cache中取值;
而其他情況下則新創(chuàng)建一個(gè)Integer對(duì)象彤悔。
IntegerCache的low和high是多少嘉抓,還有cache又是什么呢,接著讀一下IntegerCache的源碼:
通過(guò)上面的代碼晕窑,我們得出下面結(jié)論:
1.low的值是固定為-128抑片;
2.high的默認(rèn)值是127,但是可以通過(guò)java.lang.Integer.IntegerCache.high這個(gè)property進(jìn)行設(shè)置杨赤。
最終取127和設(shè)置值中的較大值敞斋,并且取Integer.MAX_VALUE - (-low) -1和設(shè)置值中的較小值,作為最終的high的值疾牲;
3.最后根據(jù)low和high的值植捎,對(duì)cache進(jìn)行初始化,其cache[0] = -128阳柔,cache[cache.length - 1] = high焰枢。
還是上面的代碼,如果加上JVM參數(shù)-Djava.lang.Integer.IntegerCache.high=128后再次執(zhí)行舌剂,結(jié)果如下:
通過(guò)JVM參數(shù)济锄,IntegerCache緩存的最大值設(shè)置為128,128也進(jìn)行了緩存霍转,c == d就由原來(lái)的false變成了true荐绝。
四.使用Integer注意的地方
1.使用int類型替換Integer進(jìn)行數(shù)值計(jì)算
Integer類型無(wú)法直接進(jìn)行數(shù)值計(jì)算,在計(jì)算之前需要進(jìn)行拆箱變成int后進(jìn)行計(jì)算谴忧,在計(jì)算之后賦值給Integer類型的時(shí)候又要進(jìn)行裝箱操作很泊。
大量的裝箱和拆箱操作非常浪費(fèi)CPU和內(nèi)存角虫,下面代碼對(duì)比一下二者的效率,代碼如下:
上面代碼執(zhí)行結(jié)果如下所示:
computeByInt方法中委造,直接對(duì)int變量進(jìn)行操作戳鹅;
而computeByInteger方法中,sum = sum + 1昏兆,有一次自動(dòng)裝箱和一次自動(dòng)拆箱操作枫虏,極大地影響了性能。
從最終的結(jié)果來(lái)看爬虱,兩者之前有成千上萬(wàn)倍的性能差異隶债。
2.使用int[]數(shù)組替換Integer[]和ArrayList
我們知道Java的對(duì)象都是引用類型,如果是一個(gè)原始數(shù)據(jù)類型數(shù)組跑筝,它在內(nèi)存里是一段連續(xù)的內(nèi)存死讹;
而對(duì)象數(shù)組則不一樣,數(shù)據(jù)存儲(chǔ)的是引用曲梗,對(duì)象往往是分散地存儲(chǔ)在堆的不同位置赞警。
這種設(shè)計(jì)雖然帶來(lái)了極大靈活性,但是也導(dǎo)致了數(shù)據(jù)操作的低效虏两,尤其是無(wú)法充分利用現(xiàn)代CPU緩存機(jī)制愧旦。
下面舉個(gè)例子說(shuō)明二者性能的差異:
上面代碼運(yùn)行結(jié)果如下:
從運(yùn)行結(jié)果上看,sumInt方法和sumInteger兩個(gè)方法的性能差異巨大定罢,主要有兩個(gè)原因:
1. sumInteger進(jìn)行加法操作的時(shí)候笤虫,多了一次拆箱操作;
2. int[]數(shù)組內(nèi)數(shù)據(jù)存儲(chǔ)是連續(xù)的祖凫,可以充分利用現(xiàn)代CPU緩存機(jī)制琼蚯,而Integer[]中每個(gè)元素的intValue值,就分散地存儲(chǔ)在堆的不同位置蝙场,無(wú)法充分利用CPU緩存機(jī)制凌停,導(dǎo)致性能損失。
所以售滤,使用原始數(shù)據(jù)類型替換包裝類,使用數(shù)組替換動(dòng)態(tài)數(shù)組(如ArrayList)台诗,在性能極度敏感的場(chǎng)景往往具有比較大的優(yōu)勢(shì)完箩,一些追求極致性能的產(chǎn)品或者類庫(kù),會(huì)極力避免創(chuàng)建過(guò)多對(duì)象拉队。
當(dāng)然弊知,在大多數(shù)純業(yè)務(wù)功能代碼里,并沒(méi)有必要這么做粱快,還是以開發(fā)效率優(yōu)先秩彤。