????這段時間在讀《java程序性能優化》,看到里面有一些關于Java的一些數據結構相關的內容,主要涉及到String字符串類型和Map、List、Set等常用的數據結構的一些使用小技巧。感覺在平時的開發中還是很實用的,這里做一些延伸總結,記錄一下。
Part I.String字符串優化處理
1.1 String的實現介紹
Java中的String對象實現是通過對Char數組的擴展和進一步封裝實現的。它主要由三部分組成:Char數組、偏移量和String的長度??聪聢D源碼中String的構造函數可以明了的看出。Char value[]數組就是String的內容,它是String對象所表示的字符串的超集,意味著假如你定義了一個字符串String=”abc”,其實value[]數組其實可能不單單就是一個長度為3的存在a,b,c三個字符的數組,長度可能是4或者其他值,只不過其他位置沒存值。String的真實內容還需要用偏移量和長度在這個value[]數組中去定位和截取。這點是平時很容易忽略的,但是知道這點對后面要講的String的一些字符串處理方法的性能和內存泄漏的理解很有幫助。
String對象有三個基本特點:
■不變性;
■針對常量池的優化;
■類的final定義;
1.不變性是指String對象一旦生成了,就不能在對它進行改變。這個不變性其實和并發模式里面的不變模式異曲同工,有很多共通的地方。即一個對象一旦創建了,就不會再發生改變,并發的不變模式主要用在當一個對象需要被多個線程共享的時候,并且訪問很頻繁,這個時候不變性可以保證省略同步和鎖的情況下仍是線程安全的,從而大幅度提高系統性能
這里既然順便擴展介紹一下不變模式:不變模式就是依靠對象的不變性,確保在沒有同步操作的多線程環境中依然始終保持內部狀態一致性和正確性。不變模式的實現很簡單,為了確保對象一旦被創建就不會再被改變只要在創建對象時做到下面幾點:
①去除setter方法以及所有修改自身屬性的方法。
②將所有的屬性設置為私有,并用final標記,確保其不可修改性。
③確保沒有子類可以重載修改它的方法。
④有一個可創建完成對象的構造函數。
舉個栗子吧,實現一個產品對象,有序列號,名稱,價格三個屬性:
public final class Product{
????private final String no;
????private final String name;
????private final double price;
public Product (String no ,String name, double price){
????Super();
????this.no =no;
????this.name=name;
????this.price=price;
}
public String getNo(){
????return no;
}
public String getName(){
????return name;
}
public double getPrice(){
????return price;
}
}
在不變模式中final關鍵字起到了重要作用,不管是上面這個例子還是String的定義,都用到final關鍵字。對class的final定義保證了不變類沒有子類,確保了它里面的所有getter方法不會被重寫。對屬性值的fianl定義確保所有的數據只能在對象被構造時賦值一次,之后就不會再發生改變。我們可以看到String類的定義是完全符合以上這些要求的。?
2.針對常量池的優化,當兩個String對象擁有相同的值得時候,它們只引用常量池中的同一個拷貝。這個在同一個字符串反復出現時,可以大幅度節省內存空間。這里的常量池就是JVM中的常量池那個內存區域,需要注意的是當你使用new String("")方式創建的字符串時,會被存在堆中,因為對象實例都是存放在堆上。
String str1="abc";
String str2="abc";
String str3=new String("abc");
定義這三個變量,str1 == str2返回true,str1 == str3返回false,str1 == str3.intern()返回true。因為str3使用的new方法重新定義的,所以單獨在堆上開辟了一塊新的存儲空間,所以str1和str3是不相等的,但實際上str3指向的實體str1一樣,都是存在常量池中的“abc”,所以當str3.intern()返回String對象在常量池中的引用時,值和str1是一樣的。
3.類的final定義,用final關鍵字定義的類不可能有任何子類,從上面介紹不變性時已經講了,這樣可以保證多線程下的安全性。
1.2 String使用需要注意的地方
?????? 由于String對象是不可變對象,所以在對字符串進行修改操作時,比如說字符串拼接,替換時,String對象總是會生成新的對象,所以性能較差。針對字符串修改較多的操作,我們應該使用StringBuffer和StringBuilder類。
?????? 實際使用中,對String對象的“+”和“+=”操作其實都會在運行時編譯成StringBuilder的實現。但是當這種“+”和“+=”的操作出現在循環體中時,如果還是使用String的話,就會針對每次的“+”和“+=”操作都new一個StringBuilder對象,影響系統性能。所以在實際編碼中,盡量少用String的“+”和“+=”操作,String的concat()方法效率也要遠高于“+”和“+=”運算符,但和StringBuilder類的效率比起來還是要相差很遠。
??? 1.3 StingBuilder和StringBuffer的區別
?????? 通過查看源碼可以發現其實這兩個類實現的方法都差不多,只不過StringBuffer對幾乎所有的方法都做了同步處理,而StringBuilder并沒有做同步處理。因為做方法同步會對性能有一定的影響,所以StringBuilder的效率要優于StringBuffer,但是在多線程系統中,StringBuilder無法保證線程安全。
?????? 在初始化StringBuilder和StringBuffer時都可以預先設置一個容量參數,如果可以預先預測到大小時,可以設置一個初始容量。因為擴容函數的策略是將原有的容量翻倍,創建一個新的數組,然后將原有的數組中的內容復制到這個新的數組中去。
Part II.List類型
????List有三種最常用也是最重要的實現就是,ArrayList、Vector和LinkedList。以LinkedList的類圖為例。這三種List都是來自AbstractList的實現,AbstracList直接實現了List接口,并擴展自AbstractCollection。
????需要注意的是,三種List的實現并不完全一樣,ArrayList和Vector使用了數組實現,可以理解為這兩種List里面的方法其實是封裝好的對內部數組的操作。對這兩種List的操作等價于對內部數組的操作。而LinkedList則是使用雙向循環鏈表實現的。這兩種不同的實現方式決定了他們適用于不一樣的工作場景。ArrayList和Vector幾乎使用了相同的算法,唯一的區別就是對多線程的支持。ArrayList中沒有對方法進行線程同步處理,因此不是線程安全的。Vector中絕大部分方法都是做了線程同步的,是一種線程安全的實現。
????所以我們主要是要對比ArrayList和LinkedList的實現區分兩者更適用的場景。
操作一:在任意位置做插入操作。
????這是ArrayList的實現,可以看出里面需要先確定數組容量是否足夠,不夠的話涉及到擴容的處理,擴容需要重新創建一個數組并且將整個數組復制過去。在不需要擴容的情況下,對ArrayList的每一次插入操作也會涉及到一個數組復制,只有在插入List的尾端時,這個操作才不消耗資源。而且如果插入的元素位置越靠前,數組的重組開銷也越大。試想一下假設數組很大,此時要在List的頭部位置插入數值,這種操作是相當耗費資源和時間的。
????這是LinkedList的add函數的實現。對于使用雙向循環鏈表實現的LinkedList來說,不管在哪個位置插入都是一樣的。因為只需要修改一下對象的前后指針的指向位置。
所以在系統中如果需要經常在任意位置插入元素,則可以考慮使用LinkedList代替ArrayList。
操作二:在任意位置做刪除操作。
????在對ArrayList進行刪除操作時,由于自身是數組實現的,所以也需要進行數組重組。而LinkedList只需要遍歷到對應要刪除的對象位置然后修改前后位置的指針指向就好了。
操作三:遍歷列表。
????我們常用的遍歷方式有三種:ForEach操作,迭代器和for循環。書中對這三種方法做了比較,最后發現對ArrayList來說for循環這種隨機訪問的方式效率最高,這是由于他們實現了RandomAccess接口。迭代器比ForEach的方式效率要高一些。但是對于LinkedList來說,for循環這種隨機訪問的方式性能非常的差。所以對于ArrayList這些基于數組是實現的來說,在遍歷對象時可以優先考慮隨機訪問,對于LinkedList這種基于鏈表實現的,隨機訪問的性能特別差,要遍歷的話考慮用迭代的方式去實現。
Part III.Map類型
????Map是非常常用的數據結構,下面是從網上找的Map的類圖,圍繞著Map接口,最主要的實現類有Hashtable、HashMap、LinkedHashMap和treeMap。我們平時用的最多的可能是中間兩個。還有我們平時讀取配置文件經常用到的properties類也是Map的一種實現。值得我們關注的是,HashMap和Hashtable有兩套不同的實現,兩者都是實現了Map接口,但不同的是,Hashtable的大部分方法都做了同步,而HashMap沒有,所以HashMap不是線程安全的。其次Hashtable不允許key或者value使用null值,而HashMap可以。第三,他們對key的hash算法和hash值到內存索引的映射算法不同。
HashMap的實現原理:
????HashMap其實就是將Key做hash算法,然后得到的hash值映射到內存地址,直接取得key所對應的數據。在HashMap中,底層數據結構使用的是數組,內存地址其實就是數組下標索引。HashMap的高性能需要保證以下幾點:
■? ? Hash算法必須是高效的。
■? ?Hash值到內存地址的算法是快速的
■? ?根據內存地址可以直接取到對應的值
下圖中就是HashMap中hash計算方法
????key.hashCode()函數調用的是key鍵值類型自帶的哈希函數,返回int型散列值。在取到散列值后還做一個操作就是先右移了16位再和自己異或處理了一下。其實這個處理是有一定的原因的。因為HashMap的put方法里除了調用到了hash()方法外,調用到了putVal方法。在putVal方法里面有個被選中的部分,就是來確定數組下標的(上面說過了HashMap底層其實就是用數組和鏈表實現的)。因為我們知道HashMap的初始大小是16,但是通過hashCode()返回的散列值的二進制后面幾位可能都是0,這個時候直接和n-1(n是整個散列表的長度)按位與的話返回結果可能就是0,如果這樣的話,可能很多值都會堆積在散列表索引為0的這個位置,導致效率低下。
????舉個栗子,我們默認n為16,然后16-1=15,轉化為二進制后為1111。此時,假設hash的值不做 h = key.hashCode() ^ (h >>> 16) 這樣的處理而是直接調用 h = key.hashCode() 得到h,h的值為10100110101000000,那么1111和h按位與,i 結果就為0 。那么傳入的鍵值對放的位置就是tab[0]位置或者說是在掛tab[0]位置下面的鏈表的一個節點。這樣的話大多數類似于 xxxx xxxx 0000這樣的數和 1111 按位與或結果都為0。那么tab[0]位置有可能會存儲很多的值,即鏈表的長度會很長,這樣查找時就會降低了性能。
????如果我們這個時候做了右移16位然后和自己異或的操作的話,h的值就是00000000000000001,然后再和1111按位與,得到就是00000000000001110,即14。這個時候就不再返回0了。
????其實介紹了這么多,就是想說在計算hash值時效率是很高的。
????Hash沖突問題,在一些特殊情況下,HashMap還是會遇到一些性能問題,當通過Hash計算后,發現對應的內存中的同一個地址時,就出現了Hash沖突的問題,雖然HashMap底層使用數組實現的,但是數組里的元素不知一個簡單值,而是一個Entry對象,每個Entry對象里面包含key,value,next,hash等幾項。里面的next是一個指向另外一個Entry的指針。所以當put操作有沖突時,新的Entry依然會被放在其hash值對應的索引下標內,并替換原有的值,同時,為了保證舊值不丟失,會將新的Entry的next指向舊值。這樣就實現了一個數組索引空間內存放多個值,其實相當于一個數組項里面存的是一個單向鏈表。
????除了hashcode沖突導致的性能問題,還有個影響HashMap性能的是它的容量參數。和ArrayList和Vector這種基于數組的結構一樣,在空間不足時同樣需要進行擴展。
HashMap有兩個可以指定初始化大小的構造函數,其中一個可以設置一個負載因子,我們以這個函數為例。
????initialCapacity指定了HashMap的初始容量,loadFactor指定了其負載因子。一般初始默認情況,初始大小為16,負載因子為0.75.
負載因子=元素個數/內部數組總大小
????在HashMap內部還有一個threshold的變量,這個變量值等于當前數組總容量和負載因子的乘積。它表示HashMap的閾值,當HashMap的實際容量超過閾值時,HashMap就會進行擴容。很明顯,HashMap的擴容操作會遍歷整個HashMap,所以應該設置合理的初始大小和負載因子,以避免擴容操作的發生。
????上面我們介紹了HashMap的實現原理和一些需要注意的地方,雖然HashMap有很不錯的效率,但是它是無序的,被存入在HashMap的元素,在遍歷HashMap時,其輸出是無序的。如果希望元素是輸出時是按照存入的順序的話,就需要使用LinkedHashMap來替代。與HashMap不同的是,LinkedHashMap內部增加了一個鏈表,用于存放元素的順序。
?????? LinkedHashMap可以提供兩種類型的順序:一種是是元素插入時的順序,第二種是最近訪問的順序。默認是按插入順序的,當accessOrder聲明為true時,則是按照最近訪問順序。LinkedHashMap繼承了HashMap的Entry類,同時還增加了before和after兩個屬性,用來記錄某一個表項的前驅和后繼,并構成了循環鏈表。
? ??TreeMap,支持進行排序的一種Map,他實現了SortedMap接口,可以對元素進行排序。TreeMap是根據元素的Key進行排序的,為了確定Key的排序順序,可以使用兩種方法指定。(1)在TreeMap的構造函數中注入一個Comparator(2)使用一個實現了Comparable接口的Key。使用TreeMap時是必須進行排序操作的,所以使用過程中一定要通過上面兩個方式中的其中一種將排序規則遞給TreeMap。TreeMap的內部實現是基于紅黑樹,紅黑樹是一種平衡查找樹。關于紅黑樹的算法有些復雜,有空再詳細了解一下。
?????? 如果需要將排序功能假如HashMap時,盡量使用TreeMap而不是本人在程序中實現排序算法。
Part IV.Set接口
?????? Set接口在Collection接口之上沒有增加額外的操作,Set集合中的元素是不能重復的。關于Set的實現有HashSet、LinkedHashSet和TreeSet。這三個都是基于上面介紹的對應的Map的一種封裝而已,內部的方法都是差不多的。這里就不再重復介紹,只要記住元素不能重復就好了。