《Java特種兵》1.5 功底補充
1.5 功底補充
看完1.4節發現胖哥廢話很多貌似沒啥幹貨了
為了不讓大家認為功底隻有String那麼一點點東西胖哥就再增加對原生態類型、集合類的說明這兩方麵的內容相信所有的Java開發者都必然會用到。
1.5.1 原生態類型
原生態類型是“神馬”
原生態類型就是Java中不屬於對象的那5%部分。
那到底是哪些東西呢
包含boolean、byte、short、char、int、float、long、double這8種常見的數據類型Primitive。
好麻煩為啥會用它們呢用對象不可以嗎
計算機中的運算基礎都來源於簡單數字包括Java即使是包裝後的對象Wrapper在真正計算的時候也是通過內在的數字來完成的Java失去了它們就好比魚兒失去了水一樣失去了生命力。
它與包裝後的對象有什麼區別呢
包裝後的對象會按照對象的規則存儲在堆中例如int所對應的就是Integer類的對象而“線程棧”上隻存儲引用地址。對象自然會占用相對較大的空間存放在堆中在原生態類型中“棧”上直接保存了它們的值而不是引用Reference。
下麵看個例子。
代碼清單1-5 一個Integer的簡單測試
1 |
public static void main(String []args) {
|
2 |
Integer a = 1 ;
|
3 |
Integer b = 1 ;
|
4 |
Integer c = 200 ;
|
5 |
Integer d = 200 ;
|
6 |
System.out.println(a == b);
|
7 |
System.out.println(c == d);
|
8 |
} |
輸出結果
true
false
這個結果在較低版本的JDK當中不會出現。
現在胖哥來解釋一下這個結果。
在編譯階段若將原始類型int賦值給Integer類型就會將原始類型自動編譯為Integer.valueOf(int)如果將Integer類型賦值給int類型則會自動轉換調用intValue()方法如果Integer對象為空這時會在自動拆箱的時候拋空指針這個自動轉換可以通過後文中介紹javap命令的方法來證明。
這些賦值操作可能不是那麼明顯例如一些集合類的寫入、一些對比操作這就需要我們知道什麼時候會自動拆裝箱。換句話說它簡化了代碼但是並不是讓我們一無所知。
即使是這樣兩個結果也應該一樣要麼都是true要麼都是false但為何不一樣呢這算是一個Java API的坑如果我們不知道這些坑稍微不留神就會掉進去。知道了裝箱是Integer.valueOf(int)方法那麼就來看看Integer.valueOf(int)方法的源碼如圖1-8所示。
圖1-8 Integer.valueOf(int)方法的源碼截圖
根據代碼可以看出當傳入i的值在[-128, IntegerCache.high]區間的時候會直接讀取IntegerCache.cache這個數組中的值。
在代碼中為什麼使用i + 128作為數組的下標呢
因為數組下標是從0開始的而表示的數字範圍是從-128開始的加上128就正好對上了。
繼續跟蹤源碼可以得到在默認情況下IntegerCache.high是127。也就是說如果傳入的int值是-128127之間的數字那麼通過Integer.valueOf(int)得到的對象是被cache的自然的對於同一個數字cache的對象是同一塊內存地址所以第1個輸出結果是true。第2個輸出已經不在這個範圍因此會重新new Integer(int)創建一個新對象所以得到的結果是false。
我們可以通過設置JVM啟動參數-Djava.lang.Integer.IntegerCache.high=200來間接設置IntegerCache.high值也可以通過設置參數-XX:AutoBoxCacheMax來達到目的這個不用查官方資料看看源碼以及源碼周邊的注釋就懂了。如果要將這個值變得更大來滿足自己的需求則可以在啟動參數中增加該值縮小也是一樣的道理。
這好像是做好事將我們的數據cache起來更加節約空間了但是有的同學開始認為Integer可以用“==”匹配了因為大家自己“測試”的時候發現1、2、3、4等數據都是沒有問題的但是程序發布後出現了詭異的問題而這個最不容易被認為是問題的地方卻真的成了問題。而Java API中沒有明確地說明這一點但開發人員不會將官方文檔都學習一遍再來做開發吧所以我們說它是“坑”。
有人問真正的場景中會這樣嗎
胖哥認為肯定會而且你遇不到的場景並不代表不會發生今天遇不到的事情並不代表明天不會發生。例如在某些工程設計中某些狀態值有特殊的意義如果它們是非連續排布的那麼不在-128127範圍內的可能性是肯定存在的。
這個例子很簡單我們學到的應該不止這些因為坑無處不在我們要學會看源碼和本質否則即使是Java本身提供的API出現了“坑”也會讓我們“防不勝防”在技術麵前變得十分“可憐”。
我們可以說這個API寫得不好沒有說明詳細的使用情況但是一個老A不應當被“武器”所玩弄而是要駕馭武器老A即使拿一把普通步槍也同樣能戰勝拿著“狙擊步槍”的普通士兵因為他們除了擁有極強的戰鬥素質外還深知武器的脾氣與秉性這是人與武器之間的駕馭和被駕馭關係。
也許自動拆裝箱還有另一個很大的“坑”就是如果大家不知道自動拆裝箱是怎麼完成的可能就會有更多的問題發生在程序中傳遞參數可能一會用Integer類型一會用int類型自然的就一會在做拆箱操作一會在做裝箱操作這貌似沒有太大的問題但每次裝箱的時候都有可能會創建一個對象因為很多時候數字不一定在cache範圍內較低版本的JDK是沒有cache的。另外這種裝箱操作是很隱藏的。例如我們想要用一個int類型的數字來作為HashMap的Key那麼在put()操作的時候就會自動發生裝箱操作因為Key被認為是Object的HashMap需要獲取這個對象的hashCode()方法來做離散規則所以它會自動轉型為Integer。同樣的如果想將許多基本類型的數據放在List裏麵在add()操作的時候也會自動發生裝箱操作。此時如果數據取出來後變成了基本類型再用這個基本類型放入另一個集合類就又會發生裝箱操作在這個過程中就會隱藏地浪費大量的空間而自己卻什麼也不知道。
關於對象空間的大小請參看第3章的內容。
□ 橫向擴展
通過對Integer的一些了解想到了Boolean、Byte、Short、Long、Float、Double它們是否有同樣的情況胖哥不想寫重複的東西直接給出結果大家可以自己去看看代碼看看這些類型中的valueOf()方法是如何操作的或者說自動裝箱是如何操作的。
◎ Boolean的兩個值true和false都是cache在內存中的無須做任何改造自己new Boolean是另外一塊空間。
◎ Byte的256個值全部cache在內存中和自己new Byte操作得到的對象不是一塊空間。
◎ Short、Long兩種類型的cache範圍為-128127無法調整最大尺寸即沒有設置代碼中完全寫死如果要擴展需自己來做。
◎ Float、Double沒有cache要在實際場景中cache需自己操作例如在做圖紙尺寸時可以將每種常見尺寸記錄在內存中。
□ 思維發散擴展
如果上麵的操作變成Integer與int類型比較會是什麼樣的結果呢如果是兩個Integer數據做“>”、“>=”、“<”、“<=”比較做switch case操作會得到什麼結果反射的時候是否有特殊性
這個結果大家可以去論證且測試結果可以就認為是當前虛擬機的設計規範。下麵胖哥直接給出結果。
◎ 當Integer與int類型進行比較的時候會將Integer轉化為int類型來比較也就是通過調用intValue()方法返回數字直接比較數字在這種情況下是不會出現例子中的問題的。
◎ Integer做“>”、“>=”、“<”、“<=”比較的時候Integer會自動拆箱就是比較它們的數字值。
◎ switch case為選擇語句匹配的時候不會用equals()而是直接用“==”。而在switch case語句中語法層麵case部分是不能寫Integer對象的隻能是普通的數字如果是普通的數字就會將傳入的Integer自動拆箱所以它也不會出現例子中的情況。
在JDK 1.7中支持對String對象的switch case操作這其實是語法糖在編譯後的字節碼中依然是if else語句並且是通過equals()實現的。
◎ 在反射當中對於Integer屬性不能使用field.setInt()和field.getInt()操作。在本書的src/chapter01/AutoBoxReflect.java中用例子來說明。
1.5.2 集合類
如果讀了上一節後你有所體悟那麼胖哥認為你可以跳過此節因為此節知識為上一節的一個平行擴展我們不重視知識點本身而在於讓大家了解到許多秘密。
集合類非常多從早期的java.utils的普通集合類到現在增加的java.util.concurrent包下麵的許多並發集合類其實我們有些時候隻是知道它們是很好用的東西但在遇到某些問題的時候是否會想到是它們造成的就像String一樣它們的使用技巧有哪些它們的設計思想是什麼
本節不討論並發包就簡單說說集合類的故事。
疑惑集合類中包含了List、Map、Set幾大類基本接口而我們最常用、最簡單的集合類是什麼呢
答曰ArrayList、HashMap。
那麼當你用ArrayList的時候是否想起了LinkedList、Vector當你用HashMap的時候是否想起了TreeMap、HashSet、HashTable當你要排序的時候是否想起了SortedSet等。
它們有何區別在什麼情況下使用
在討論String後麵的部分內容中我們提到了StringBuilder它內在的數組的實現有大量的拷貝這在集合類的內存拷貝方麵的體現更加明顯並且占用空間更大。
占用更大空間的原因是集合類都是存儲對象的引用的在32bit及64bit壓縮模式下一個引用會占用4個字節在64bit非壓縮模式下會占用8個字節而StringBuilder隻是存儲char字符的數組每個位隻占用2個字節。
此時以ArrayList為例我們看看它的add(E e)方法源碼如圖1-9所示。
圖1-9 ArrayList的add(E e)方法源碼截圖
通過源碼我們發現如果空間不夠會通過Arrays.copyOf創建一個新的內存空間新空間的大小最小為原始空間的3/2 倍+1並將原始空間的內容拷貝進去。
這裏所提到的新空間的大小為原始空間的3/2倍+1是最小的在add(E)方法中不會發生而在addAll()方法中會發生。addAll()允許同時寫入多個數據如果寫入的數據較多每次按照1.5倍數擴容可能發生多次擴容這樣就會有多餘的垃圾空間產生addAll()操作就會對比寫入的量與1.5倍的大小誰大就用誰這個道理在StringBuilder中我們就知道因此文中提到的是“最小”。
我們知道ArrayList是基於“數組”來實現的本書3.5節會詳細介紹內存結構如果遇到remove()操作add(int index)指定的位置寫入操作我們有沒有慮過ArrayList內部其實會移動相關的數據而且隨著數組越長移動的數據會越多。如果要替換一個數據我們會不會先remove再add一個數據或者是通過set(int index , E e)將對應下標的數據替換掉。
基於數組的ArrayList是非常適合於基於下標訪問的這是它擅長的地方又回到基本的數據結構與算法了。下麵胖哥給出幾個簡單擴展希望大家去思考。
◎ 在經常做修改操作的列表中或者在數組通過下標檢索並不是那麼多的情況下你是否考慮過使用LinkedList呢因為ArrayList通常始終有些數組元素是空著的。
◎ 在知道List長度範圍的情況下你是否在實例化 ArrayList的時候帶上長度例如new ArrayList(128); 這樣就降低了內存碎片和內存拷貝的次數。
◎ 當List太大的時候是否考慮過對它做分段處理而不要一次加載到內存中其實很多OOM都會在集合類中找到問題。
◎ 常見的框架中用了什麼集合類在什麼情況下也會出現問題
大家熟知的HashMap浪費空間更加嚴重它的代碼裏麵有一個0.75因子當寫入HashMap的數據個數不是說所使用的數組下標個數而是所有元素個數也就是說包含了同一個下標的鏈表中的所有元素個數達到數組長度的0.75後數組會自動擴展1倍並且還需要做一個rehash操作其實這個時候也許很多桶上的節點都是空的。
胖哥不想扯太多的集合類出來把讀者“讀暈”大家在理解這兩個基本的集合類基礎上再去看其他的集合類也許會簡單一點。胖哥隻想讓你知道這些東西是可選擇的在什麼時候去選擇如何去選擇完全要看你自己的功底不論是做基礎程序、做功底還是去做優化都需要深知它的細節才能做到心中有數
最後更新:2017-05-23 14:02:36