《Java特種兵》1.5 功底補充

杜老師說 2022-01-07 10:02:01 阅读数:98

java 功底

本文是《Java特種兵》的樣章,感謝博文視點和作者授權本站發布

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的簡單測試

public static void main(String []args) { Integer a = 1; Integer b = 1; Integer c = 200; Integer d = 200; System.out.println(a == b); System.out.println(c == d);}

輸出結果:

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所示。

ADR90{5E5RLQEMNZO{T0P62

圖1-8 Integer.valueOf(int)方法的源碼截圖

根據代碼可以看出,當傳入i的值在[-128, IntegerCache.high]區間的時候,會直接讀取IntegerCache.cache這個數組中的值。

在代碼中為什麼使用i + 128作為數組的下標呢?

因為數組下標是從0開始的,而錶示的數字範圍是從-128開始的,加上128就正好對上了。

繼續跟踪源碼可以得到,在默認情况下IntegerCache.high是127。也就是說,如果傳入的int值是-128~127之間的數字,那麼通過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中沒有明確地說明這一點,但開發人員不會將官方文檔都學習一遍再來做開發吧,所以我們說它是“坑”。

有人問:真正的場景中會這樣嗎?

胖哥認為:肯定會,而且你遇不到的場景並不代錶不會發生,今天遇不到的事情並不代錶明天不會發生。例如,在某些工程設計中,某些狀態值有特殊的意義,如果它們是非連續排布的,那麼不在-128~127範圍內的可能性是肯定存在的。

這個例子很簡單,我們學到的應該不止這些,因為坑無處不在,我們要學會看源碼和本質;否則,即使是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範圍為-128~127,無法調整最大尺寸,即沒有設置,代碼中完全寫死,如果要擴展,需自己來做。

◎ 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所示。

SUINFCXN7`QR[L]IB9W})ZD

圖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操作,其實這個時候也許很多桶上的節點都是空的。

胖哥不想扯太多的集合類出來,把讀者“讀暈”,大家在理解這兩個基本的集合類基礎上,再去看其他的集合類也許會簡單一點。胖哥只想讓你知道這些東西是可選擇的,在什麼時候去選擇,如何去選擇完全要看你自己的功底,不論是做基礎程序、做功底還是去做優化,都需要深知它的細節,才能做到心中有數

原創文章,轉載請注明: 轉載自並發編程網 – ifeve.com本文鏈接地址: 《Java特種兵》1.5 功底補充

FavoriteLoading添加本文到我的收藏
版权声明:本文为[杜老師說]所创,转载请带上原文链接,感谢。 https://gsmany.com/2022/01/202201071002010086.html