程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> java核心數據結構總結,java核心數據結構

java核心數據結構總結,java核心數據結構

編輯:JAVA綜合教程

java核心數據結構總結,java核心數據結構


  JDK提供了一組主要的數據結構的實現,如List、Set、Map等常用結構,這些結構都繼承自java.util.collection接口。

  • List接口

  List有三種不同的實現,ArrayList和Vector使用數組實現,其封裝了對內部數組的操作。LinkedList使用了循環雙向鏈表的數據結構,LinkedList鏈表是由一系列的鏈表項連接而成,一個鏈表項包括三部分:鏈表內容、前驅表項和後驅表項。

  LinkedList的表項結構如圖:

     

  LinkedList表項間的連接關系如圖:

  

  可以看出,無論LinkedList是否為空,鏈表都有一個header表項,它即表示鏈表的開頭也表示鏈表的結尾。表項header的後驅表項便是鏈表的第一個元素,其前驅表項就是鏈表的最後一個元素。

  對基於鏈表和基於數組的兩種List的不同實現做一些比較:

  1、增加元素到列表的末尾:

  在ArrayList中源代碼如下:

1 public boolean add(E e) {
2         ensureCapacityInternal(size + 1);  // Increments modCount!!
3         elementData[size++] = e;
4         return true;
5     }

  add()方法性能的好壞取決於grow()方法的性能:

 1 private void grow(int minCapacity) {
 2         // overflow-conscious code
 3         int oldCapacity = elementData.length;
 4         int newCapacity = oldCapacity + (oldCapacity >> 1);
 5         if (newCapacity - minCapacity < 0)
 6             newCapacity = minCapacity;
 7         if (newCapacity - MAX_ARRAY_SIZE > 0)
 8             newCapacity = hugeCapacity(minCapacity);
 9         // minCapacity is usually close to size, so this is a win:
10         elementData = Arrays.copyOf(elementData, newCapacity);
11     }

  可以看出,當ArrayList對容量的需求超過當前數組的大小是,會進行數組擴容,擴容的過程中需要大量的數組復制,數組復制調用System.arraycopy()方法,操作效率是非常快的。

  在LinkedList源碼中add()方法:

1 public boolean add(E e) {
2         linkLast(e);
3         return true;
4     }

  linkLast()方法如下:

 1 void linkLast(E e) {
 2         final Node<E> l = last;
 3         final Node<E> newNode = new Node<>(l, e, null);
 4         last = newNode;
 5         if (l == null)
 6             first = newNode;
 7         else
 8             l.next = newNode;
 9         size++;
10         modCount++;
11     }

  LinkedList是基於鏈表實現,因此不需要維護容量大小,但是每次都新增元素都要新建一個Node對象,並進行一系列賦值,在頻繁系統調用中,對系統性能有一定影響。性能測試得出,在列表末尾增加元素,ArrayList比LinkedList性能要好,因為數組是連續的,在末尾增加元素,只有在空間不足時才會進行數組擴容,大部分情況下追加操作效率還是比較高的。

  2、增加元素到列表的任意位置:

  List接口還提供了在任意位置插入元素的方法:void add(int index,E element)方法,由於實現方式不同,ArrayList和LinkedList在這個方法上存在一定的差異。由於ArrayList是基於數組實現的,而數組是一塊連續的內存,如果在數組的任意位置插入元素,必然會導致該位置之後的所有元素重新排序,其效率相對較低。

  ArrayList源碼實現:

