1. 程式人生 > >JAVA基礎第四章-集合框架Collection篇 JAVA基礎第一章-初識java JAVA基礎第二章-java三大特性:封裝、繼承、多型 JAVA基礎第三章-類與物件、抽象類、介面 記一次list迴圈刪除元素的突發事件!

JAVA基礎第四章-集合框架Collection篇 JAVA基礎第一章-初識java JAVA基礎第二章-java三大特性:封裝、繼承、多型 JAVA基礎第三章-類與物件、抽象類、介面 記一次list迴圈刪除元素的突發事件!

 業內經常說的一句話是不要重複造輪子,但是有時候,只有自己造一個輪子了,才會深刻明白什麼樣的輪子適合山路,什麼樣的輪子適合平地!

我將會持續更新java基礎知識,歡迎關注。

 

往期章節:

JAVA基礎第一章-初識java

JAVA基礎第二章-java三大特性:封裝、繼承、多型

JAVA基礎第三章-類與物件、抽象類、介面

 


 

說起集合框架,很多面試官在面試初級javaer的時候也是很喜歡問的一個知識點

我們先上一張圖看看

從上面的關係圖中,我們可以看到從上往下分呢~最上面的是介面,中間是抽象類,最下面就是各個具體的實現類,這個在我們上一章節中說到的抽象類與介面之間的關係的時候有提到過。

再從左往右看呢,也是大致分三塊,Iterator,Collection,Map 三個最頂級的介面,但是最主要的部分還是Collection,Map 2個介面,而Iterator更多的像是附加產品。

 

Collection

我們先看看Collection介面,從這個介面往下,他的子介面有List、Set、Queue。

List

List的具體實現類有 ArrayList、LinkedList,Vector。

ArrayList 

顧名思義,是一個“陣列型”的集合,對於陣列,我們應該知道的是,陣列在定義的時候就確定了大小,不可更改。那優點就是陣列的元素是通過索引訪問的,效率較高,因為陣列是一塊連續的儲存空間。

所以呢,ArrayList 就是在陣列的基礎上,增加了可以改變大小的介面(方法),如 add 、remove 等方法,方便我們去操作修改當前集合中的資料元素,當集合中新新增的資料超過了當前的儲存空間大小時

會申請一個新的儲存空間,然後將這些已有的資料拷貝過去,再新增新的資料 ,擴容後的集合的大小等於擴容前集合的1.5倍。

當我們刪除一個元素的時候,會將當前被刪除元素之後的元素統一向前移動一位,然後將最後的一位元素置為null,以便於gc回收。

所以,如果我們對一個ArrayList 有頻繁的增刪操作,這樣對效能是一個極大的損耗。

ArrayList 的資料儲存結構示意圖如下:

假設上圖中每一個黃色的格子都代表一個ArrayList 的儲存空間

步驟1:在我們第一次呼叫add方法增加一個元素1的時候,那麼list會直接擴容為預設的大小10,我們也可以在呼叫ArrayList 建構函式的時候傳入引數,指定初始化空間大小;

步驟2:我們再繼續新增資料,直到新增到11時,會判斷當前的儲存空間放不下要增加的資料了,這個時候會繼續擴容,之後再放入資料11;

步驟3:在這一步,我們決定刪除資料2,2的下標為1(陣列的下標都是從0開始),也就是呼叫remove方法;

注意:當我們呼叫size方法獲取到的是實際的儲存的資料大小,而不是整個ArrayList 獲得的儲存空間大小,例如 ,步驟2中呼叫size方法返回的會是11,而不是15。

 

LinkedList

從這個名字上,我們也可以大概知道,link是關聯的意思。LinkedList 和ArrayList 不同的一點是,他實現了Deque介面 這是一個雙向連結串列的介面。

我們先看下儲存結構示意圖:

 

 如上圖中所示,每一個節點都是一個Node物件,其中每個Node都有三個屬性,item 實際儲存的資料元素,如上圖中的綠色格子,next和prev,這樣就構成了一個連結串列結構。

而要注意的是next 和prev 也是一個Node物件,而Node是LinkedList 中的靜態內部類。如下圖中程式碼所示:

在這個連結串列中還存在2個屬性 first 和 last,分別用於存放整個集合連結串列中的頭和尾,如果只有一個元素,那麼first 和last就指向同一個元素。

資料的新增

當我們在連結串列中新增一個元素的時候,最後一個元素的null位置會設定引用新的Node節點,然後新新增的節點的prev會指向前一個元素的位置

我們從LinkedList 原始碼中做一些簡單的分析

 1     /**
 2      * Appends the specified element to the end of this list.
 3      *
 4      * <p>This method is equivalent to {@link #addLast}.
 5      *
 6      * @param e element to be appended to this list
 7      * @return {@code true} (as specified by {@link Collection#add})
 8      */
 9     public boolean add(E e) {
10         linkLast(e);
11         return true;
12     }

如上所示,從 “Appends the specified element to the end of this list.” 這句註釋中,我們就大致可以明白其意,當我們呼叫add方法增加元素的時候,預設是在末尾追加資料。

這個時候add方法中會呼叫linkLast方法,具體程式碼如下:

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

上述程式碼中,首先會將當前的last賦給l,然後新建一個Node物件,傳入新新增的資料,以及將當前集合中的last賦值給新新增節點的prev屬性。

然後將新建的物件賦值給last,之後再判斷最開始的last,也就是當前的l是否為null,如果是null,也就代表集合是空的,這是第一個元素,那麼就把它賦給frist,否則,那麼就說明已經有元素存在了,讓上一個元素的next指向當前新建的物件。

最後再進行調整大小等操作。

資料新增的操作示意圖如下:

 

資料的刪除

下面我們再來看一下,LinkedList 的刪除操作,也就是我們預設呼叫remove方法。

原始碼如下所示:

 1  /**
 2      * Retrieves and removes the head (first element) of this list.
 3      *
 4      * @return the head of this list
 5      * @throws NoSuchElementException if this list is empty
 6      * @since 1.5
 7      */
 8     public E remove() {
 9         return removeFirst();
10     }

同樣,從“Retrieves and removes the head (first element) of this list.”註釋中,我們大致可以明白,大意是檢索並移除list頭部的元素。

在這個方法中直接呼叫了removeFirst方法,下面我們看一下removeFirst程式碼:

 1    /**
 2      * Removes and returns the first element from this list.
 3      *
 4      * @return the first element from this list
 5      * @throws NoSuchElementException if this list is empty
 6      */
 7     public E removeFirst() {
 8         final Node<E> f = first;
 9         if (f == null)
10             throw new NoSuchElementException();
11         return unlinkFirst(f);
12     }

如上所示,在這個程式碼中,直接判斷是不是存在first,也就是集合是不是空的,不是那就繼續呼叫unlinkFirst方法,

unlinkFirst程式碼如下所示:

 1   /**
 2      * Unlinks non-null first node f.
 3      */
 4     private E unlinkFirst(Node<E> f) {
 5         // assert f == first && f != null;
 6         final E element = f.item;
 7         final Node<E> next = f.next;
 8         f.item = null;
 9         f.next = null; // help GC
10         first = next;
11         if (next == null)
12             last = null;
13         else
14             next.prev = null;
15         size--;
16         modCount++;
17         return element;
18     }

如上所示,從“Unlinks non-null first node f”註釋中,可知,解開非空的第一個節點的關聯。首先將first節點的f.item 以及f.next設定為null,以便於gc回收。在將f.next置為null之前賦值給了臨時的next。

然後判斷next是否為null,如果是,則說明後面沒有元素了,這是集合中的唯一一個元素,將last也設定為null;否則,將next中的prev設定為null。

資料刪除操作示意圖如下:

所以呢,當我們對linkedList進行增刪操作的時候只需要對2個節點進行修改,而對其他節點沒有任何影響。

 

 

Vector

這個類和ArrayList基本相似,不同的點在於他是執行緒安全的,也就是在同一個時刻,只能有一個執行緒訪問Vector;另外Vector擴容不同於ArrayList,他每次擴容預設都是按2倍,而ArrayList是1.5倍。

ArrayList、LinkedList、 Vector三者之間的異同

ArrayList與LinkedList相比查詢速度快,增刪速度慢。

所以如果只是查詢,建議用前者,反之建議用後者,因為後者再增刪的時候,只需要修改2個節點的 prev和next ,而不存在複製當前已有的元素到新的儲存空間。

Vecor和ArrayList基本相似,區別是前者是執行緒安全的,後者不是。但是2個底層實現都是陣列,LinkedList底層實現是連結串列。

集合的遍歷

經常用到有三種方式,程式碼示意如下:

 1    /* 第一種遍歷方式 
 2         for迴圈的遍歷方式
 3    */
 4         for (int i = 0; i < lists.size(); i++) {
 5             System.out.print(lists.get(i));
 6         }
 7  8         
 9         /* 第二種遍歷方式 
10           foreach的遍歷方式
11         */
12         for (Integer list : lists) {
13             System.out.print(list);
14         }
15 16         
17         /* 第三種遍歷方式
18          Iterator的遍歷方式
19       */
20         for (Iterator<Integer> list = lists.iterator(); list.hasNext();) {
21             System.out.print(list.next());
22         }

for迴圈效率高於Iterator迴圈,高於foreach迴圈,因為我們都知道他們的底層實現都是陣列,而for迴圈是通過下標查詢是最適合的遍歷方式; 而foreach迴圈是在Iterator基礎上進行的,所以最慢。

另外,迭代器遍歷方式, 適用於連續記憶體儲存方式,比如陣列、 ArrayList,Vector。 缺點是隻能從頭開始遍歷, 優點是可以一邊遍歷一邊刪除。

for迴圈這種方式遍歷比較靈活,可以指定位置開始遍歷。效能最高,但有一個缺點就是遍歷過程中不允許刪除元素,否則會拋ConcurrentModificationException。

注:但是曾經發現在刪除倒數第2個元素的時候,並不會丟擲異常,詳見 記一次list迴圈刪除元素的突發事件!

 

 

Set

Set不允許包含相同的元素,如果試圖把兩個值相同元素加入同一個集合中,add方法返回false。

Set判斷兩個物件相同不是使用==運算子,而是根據equals方法。也就是說,只要兩個物件用equals方法比較返回true,Set就不會再儲存第二個元素。

set的實現類有HashSet、TreeSet、LinkedHashSet

HashSet

不能保證元素的排列順序;不是執行緒安全的;集合元素可以是null,但只能放入一個null,其他相同資料也只能有一份存在;

對於HashSet我們要知道的是,他是依靠HashMap中的key去維護存放的資料,所以HashSet的這些特性都是和HashMap的key相關的。

hashSet預設建構函式程式碼如下:

1    /**
2      * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
3      * default initial capacity (16) and load factor (0.75).
4      */
5     public HashSet() {
6         map = new HashMap<>();
7     }

如上程式碼所示,他是呼叫了HashMap的預設建構函式。

LinkedHashSet

LinkedHashSet和HashSet的區別是前者是有序的,也就是當你插入資料的時候會按順序排放,這樣我們遍歷時就可以按照之前插入的順序獲取資料。

和HashSet相似也是在建構函式中呼叫了LinkedHashMap構造方法,程式碼如下所示:

1   /**
2      * Constructs a new, empty linked hash set with the default initial
3      * capacity (16) and load factor (0.75).
4      */
5     public LinkedHashSet() {
6         super(16, .75f, true);
7     }

因為LinkedHashSet繼承了HashSet,所以他呼叫super,就是呼叫的HashSet的構造器,在HashSet中再呼叫了LinkedHashMap,程式碼如下所示:

 1  /**
 2      * Constructs a new, empty linked hash set.  (This package private
 3      * constructor is only used by LinkedHashSet.) The backing
 4      * HashMap instance is a LinkedHashMap with the specified initial
 5      * capacity and the specified load factor.
 6      *
 7      * @param      initialCapacity   the initial capacity of the hash map
 8      * @param      loadFactor        the load factor of the hash map
 9      * @param      dummy             ignored (distinguishes this
10      *             constructor from other int, float constructor.)
11      * @throws     IllegalArgumentException if the initial capacity is less
12      *             than zero, or if the load factor is nonpositive
13      */
14     HashSet(int initialCapacity, float loadFactor, boolean dummy) {
15         map = new LinkedHashMap<>(initialCapacity, loadFactor);
16     }

 

TreeSet

TreeSet是SortedSet介面的唯一實現類,TreeSet可以確保集合元素處於排序狀態。

TreeSet支援兩種排序方式,自然排序 和定製排序,其中自然排序為預設的排序方式。向TreeSet中加入的應該是同一個類的物件。

自然排序不用多說,那定製排序的意思就是,我們可以自己通過實現Comparator介面覆寫其中比較方法,然後按照我們意願進行排序,比如自然排序是升序,我們通過覆寫這個排序方法,可以修改成降序。

TreeSet帶比較器的建構函式程式碼如下:

 1  /**
 2      * Constructs a new, empty tree set, sorted according to the specified
 3      * comparator.  All elements inserted into the set must be <i>mutually
 4      * comparable</i> by the specified comparator: {@code comparator.compare(e1,
 5      * e2)} must not throw a {@code ClassCastException} for any elements
 6      * {@code e1} and {@code e2} in the set.  If the user attempts to add
 7      * an element to the set that violates this constraint, the
 8      * {@code add} call will throw a {@code ClassCastException}.
 9      *
10      * @param comparator the comparator that will be used to order this set.
11      *        If {@code null}, the {@linkplain Comparable natural
12      *        ordering} of the elements will be used.
13      */
14     public TreeSet(Comparator<? super E> comparator) {
15         this(new TreeMap<>(comparator));
16     }

TreeSet預設建構函式程式碼如下:

 1   /**
 2      * Constructs a new, empty tree set, sorted according to the
 3      * natural ordering of its elements.  All elements inserted into
 4      * the set must implement the {@link Comparable} interface.
 5      * Furthermore, all such elements must be <i>mutually
 6      * comparable</i>: {@code e1.compareTo(e2)} must not throw a
 7      * {@code ClassCastException} for any elements {@code e1} and
 8      * {@code e2} in the set.  If the user attempts to add an element
 9      * to the set that violates this constraint (for example, the user
10      * attempts to add a string element to a set whose elements are
11      * integers), the {@code add} call will throw a
12      * {@code ClassCastException}.
13      */
14     public TreeSet() {
15         this(new TreeMap<E,Object>());
16     }

從上面的原始碼註釋中,我們大致可以明白,其意是構造一個新的空的樹形set,排序按照元素的自然順序排序,所有要插入到set中的元素必須實現Comparable介面,同時,這些元素還必須是互相可以比較的。

如果使用者嘗試新增一個string型別的資料到integer型別的set中,那麼會丟擲ClassCastException 異常。

 

 

關於Map介面,我們將在下一章節中做一個詳細的分析

 

 

 

 

 


 

文中若有不正之處,歡迎批評指正!