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