1 public void add(int index, E element) {
2         rangeCheckForAdd(index);
3         ensureCapacityInternal(size + 1);  // Increments modCount!!
4         System.arraycopy(elementData, index, elementData, index + 1,
5                          size - index);
6         elementData[index] = element;
7         size++;
8     }

  可以看出每次插入都會進行數組復制,大量的數組復制操作導致系統性能效率低下。並且數組插入的位置越靠前,數組復制的開銷就越大。因此,盡可能插入元素在其尾端附近,有助於提高該方法的性能。

  LinkedList的源碼實現:

 1 public void add(int index, E element) {
 2         checkPositionIndex(index);
 3 
 4         if (index == size)
 5             linkLast(element);
 6         else
 7             linkBefore(element, node(index));
 8     }
 9 void linkBefore(E e, Node<E> succ) {
10         // assert succ != null;
11         final Node<E> pred = succ.prev;
12         final Node<E> newNode = new Node<>(pred, e, succ);
13         succ.prev = newNode;
14         if (pred == null)
15             first = newNode;
16         else
17             pred.next = newNode;
18         size++;
19         modCount++;
20     }

  對於LinkedList的在尾端插入和對任意位置插入數據是一樣的,並不會因為插入位置靠前而導致效率低下。因此,在應用中,如果經常往任意位置插入元素,可以考慮使用LinkedList提到ArrayList。

  3、刪除任意位置的元素:

  List接口還提供了在任意位置刪除元素的方法:remove(int index)方法。在ArrayList中,對於remove()方法和add()方法一樣,在任意位置移除元素,都需要數組復制。

  ArrayList的remove()方法的源碼如下: 

 1 public E remove(int index) {
 2         rangeCheck(index);
 3 
 4         modCount++;
 5         E oldValue = elementData(index);
 6 
 7         int numMoved = size - index - 1;
 8         if (numMoved > 0)
 9             System.arraycopy(elementData, index+1, elementData, index,
10                              numMoved);
11         elementData[--size] = null; // clear to let GC do its work
12 
13         return oldValue;
14     }

  可以看出,在ArrayList的每一次刪除操作,都需要進行數組重組,並且刪除元素的位置越靠前,數組重組的開銷就越大。

  LinkedList的remove()方法的源碼:

 1 public E remove(int index) {
 2         checkElementIndex(index);
 3         return unlink(node(index));
 4     }
 5 E unlink(Node<E> x) {
 6         // assert x != null;
 7         final E element = x.item;
 8         final Node<E> next = x.next;
 9         final Node<E> prev = x.prev;
10 
11         if (prev == null) {
12             first = next;
13         } else {
14             prev.next = next;
15             x.prev = null;
16         }
17 
18         if (next == null) {
19             last = prev;
20         } else {
21             next.prev = prev;
22             x.next = null;
23         }
24 
25         x.item = null;
26         size--;
27         modCount++;
28         return element;
29     }
 1 Node<E> node(int index) {
 2         // assert isElementIndex(index);
 3 
 4         if (index < (size >> 1)) {
 5             Node<E> x = first;
 6             for (int i = 0; i < index; i++)
 7                 x = x.next;
 8             return x;
 9         } else {
10             Node<E> x = last;
11             for (int i = size - 1; i > index; i--)
12                 x = x.prev;
13             return x;
14         }
15     }

  在LinkedList中首先通過循環找到要刪除的元素,如果元素位於前半段則,從前往後找;若位置位於後半段,則從後往前找,但是要移除中間的元素,卻幾乎要遍歷半個List。所有,無論元素位於較前還是較後,效率都比較高,但是位於中間效率就非常低。

  4、容量參數:

  容量參數是ArrayList和Vector等基於數組的List特有的性能參數,它表示初始化數組的大小。當數組所存儲的元素的數量超過其原有的大小時,它就會進行擴容,即進行一次數組復制,因此,合理設置數組大小有助於減少擴容次數,從而提升系統性能。

   5、遍歷列表:

  在JDK1.5之後,至少有三種遍歷列表的方式:forEach操作,迭代器,for循環。通過測試發現,forEach綜合性能不如迭代器,而for循環遍歷列表時,ArrayList的性能表現最好,而LinkedList的性能差的無法忍受,因為LinkedList進行隨機訪問,總會進行一次列表的遍歷操作。

  對於ArrayList是基於數組來實現的,隨機訪問效率快,因此有限選擇隨機訪問。而LinkedList是基於鏈表實現的,隨機訪問的性能差,應該避免使用。

  • Map接口

  圍繞著Map接口,最主要的實現類有:HashMap、hashTable、LinkedHashMap和TreeMap。在HashMap的子類中還有Properties類的實現。

  1、HashMap和Hashtable

  首先說一下,HashMap和Hashtable的區別:Hashtable的大部分方法都實現了同步,而HashMap沒有。因此,HashMap不是線程安全的。其次,Hashtable不允許key或value使用null值,而HashMap可以。第三是內部的算法不同,它們對key的hash算法和hash值到內存索引的映射算法不同。

  HashMap就是將key做hash算法,然後將hash值映射到內存地址,直接取得key所對應的數據。在HashMap的底層使用的是數組,所謂的內存地址即數組的下標索引。

  HashMap中不得不提的就是hash沖突,需要存放到HashMap中的元素1和元素2經過hash計算,發現對應的內存地址一樣。如下圖:

  

  HashMap底層使用的是數組,但是數組內的元素不是簡單的值,而是一個Entry對象。如下圖所示:

  可以看出,HashMap的內部維護了一個Entry數組,每個entry表項包括:key、value、next、hash。next部分表示指向另一個Entry。在HashMap的put()方法中,可以看到當put()方法有沖突時,新的entry依然會安放在對應的索引下標內,並替換掉原來的值,同時為了保證舊值不丟失,會將新的entry的next指向舊值。這樣便實現了在一個數組索引空間內存放多個值。

  HashMap的put()操作的源碼:

 1 public V put(K key, V value) {
 2         if (table == EMPTY_TABLE) {
 3             inflateTable(threshold);
 4         }
 5         if (key == null)
 6             return putForNullKey(value);
 7         int hash = hash(key);
 8         int i = indexFor(hash, table.length);
 9         for (Entry<K,V> e = table[i]; e != null; e = e.next) {
10             Object k;
11             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
12                 V oldValue = e.value;//取得舊值
13                 e.value = value;
14                 e.recordAccess(this);
15                 return oldValue;//返回舊值
16             }
17         }
18 
19         modCount++;
20         addEntry(hash, key, value, i);//添加當前表項到i位置
21         return null;
22     }
23 void addEntry(int hash, K key, V value, int bucketIndex) {
24         if ((size >= threshold) && (null != table[bucketIndex])) {
25             resize(2 * table.length);
26             hash = (null != key) ? hash(key) : 0;
27             bucketIndex = indexFor(hash, table.length);
28         }
29 
30         createEntry(hash, key, value, bucketIndex);
31     }
32 void createEntry(int hash, K key, V value, int bucketIndex) {
33         Entry<K,V> e = table[bucketIndex];
34         table[bucketIndex] = new Entry<>(hash, key, value, e);//將新增元素放到i位置,並把它的next指向舊值
35         size++;
36     }

  基於HashMap的這種實現,只要對hashCode()和hash()的方法實現的夠好,就能盡可能的減少沖突,那麼對HashMap的操作就等價於對數組隨機訪問的操作,具有很好的性能。但是,如果處理不好,在產生大量沖突的情況下,HashMap就退化為幾個鏈表,性能極差。

  2、容量參數:

  因為HashMap和Hashtable底層是基於數組實現的,當數組空間不足時,就會進行數組擴容,數組擴容就會進行數組復制,是十分影響性能的。

  HashMap的構造函數:

