开发高性能JAVA应用程序基础(集合篇)
集合類在開發(fā)中使用非常頻繁,使用時合理的選擇對提高性能小有幫助。而且大部分面試都會有與集合相關(guān)的問題,例如ArrayList和LinkedList的對比。
了解API的集成與操作架構(gòu),才能了解何時該采用哪個類,而不會只能抄寫范例。本文也嘗試用一些現(xiàn)實生活中的物品來描述各個集合類的特性,僅僅是幫助快速理解和記憶,不必太過較真。
首先看類結(jié)構(gòu)圖:
按用途分為可重復收集對象的List、不可重復的Set和鍵值對應的Map三個頂層接口,對應三個最常用的實現(xiàn)類是ArrayList、HashSet和HashMap,如果沒有其他的限制它們應該是默認的選擇。
注意Vector、Hashtable和Stack上都有Legacy字樣,如果不是為了兼容舊代碼,不應再使用這三個類。
一、ArrayList對比LinkedList
很多文章介紹時直接說ArrayList查詢快插入慢,LinkedList插入刪除快,但是這個是有前提的。
代碼為證:
- public static void main(String[] args) {
- List<Integer> array1 = new ArrayList<>(), array2 = new ArrayList<>();
- List<Integer> link1 = new LinkedList<>(), link2 = new LinkedList<>();
- for(int i = 0; i < 100000; i++) {
- array1.add(i); array2.add(i);
- link1.add(i); link2.add(i);
- }
- System.out.println("從0開始ArrayList消耗:" + getTime(array1, 0));
- System.out.println("從0開始LinkedList消耗:" + getTime(link1, 0));
- System.out.println("從50000開始ArrayList消耗:" + getTime(array2, 50000));
- System.out.println("從50000開始LinkedList消耗:" + getTime(link2, 50000));
- }
- public static long getTime(List<Integer> list, int index){
- long start = System.nanoTime();
- for(int i = 0; i< 100000; i++){
- list.add(index, i);
- }
- return TimeUnit.MILLISECONDS.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS);
- }
- 從0開始ArrayList消耗:3701
- 從0開始LinkedList消耗:17
- 從50000開始ArrayList消耗:2370
- 從50000開始LinkedList消耗:13363
來分析:
ArrayList內(nèi)部使用Object數(shù)組來保存收集的對象,數(shù)組在內(nèi)存中是連續(xù)的線性空間。可以想象成一排緊緊排列的桌子,因為緊密連接距離較短,隨機找到某個位置(索引)的桌子會比較快,但是如果想在中間某個位置插入一個新的桌子,那必須把排在后面的桌子一張一張向后移動,以空出一個位置。同樣,如果刪除一個桌子(怪異的說法),則需要把后面一張一張向前移動。如果是更換呢,很簡單,搬走一張桌子,把新的放在原來的位置(快)。
LinkedList內(nèi)部是雙向鏈表結(jié)構(gòu),形象是一個用線串聯(lián)的珠串,兩個珠子之間的線可能非常長。如果想找到某一個位置的珠子,必須從頭開始,沿著線一個接一個的向后找(查詢慢)。如果想在兩個珠子之間插入一個新的,那就很簡單了,把中間的線拆開,新珠子兩端的線分別接上。刪除同樣簡單。
那么為什么測試結(jié)果中LinkedList第二次表現(xiàn)遠遠落后呢?原因是想插入首先必須先定位到位置,第二次測試選定從索引50000開始,LinkedList每次操作前都需要從0開始尋址到50000,查詢消耗大量時間,所以實際執(zhí)行很慢。
LinkedList可以當作Stack和Queue使用,這是ArrayList不具備的。
二、HashMap和TreeMap
HashMap內(nèi)部使用一個名為Entry的內(nèi)部類數(shù)組保存key-value對,使用無參構(gòu)造方法的情況下, 創(chuàng)建一個長度為16(capacity)的數(shù)組。其中的16個存儲位置通常稱為哈希桶。當存儲一個Entry對象時,首先通過key的hashCode()獲得一個整形的散列碼,和數(shù)組長度做取模計算獲得位置索引。
那么一個明顯的問題是,如果兩個key經(jīng)過上面的計算后得到一個相同的位置索引怎么辦?這種情況稱為哈希沖突,HashMap解決的辦法是把新Entry和原來位置的Entry建立起鏈表,如果再有第三個相同index的key加進來,那么繼續(xù)加在鏈表的前部。一個幫助記憶的形象是皇帝冠冕前的多列垂珠。
調(diào)用get(key)方法查找時,先通過hashCode()和取模計算獲得第幾個桶,再對桶上的鏈表遍歷列表并通過key.equals()逐個比較來確定對象。
這樣看來如果鏈表過長,也會影響查詢速度,這時候就是負載因子(load factor)出場的時候了。當HashMap中已存入的對象數(shù)量超過capacity * load factor時,會對數(shù)組擴容,變?yōu)樵瓉淼膬杀丁?/p>
一些優(yōu)化提示:
1 如果開發(fā)時已經(jīng)預知HashMap要存入的對象數(shù)量,可以直接指定初始容量,避免頻繁擴容
2 int和String非常適合當作key
3 如果key使用自己的對象,那么一個好的hashCode()算法非常重要,應該使對象盡可能均勻的分布在各哈希桶,同時應該覆蓋equals方法。Effective Java書中對怎樣實現(xiàn)一個像樣的hashCode()給出了指導。
TreeMap基于紅黑樹結(jié)構(gòu)實現(xiàn),理論上來說各方面性能都比HashMap差,使用它的唯一理由就是排序。在使用keySet對TreeMap遍歷時,按照key的compareTo方法排序輸出。
三、HashSet、TreeSet、LinkedHashSet、LinkedHashMap
HashSet是最常用的Set類,內(nèi)部借助HashMap實現(xiàn),特性可以直接參考HashMap。
TreeSet內(nèi)部借助于TreeMap實現(xiàn),同理使用它的理由也是獲得排序后的對象列表。
LinkedHashSet 與HashSet類似,區(qū)別是使用iterator遍歷時,LinkedHashSet按照對象插入的順序輸出。理論上插入時性能比HashSet差。
LinkedHashMap 與HashMap類似,區(qū)別是遍歷時,按照對象插入的順序輸出。LinkedHashMap有一個三個參數(shù)的構(gòu)造方法:
- public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
如果第三個參數(shù)設置為true,排序方式為按照訪問順序排序,可以借助該功能實現(xiàn)簡單的采用"最近最少使用"失效算法(LRU)的緩存。
四、線程安全
上面提到的類中,除了Vector和Hashtable,全部都是線程不安全的。
示例1:
- public static void main(String[] args) {
- ArrayList<Integer> list = new ArrayList<>();
- Thread t1 = new Thread() {
- public void run() {
- while(true) {
- list.add(1);
- }
- }
- };
- Thread t2 = new Thread() {
- public void run() {
- while(true) {
- list.add(2);
- }
- }
- };
- t1.start();
- t2.start();
- }
執(zhí)行結(jié)果:
- Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 549
- at java.util.ArrayList.add(ArrayList.java:444)
- at Program$2.run(Program.java:28)
分析:ArrayList的add方法演示代碼
- public void add(Object o) {
- if(next == list.length) {
- list = Arrays.copyOf(list, list.length * 2);
- }
- list[next++] = o;
- }
ArrayList在添加對象時先判斷數(shù)組是否已滿,如果已滿則擴容。多線程狀態(tài)下,當next ==list.length-1時,兩個線程輪流切換執(zhí)行都不符合擴容條件進入下一步,此時第一個線程執(zhí)行賦值并把next+1,第二個線程執(zhí)行時next=list.length,出現(xiàn)ArrayIndexOutOfBoundsException。
示例2:
- public class ArrayListDemo implements Runnable {
- static ArrayList<Integer> list = new ArrayList<>();
- static CountDownLatch latch = new CountDownLatch(10000);
- public static void main(String[] args) throws InterruptedException {
- ExecutorService exec = Executors.newCachedThreadPool();
- for(int i = 0; i < 10000; i++) {
- exec.execute(new ArrayListDemo());
- }
- latch.await();
- System.out.println("list.size()=" + list.size());
- }
- @Override
- public void run() {
- list.add(1);
- latch.countDown();
- }
- }
執(zhí)行結(jié)果:
- list.size()=9977
示例3:
- public static void main(String[] args) throws InterruptedException {
- HashMap<String, String> map = new HashMap<>();
- map.put("a", "a");
- Iterator<String> iter = map.keySet().iterator();
- ExecutorService exec = Executors.newCachedThreadPool();
- exec.execute(new Runnable() {
- @Override
- public void run() {
- map.put("b", "b");
- }
- });
- exec.shutdown();
- while(iter.hasNext()) {
- System.out.println(iter.next());
- }
- }
執(zhí)行結(jié)果:
- Exception in thread "main" java.util.ConcurrentModificationException
- at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429)
- at java.util.HashMap$KeyIterator.next(HashMap.java:1453)
- at ArrayListDemo.main(ArrayListDemo.java:34)
多線程下安全讀寫集合類有三種常見辦法:
1 JDK5以后首選concurrent包下的集合類,包括ConcurrentHashMap、CopyOnWriteArrayList和CopyOnWriteArraySet
2 讀寫操作時加鎖,使用synchronized關(guān)鍵字或者java.util.concurrent.locks下的類
3 使用Collections.synchronizedList、Collections.synchronizedMap等方法獲得線程安全集合。
總結(jié)
以上是生活随笔為你收集整理的开发高性能JAVA应用程序基础(集合篇)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: springmvc和mybatis整合关
- 下一篇: 电磁流量计现场参数检查