Java暗箱操作之自動(dòng)裝箱與拆箱
我以前在寫Android項(xiàng)目的時(shí)候,估計(jì)寫得最多最熟練的幾句話就是:
- List<Integer> list = new ArrayList<Integer>(); list.add(1); //把一個(gè)整數(shù)加入到集合中
- int i = list.get(0); //從集合中取出元素
ArrayList用起來(lái)是多么的順手!當(dāng)時(shí)我只知道尖括號(hào)<>里面只能加入大寫字母開(kāi)頭的Object類型,不能加入int、char、double這些原始類型,至于原因沒(méi)研究過(guò),這么規(guī)定就這么用唄。
但是隨著對(duì)“碼農(nóng)”式無(wú)腦學(xué)習(xí)法的逐漸厭倦,我開(kāi)始重新審視Java代碼內(nèi)部的東西。
首當(dāng)其沖的就是每個(gè)項(xiàng)目一定用到的ArrayList。在我的另一篇博客中已經(jīng)對(duì)ArrayList的源碼實(shí)現(xiàn)做了大體的分析。然而還有幾個(gè)源碼中看不出來(lái),但是確實(shí)存在疑點(diǎn)的問(wèn)題亟待解決。
- List<Integer> list = new ArrayList<Integer>();
這句代碼中每個(gè)元素是Integer類型,那么往list里面add新元素的時(shí)候必須為Integer,比如加個(gè)String進(jìn)去,代碼下面就會(huì)出現(xiàn)紅色波浪線。
但是這句list.add(1) 眾所周知,代碼里面隨便寫個(gè)不帶小數(shù)點(diǎn)的數(shù)字,那它就是個(gè)int;把一個(gè)int加到一個(gè)只能有Integer的List中不報(bào)錯(cuò),不覺(jué)得有貓膩嗎?
同樣地,int i = list.get(0),取出list中索引為0的元素,也應(yīng)該是個(gè)Integer,為什么接收的變量就是個(gè)int呢?這是一個(gè)多么明顯的類型不匹配錯(cuò)誤啊!
以前,我確實(shí)聽(tīng)說(shuō)過(guò)“包裝類”這個(gè)概念,但是忽視了它,因?yàn)槲乙恢庇X(jué)得Integer,F(xiàn)loat這些東西,說(shuō)難聽(tīng)點(diǎn)就是擺出來(lái)裝裝逼的,只是因?yàn)長(zhǎng)ist不接受int,float類型,迫不得已發(fā)明了Integer,Float,實(shí)際并沒(méi)有卵用。
最近看了《Effective Java》里面的一節(jié),名字叫“Prefer primitive types to boxed primitives”。里面羅列了很多原始類型和包裝類型混用的例子,搞得我暈頭轉(zhuǎn)向的。下面是其中一段代碼:
- Long sum = 0L; for (long i = 0; i < Integer.MAX_VALUE; i++) { sum += i; } System.out.println(sum);
據(jù)書中講,這是一段運(yùn)行效率低到不可救藥的代碼,你能看出其中的問(wèn)題嗎?
反正我當(dāng)時(shí)看到這段代碼就明顯感覺(jué)到,Java對(duì)于原始類型與相應(yīng)的Object類型的轉(zhuǎn)化,在編譯過(guò)程中肯定做了什么見(jiàn)不得人的事情……
下面正式引出本文的話題:AutoBoxing and Unboxing(自動(dòng)裝箱&自動(dòng)拆箱)
看一個(gè)最簡(jiǎn)單的例子:
- Character ch = 'a'; //Character是char的包裝類
這里沒(méi)有出現(xiàn)任何錯(cuò)誤,其實(shí)編譯器在代碼優(yōu)化的時(shí)候,暗中轉(zhuǎn)化成了下面的代碼:
- Character ch = Character.valueOf('a');
這就是說(shuō),"="右側(cè)自動(dòng)調(diào)用Character類對(duì)應(yīng)的靜態(tài)方法構(gòu)造出了一個(gè)Character的實(shí)例。
為了進(jìn)一步說(shuō)明,這里稍微看一下valueOf方法
- public static Character valueOf(char c) { return c < 128 ? SMALL_VALUES[c] : new Character(c); } //如果字符在緩沖區(qū)中,直接取出Character實(shí)例,否則要重新構(gòu)造
- private static final Character[] SMALL_VALUES = new Character[128]; //類中自帶一個(gè)靜態(tài)的緩沖區(qū),保存128個(gè)常用ASCII碼字符對(duì)應(yīng)的Character實(shí)例,免去每次重新構(gòu)造實(shí)例的麻煩
- static { for (int i = 0; i < 128; i++) { SMALL_VALUES[i] = new Character((char) i); //調(diào)用構(gòu)造函數(shù)
- } }
對(duì)于Integer等其他包裝類,自身都帶有一個(gè)靜態(tài)的valueOf方法。每次編譯器檢查到需要把一個(gè)int傳給Integer時(shí),就自動(dòng)對(duì)代碼進(jìn)行轉(zhuǎn)化。
比如上面的list.add(1),在編譯過(guò)程中編譯器發(fā)現(xiàn)要傳進(jìn)去的參數(shù)是int,但是要接收的是Integer,于是代碼變?yōu)椋?/span>
- list.add(Integer.valueOf(1));
以上就是自動(dòng)裝箱(auto-boxing)的過(guò)程。
自動(dòng)裝箱一般在兩種情況下會(huì)發(fā)生(以int和Integer為例):
1、把int作為一個(gè)方法的參數(shù)傳進(jìn)來(lái),但是方法體里面希望得到的參數(shù)是Integer;
2、在賦值過(guò)程中,"="左邊是Integer變量,右邊是int變量。
這樣一來(lái),自動(dòng)拆箱的過(guò)程就順理成章了??匆韵麓a:
- public static int sumEven(List<Integer> li) { int sum = 0; for (Integer i: li) if (i % 2 == 0) sum += i; return sum; }
在循環(huán)體內(nèi)做了兩次拆箱操作,編譯器會(huì)轉(zhuǎn)換成以下代碼:
- public static int sumEven(List<Integer> li) { int sum = 0; for (Integer i: li) if (i.intValue() % 2 == 0) sum += i.intValue(); return sum; }
Integer的intValue方法就簡(jiǎn)單多了,直接返回被包裝的int值
自動(dòng)拆箱的用處跟自動(dòng)裝箱正好相反,也是用在參數(shù)傳遞和賦值過(guò)程中,這里就不贅述了。
- @Override public int intValue() { return value; //value是Integer的成員變量
- }
我們?cè)賮?lái)分析一下那段超級(jí)低效的代碼吧,經(jīng)過(guò)自動(dòng)拆裝箱轉(zhuǎn)換之后應(yīng)該是這樣子的:
- Long sum = Long.valueOf(0L); for (long i = 0; i < Integer.MAX_VALUE; i++) { sum = Long.valueOf(sum.longValue() + i); //低效所在
- } System.out.println(sum.toString());
在循環(huán)體里面,簡(jiǎn)簡(jiǎn)單單只有一句話,竟然包含一次拆箱和一次裝箱操作,在經(jīng)過(guò)20多億次的循環(huán)之后,效率損耗得難以置信!
既然拆箱和裝箱可以看做“逆運(yùn)算”,那么為什么還要進(jìn)行多余的操作呢?直接用原始值運(yùn)算,然后一次裝箱不是更省事嗎
參考資料:https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html