1 public HashMap(int initialCapacity)
2 public HashMap(int initialCapacity, float loadFactor)

  initialCapacity指定HashMap的初始容量,loadFactor是指負載因子(元素個數/元素總量),HashMap中還定義了一個阈值,它是當前數組容量和負載因子的乘積,當數組的實際容量超過阈值時,就會進行數組擴容。

  另外,HashMap的性能一定程度上取決於hashCode()的實現,一個好的hashCode()的實現,可以盡可能減少沖突,提升hashMap的訪問速度。

  3、LinkedHashMap

  HashMap的一大缺點就是無序性,放入的數據,在遍歷取出時候是無序的。如果需要保證元素輸入時的順序,可以使用LinkedHashMap。

  LinkedHashMap繼承自HashMap,因此,其性能是比較好。在HashMap的基礎上,LinkedHashMap內部又增加了一個鏈表,用於存放元素的順序。LinkedHashMap提供了兩種類型的順序,一種是元素插入時的順序,一種是最近訪問的順序。

1  public LinkedHashMap(int initialCapacity,
2                          float loadFactor,
3                          boolean accessOrder)

  其中,accessOrder為true是,是按元素最後訪問時間排序,當accessOrder為false時,按插入順序排序。

  4、TreeMap

  TreeMap可以對元素進行排序,TreeMap是基於元素的固有順序而排序的(有Comparable或Comparator確定)。

  TreeMap是根據key進行排序的,為了確定key的排序算法,可以使用兩種方法指定:

  1:在TreeMap的構造函數中注入Comparator

  TreeMap(Comparator<? super K> comparator);

  2:使用一個實現了Comparable接口的key。

  TreeMap是內部是基於紅黑樹實現,紅黑樹是一種平衡查找樹,其統計性能優於平衡二叉樹。

  • Set接口

  set集合中的元素是不能重復的,其中最主要的實現就是HashSet、LinkedHashSrt和TreeSet。查看Set接口實現類,可以發現所有的Set的一些實現都是相應Map的一種封裝。

  set特性如圖所示:

  • 集合操作的一些優化建議

  1、分離循環中被重復調用的代碼。如:for(int i=0;i<list.size();i++),可以將list.size()分離出來。

  2、省略相同的操作

  3、減少方法的調用,方法調用時消耗系統堆棧的,會犧牲系統的性能。

  • RandomAccess接口

  RandomAccess接口是一個標識接口,本身沒有提供任何方法。主要的目的是為了標識出那些可以支持快速隨機訪問的List的實現。例如,根據是否實現RandomAccess接口在變量的時候選擇不同的遍歷實現,以提升性能。

  

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved