整理了一篇關於java集合的文章,有圖有真相

1.概述

java集合是用來保存對象的,而數組只能存儲基本數據類型,爲什麼要引入集合呢?我們先來看一看數組的特點和缺點,首先數組是一段連續的內存空間,在初始化之後長度就確定了,並且也確定了初始化時的類型,這兩個特點就導致了數組存在一些弊端

  • 長度固定不易於擴展
  • 只能存儲固定類型的數據
  • 數組提供的方法API少,增刪改等操作不方便
  • 存儲有序可重複的元素

在這裏插入圖片描述

基於這些問題,java推出了一系列的用不同數據結構實現的集合,可以動態的存儲對象,並且提供了大量的API,方便我們進行操作,不管是在做算法題還是開發具體的應用,這些API都起到了非常重要的作用,舉個例子,springMVC中視圖和數據封裝,JSON數據傳遞,Spring存儲bean都是基於集合中的Map結構,下面我們就來具體瞭解以下這些集合吧

2.集合框架圖

在這裏插入圖片描述
這是整個java集合的框架圖,虛線框表示抽象類(接口可看作特殊的抽象類),粗實線是我們關注的重點集合類,仔細觀察上面這張圖,可以看出這個框架可以分爲CollectionMap兩個不同的部分

2.1 Collection系

在這裏插入圖片描述
基於Collection接口實現的集合又可以分爲

  • Set:元素無序不可重複的集合
  • List:元素有序可重複的集合

2.2 Map系

在這裏插入圖片描述

3.Collection接口

這個接口是LIst , Set , Queue 的父接口,Collection接口的方法:
在這裏插入圖片描述

只要實現了這個接口,都可以使用這些API,需要注意的是,當我們的對象需要添加進容器時,最好重寫equals方法,因爲在contains或者remove時都會用equals判斷該對象是否存在,比如ArrayList中的contains方法:

	public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

	public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

4.Foreach與迭代器

我們之前使用foreach都是在遍歷數組的時候使用的,前面的繼承圖也指出了Collection可以得到迭代器:

  • public interface Collection<E> extends Iterable<E>

由於繼承了Iterable接口,其內部有Iterator<E> iterator();來使實現了Collection的類可以通過這個方法獲取迭代器,換句話說,只要實現了Iterable接口,就要重寫Iterator<E> iterator();方法,就可以通過Foreach來遍歷元素,下面舉個例子:

  public static void main(String[] args) {
        ArrayList<Person> array = new ArrayList<Person>();
 
        Person p1 = new Person("Tom1");
        Person p2 = new Person("Tom2");
        Person p3 = new Person("Tom3");
        Person p4 = new Person("Tom4");
 
        array.add(p1);
        array.add(p2);
        array.add(p3);
        array.add(p4);
 
        Iterator<Person> iterator = array.iterator();
 
        for (Person pp : array){
            System.out.println(pp.getName());
        }
        
         while(iterator.hasNext()){
            System.out.println(iterator.next().getName()); //輸出的是wang,而不是tom
        }

        

我們可以發現,用Foreach和迭代器實現的效果是一樣的,其實Foreach就是利用迭代器實現的,這是因爲程序在運行時,發現我們正在用foreach遍歷集合,並且該集合實現了Iterable接口,就會在底層用迭代器的hasNext和next方法來實現遍歷,需要注意的是:

由於迭代器和Collection實現類都有remove方法,且foreach是通過迭代器實現,故在使用增強for循環時,使用集合的remove方法則會導致原來的集合變化而導致錯誤,所以應該使用迭代器的remove方法

既然提到了使用迭代時remove方法會出錯,大家就不妨看看這篇文章:
Java List的remove()方法陷阱以及性能優化

4.1迭代器執行原理

受制於篇幅以及不想重新寫一遍,可以去看我的另一篇文章:
Iterator迭代器執行原理,hasNext和next指針問題

4.2使用for循環還是迭代器Iterator對比

採用ArrayList對隨機訪問比較快,而for循環中的get()方法,採用的即是隨機訪問的方法,因此在ArrayList裏,for循環較快

採用LinkedList則是順序訪問比較快,iterator中的next()方法,採用的即是順序訪問的方法,因此在LinkedList裏,使用iterator較快

從數據結構角度分析,for循環適合訪問順序結構,可以根據下標快速獲取指定元素.而Iterator 適合訪問鏈式結構,因爲迭代器是通過next()和Pre()來定位的.可以訪問沒有順序的集合.

而使用 Iterator 的好處在於可以使用相同方式去遍歷集合中元素,而不用考慮集合類的內部實現(只要它實現了 java.lang.Iterable 接口),如果使用 Iterator 來遍歷集合中元素,一旦不再使用 List 轉而使用 Set 來組織數據,那遍歷元素的代碼不用做任何修改,如果使用 for 來遍歷,那所有遍歷此集合的算法都得做相應調整,因爲List有序,Set無序,結構不同,他們的訪問算法也不一樣.(還是說明了一點遍歷和集合本身分離了

5.List

5.1 List概述

List接口是Collection接口的子接口之一,這個接口的實現類存儲元素的特點:有序可重複,集合中每個元素都可以根據索引獲取:list.get(int index),可以將它理解爲一個"動態數組",至於爲什麼動態下面再講,它有三個主要的實現類:

  • ArrayList:底層數據結構是數組,查詢快,增刪慢,線程不安全,效率高,可以存儲重複元素
  • LinkedList 底層數據結構是鏈表,查詢慢,增刪快,線程不安全,效率高,可以存儲重複元素
  • Vector:底層數據結構是數組,查詢快,增刪慢,線程安全,效率低,可以存儲重複元素

在這裏插入圖片描述

5.2 ArrayList

這個是我們用的最多的List接口了,ArrayList底層使用
在這裏插入圖片描述
數組來存儲元素,它的構造方法如下:

public ArrayList(int initialCapacity)//構造一個具有指定初始容量的空列表。    
public ArrayList()      //默認構造一個初始容量爲10的空列表。    
public ArrayList(Collection<? extends E> c)//構造一個包含指定 collection 的元素的列表

這裏要說明一下,在jdk7中,通過默認構造器創建ArrayList是在底層創建長度是10的Object數組,通過不斷的add(E e)添加元素,當下次添加達到最大容量,就會觸發擴容機制,即擴容爲原來的1.5倍,並且將原數組中的內容複製到擴完容的新數組中,在jdk8中出現了一點變化,使用默認構造器初始化時:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

其實是創建了一個空的object數組,當我們在第一次添加數據的時候纔會創建長度爲10的數組,其他方面還是和JDK7一樣

5.3 LinkedList

這個集合類底層使用雙向鏈表來存儲數據,內部沒有聲明數組,而是定義了一個內部類和兩個指針:


transient Node<E> first;

transient Node<E> last;

private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

對於雙向鏈表的增刪改查大家可以去看看LinkedList的底層源碼實現,並嘗試手寫出一個鏈表

5.4 Vector

這個類是一個線程安全的類,和ArrayList在jdk7中默認初始化容量爲10的數組一樣,不同的地方在於它默認擴容爲原來的兩倍,現在很少幾乎不用這個類了

5.5 ArrayList和LinkedList

ArrayList

  • 優點:ArrayList是實現了基於動態數組的數據結構,因爲地址連續,一旦數據存儲好了,查詢操作效率會比較高(在內存裏是連着放的)
  • 缺點:因爲地址連續, ArrayList要移動數據,所以插入和刪除操作效率比較低

LinkedList

  • 優點:LinkedList基於鏈表的數據結構,地址是任意的,所以在開闢內存空間的時候不需要等一個連續的地址,對於新增和刪除操作add和remove,LinedList比較佔優勢。LinkedList 適用於要頭尾操作或插入指定位置的場景
  • 缺點:因爲LinkedList要移動指針,所以查詢操作性能比較低

比較

  • 當需要對數據進行對此訪問的情況下選用ArrayList,當需要對數據進行多次增加刪除修改時採用LinkedList

5.6 ArrayList和Vector

ArrayList和Vector都是用數組實現的,區別:

  • Vector是多線程安全的,線程安全就是說多線程訪問同一代碼,不會產生不確定的結果。而ArrayList不是,這個可以從源碼中看出,Vector類中的方法很多有synchronized進行修飾,這樣就導致了Vector在效率上無法與ArrayList相比
  • 兩個都是採用的線性連續空間存儲元素,但是當空間不足的時候,兩個類的增加方式不同,Vector可以設置增長因子,而ArrayList不可以

6.Set

6.1 概述

set接口沒有定義新的方法,使用的都是collection接口中的方法,Set集合中不存在下標,因此無法通過下標遍歷,可以用迭代器和forEach遍歷,用hashSet舉例來說明Set接口的特性:

  • 無序性:不等於隨機性,存儲的數據在底層數組中並非按照數組索引的順序添加,而是根據哈希值決定的
  • 不可重複性:保證添加的元素按照equals()判斷時,不能返回true,即相同的元素只能添加一個

它主要有三個實現類,簡單介紹一下:

  • HashSet:主要實現類,可存儲null值
  • LinkedHashSet:HashSet的子類,遍歷其內部數據時,可按照添加的順序遍歷
  • TreeSet:可以按照添加對象的指定屬性進行排序

6.2 HashSet

HashSet底層數據結構採用哈希表實現,元素無序且唯一,線程不安全,效率高,可以存儲null元素,但不能存多個null,元素的唯一性是靠所存儲元素類型是否重寫hashCode()和equals()方法來保證的,如果沒有重寫這兩個方法,則無法保證元素的唯一性,所以往HashSet添加的元素必須重寫hashcode()和equals()方法

  • HashSet存儲元素過程:

存儲元素首先會使用hash()算法函數生成一個int類型hashCode散列值,然後已經的所存儲的元素的hashCode值比較,如果hashCode不相等,則所存儲的兩個對象一定不相等,此時存儲當前的新的hashCode值處的元素對象;如果hashCode相等,存儲元素的對象還是不一定相等,此時會調用equals()方法判斷兩個對象的內容是否相等,如果內容相等,那麼就是同一個對象,無需存儲;如果比較的內容不相等,那麼就是不同的對象,就該存儲了,此時就要採用哈希的解決地址衝突算法,在當前hashCode值處類似一個新的鏈表, 在同一個hashCode值的後面存儲存儲不同的對象,這樣就保證了元素的唯一性

其實閱讀過源碼的人已經知道了,HashSet底層是用的HashMap實現的:
在這裏插入圖片描述
所以HashSet的存儲元素的不可重複性,以及特殊的數組+鏈表存儲元素的方式也和HashMap有關了

6.3 LinkedHashSet

底層數據結構採用鏈表和哈希表共同實現,鏈表保證了元素的順序與存儲順序一致,哈希表保證了元素的唯一性。線程不安全,效率高

6.4 TreeSet

底層數據結構採用二叉樹來實現,元素唯一且已經排好序;唯一性同樣需要重寫hashCode和equals()方法,二叉樹結構保證了元素的有序性。根據構造方法不同,分爲自然排序(無參構造)和比較器排序(有參構造),自然排序要求元素必須實現Compareable接口,並重寫裏面的compareTo()方法,元素通過比較返回的int值來判斷排序序列,返回0說明兩個對象相同,不需要存儲;比較器排需要在TreeSet初始化是時候傳入一個實現Comparator接口的比較器對象,或者採用匿名內部類的方式new一個Comparator對象,重寫裏面的compare()方法,返回0就認爲兩個對象相等

7.List和Set小結

  • List,Set都是繼承自Collection接口
  • List特點:元素有放入順序,元素可重複 ,Set特點:元素無放入順序,元素不可重複,重複元素會覆蓋掉,(注意:元素雖然無放入順序,但是元素在set中的位置是有該元素的HashCode決定的,其位置其實是固定的,加入Set 的Object必須定義equals()方法 ,另外list支持for循環,也就是通過下標來遍歷,也可以用迭代器,但是set只能用迭代,因爲他無序,無法用下標來取得想要的值。)
  • Set和List對比:
    • Set:檢索元素效率低下,刪除和插入效率高,插入和刪除不會引起元素位置改變。
    • List:和數組類似,List可以動態增長,查找元素效率高,插入刪除元素效率低,因爲會引起其他元素位置改變。
  • TreeSet 是二叉樹(紅黑樹的樹據結構)實現的,Treeset中的數據是自動排好序的,不允許放入null值
  • HashSet是基於Hash算法實現的,其性能通常都優於TreeSet。爲快速查找而設計的Set,我們通常都應該使用HashSet,在我們需要排序的功能時,我們才使用TreeSet

在這裏插入圖片描述

8.Map

Map用於保存具有映射關係的數據,Map裏保存着兩組數據:key和value,它們都可以使任何引用類型的數據,但key不能重複。所以通過指定的key就可以取出對應的value。
在這裏插入圖片描述

8.1 散列表

在講HashMap之前我們需要先了解什麼是散列表HashTable,首先它是一種數據結構,它會爲存入它的對象計算哈希值(hashcode),也叫散列碼,hashcode由特殊的算法產生,在java中,object類就有計算hashcode的方法,而且該方法應該與equals方法兼容,即equals相等的兩對象hashcode相等,hashcode相等的兩對象equals不一定相等,而hashcode相等了往往會進行equals進行二次判斷,才能最終確定兩個對象是否相等,在Java中,散列表利用數組+鏈表實現,其添加元素的具體思路就是:計算對象得到hashcode值,然後經過特定的算法得到在數組中存儲的下標,如果該下標對應的內存空間沒有其他元素就可以添加進去,如果有,就是我們說的散列衝突(哈希衝突),這時候就需要對已經存在的對象進行比較,看這個對象是否存在

在java8中,如果數組存儲元素滿了,則該數組對應索引上的鏈表變爲平衡二叉樹,也就是紅黑樹

想要更好的提高散列表的性能,需要指定一個初始長度的數組,有一種說法認爲講數組的長度設置爲一個素數,爲2的冪,通常是16,當需要擴容時,就再次進行2的冪運算,但是我們並不能知道要存儲多少個元素,如果這個散列表小了,很容易存滿,就需要再散列,即擴容,講所有元素再次計算hashcode,插入新的散列表中,丟棄原有的表,裝載因子決定何時對錶進行再散列,一般是0.75,即表中超過75%的位置存儲了數據,就對錶進行再散列

8.2 HashMap

研究HashMap對於一個java開發人員來說必不可少,甚至是一個必須的過程了,衆所HashMap底層是數組+鏈表+紅黑樹實現的,那麼我們就來探討一下這個過程,剛纔講到了散列表,HashMap就是用的散列表實現的,我們這裏針對JDK8來探討,先搞懂一下HashMap類中各個常量的意思:
在這裏插入圖片描述
HashMap中存入鍵值對數據其實是存入它的內部類Node數組中的:
在這裏插入圖片描述
在這裏插入圖片描述
已經可以看出,HashMap的散列表其實就是Node數組+Node類中的指針實現的,結合上面的散列表和相關常量的解釋,現在來說一下HashMap的存儲過程:
HashMap在調用默認構造器初始化的時候並沒有立刻創建數組,而是在首次put時創建長度爲16的數組,並使用默認加載因子,在第一次put時,擴容臨界值threshold爲12,如果之後的put一旦超過這個值就會觸發擴容,擴容的方式爲兩倍,而添加元素的過程和HashSet一樣,這裏就不贅述了,需要注意的是,當數組長度>64,且鏈表長度>8時,就會將鏈表轉化爲紅黑樹

8.3 HashMap和HashTable的比較

在這裏插入圖片描述

8.4 TreeMap

在這裏插入圖片描述

8.5 Map的其他類

IdentityHashMap和HashMap的具體區別,IdentityHashMap使用 == 判斷兩個key是否相等,而HashMap使用的是equals方法比較key值。
在這裏插入圖片描述

9. 小結

HashMap 非線程安全
HashMap:基於哈希表實現。使用HashMap要求添加的鍵類明確定義了hashCode()和equals()[可以重寫hashCode()和equals()],爲了優化HashMap空間的使用,您可以調優初始容量和負載因子。
TreeMap:非線程安全基於紅黑樹實現。TreeMap沒有調優選項,因爲該樹總處於平衡狀態。
適用場景分析:
HashMap和HashTable:HashMap去掉了HashTable的contains方法,但是加上了containsValue()和containsKey()方法。HashTable同步的,而HashMap是非同步的,效率上比HashTable要高。HashMap允許空鍵值,而HashTable不允許。

HashMap:適用於Map中插入、刪除和定位元素。
Treemap:適用於按自然順序或自定義順序遍歷鍵(key)。

線程安全類集合:Vector,HashTable
非線程安全集合類:LinkedList,ArrayList,HashSet,HashMap
根據數據結構分類
ArrayXxx:底層數據結構是數組,查詢快,增刪慢
LinkedXxx:底層數據結構是鏈表,查詢慢,增刪快
HashXxx:底層數據結構是哈希表。依賴兩個方法:hashCode()和equals()
TreeXxx:底層數據結構是二叉樹。兩種方式排序:自然排序和比較器排序

參考:
java集合超詳解
《Java編程思想》
《Java核心技術卷一》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章