Java 集合框架

atroot發表於2022-01-12

集合結構圖

image
Java 中集合又叫做容器,主要分為兩大陣營:Collection、Map;前者用來存放單一元素,後者用來存放鍵值對;

  1. Collection 主要包含兩個集合類(1. List,List 集合的特點是,元素是有序的,可重複的) (2. Set 元素是無序的,不可重複的集合)
  2. Map(對映)雙列資料,儲存對映關係的集合 也就是傳說中的 key-value 鍵值對兒

Collection 的主要實現介面有 List、Set、Queue;

List、Set、Map、Queue四者的區別

  • List 儲存元素是有序的,可重複的!可以存放多個空值!
  • Set 儲存的元素是無序的,不可重複的。可以存放一個 null 值,TreeSet 涉及到排序所以不可以存放 null;
  • Queue 按照特定的排隊規則進行排序,儲存的元素是有序的、可重複的;
  • Map 是用來儲存鍵值對的,只能存放一個 null 值(因為其他的會被覆蓋掉);HashTable、TreeMap 依然不可以存放空值;

Collection 與陣列的區別

陣列的特點

  • 陣列一旦初始化就無法進行擴充,無法改變其陣列長度和資料型別;
  • 陣列提供給我們的處理方法也是很少的,對於插入刪除等操作還是非常的麻煩的;
  • 陣列中儲存資料的特點是:有序性、可重複;

Collection 集合介面中常用的方法

@Test
public void test1() {
    // 1. add(Object obj); 新增一個元素  注意 在Collection介面的實現類物件中
    // 新增obj時,要求obj所在的類必須重寫equals()方法
    java.util.Collection coll = new ArrayList();
    coll.add(132);
    System.out.println(coll);  // [132]

    // 2. addAll(Collection coll); 新增一個Collection的集合的全部元素到現有的集合中
    java.util.Collection coll1 = new ArrayList();
    coll1.add(555);
    coll1.addAll(coll);
    System.out.println(coll1); // [555, 132]

    // 3. int size(); 返回集合中元素的個數
    System.out.println(coll.size());  // 1
    System.out.println(coll1.size()); // 2

    // 4. void clear(); 清空集合
    coll1.clear();
    System.out.println(coll1.size()); // 0

    // 5. boolean isEmpty(); 判斷是否為空集合
    System.out.println(coll.isEmpty());  //false
    System.out.println(coll1.isEmpty()); //true

    // 6. boolean contains(Object obj); 底層是通過元素呼叫equals()方法,
    // 判斷是否為同一個物件
    // 注意,在用contains()方法檢視集合中是否有該物件時,
    // 會直接呼叫該物件所在類的equals()方法

    // 此處用String時因為String類中對equals()類進行了重寫
    coll.add(new String("hello"));
    boolean hello = coll.contains(new String("hello"));
    System.out.println(hello);  //true

    // 7. boolean containsAll(Collection coll);
    // 底層是通過呼叫equals()方法,對集合裡的元素挨個比較 
    // 判斷形參coll中的所有元素是否都在當前集合中
    coll1.add(555);
    coll.add(555);
    coll1.add(new String("hello"));
    boolean b = coll.containsAll(coll1);
    System.out.println(coll);  // [132, hello, 555]
    System.out.println(coll1);  // [555, hello]
    System.out.println(b);  // true

    // 7. boolean remove(Object obj); 通過元素的equals()方法判斷
    // 是否是要刪除的那個元素,只會刪除找到的第一個元素
    coll.remove(555);
    System.out.println(coll); // [132, hello]

    // 8. boolean removeAll(Collection coll);取當前集合與形參裡集合的差集
    coll.removeAll(coll1);
    System.out.println(coll); // [132]

    // 9. retainAll(Collection coll);獲取當前集合物件和形參裡集合的交集,
    // 並返回給呼叫者
    java.util.Collection coll2 = Arrays.asList(132, 563);
    coll.retainAll(coll2);
    System.out.println(coll); // [132]
    // 10. equals(Object obj); 比較兩個集合裡的元素是否一樣 
    // 這裡要注意區分 集合是否有序

    // 11. hashcode(); 返回當前物件的雜湊值

    // 12. toArray(); 集合-->陣列
    Object[] objects = coll2.toArray();
    System.out.println(Arrays.toString(objects));

    // 13. 陣列-->集合 Arrays.asList();
    List list = Arrays.asList(45, 66, 48, "hello world");
    System.out.println(list); //[45, 66, 48, hello world]
    System.out.println("長度為:" + list.size());//長度為:4

    List<String> list1 = Arrays.asList(new String[]{"hello java","你好"});
    System.out.println(list1); //[hello java, 你好]
    System.out.println("長度為:" + list1.size()); //長度為:2

    List list3 = Arrays.asList(new int[]{1, 3, 5});
    System.out.println("長度為:" + list3.size()); //長度為:1

    List list4 = Arrays.asList(new Integer[]{154, 15, 33, 46});
    System.out.println(list4);  //[154, 15, 33, 46]
    System.out.println("長度為:" + list4.size()); //長度為:4
}

遍歷集合

Iterator 遍歷集合,用於遍歷 Collection 集合
注意:集合物件每次呼叫 iterator() 都會得到一個全新的迭代器;

方式一

通過for迴圈遍歷 不推薦使用

for (int i = 0; i < list4.size(); i++) {
    System.out.println("第"+ (i+1) +"個元素"+ iterator.next());
}

方式二

通過配合迭代器的 hasNext() 方法使用 while 迴圈進行遍歷

hasNext() 判斷下一個位置是否有元素

while (iterator.hasNext()) {
    // next(),將指標下移,並輸返回下移後位置上的元素
    System.out.println(iterator.next());
}

Collection 的子介面 List

List 正如我們上面提到的,元素是有序的,可重複的,底層是陣列。但是也是需要重寫 equals( ) 方法的,因為判斷元素是否存在的時候是需要的;

List介面主要有三個實現類

概述

  1. ArrayList 作為一個 List 介面的主要實現類而存在,也是我們平時用的比較多的一個集合類;
  2. LinkedList 雙向連結串列(1.6包括之前為迴圈連結串列),具體的 LinkedList 我們下面會做具體分析;
  3. Vector 是作為 List 介面的一個古老的實現類而存在的;

差異

相同點:都實現了 List 介面,切都遵循 List 的特點,元素有序,且可重複;

不同點:

  1. ArrayList 作為 List 介面的主要實現類,是執行緒不安全的,但是效率極高(底層使用Object {} elementData 儲存資料);
  2. LinkedList 對於頻繁的插入刪除操作來說,比 ArrayList 的效率高得多,因為其底層實現是雙向連結串列;
  3. Vector 作為一個古老的實現類而存在,執行緒安全但效率底下,底層視同 Object {} elementData 儲存資料;

對於 ArrayList 的原始碼簡略分析

基於 jdk1.7 ArrayList list = new ArrayList(); 底層建立了長度為 10 的 Object [ ] 陣列 ElementData,然後直接對建立好的陣列進行賦值,如果不夠則進行擴容(預設情況下擴容為原來的 1.5 倍),同時將原來的陣列複製到新的陣列當中;

基於 jdk1.8 ArrayList list = new ArrayList(); 底層陣列進行了初始化,Object [ ] elementData 初始化為{ } ,並沒有建立長度,當第一次呼叫 add();新增元素時,底層才進行了建立長度為 10 的陣列,並將資料新增到陣列當中.後面的操作則與 jdk1.7 無異
jdk1.7 中的 ArrayList 的物件的建立類似於單例的餓漢式,而 jdk1.8 中的 ArrayList 的物件的建立類似於單例模式的懶漢式,延遲了陣列的建立,節省記憶體;
結論:建議使用帶有引數的構造器:ArrayList list = new ArrayList(int capacity);直接初始化容量;

對 LinkedList 的原始碼分析

LinkedList list = new LinkedList(); 內部宣告瞭 Node 型別的 first 和 last 屬性,預設值為 null,list.add(); 將物件封裝到 Node 中,建立了 Node 物件
Node 的定義體現了 LinkedList 的雙向連結串列的說法

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;
    }
}

List 介面特有的常用方法

總結:常用方法
 	// 增
    add(Object obj);
    // 刪
    remove(int index);
    remove(Object obj));
    // 改
    set(int index,Object ele);
    // 查
    get(int index)
    // 插
    add(int index,Object ele)  
    // 長度
    size()  
    // 遍歷
    Iterator // 迭代器

增加元素

@Test
public void test() {
    ArrayList list = new ArrayList();
    list.add(123);
    list.add(556);
    list.add("hello");
    list.add(new person("小紅", 23, "女"));
    System.out.println(list);
    //[123, 556, hello, person{name='小紅', age=23, sex='女'}]
}

在指定索引位置插入 ele 元素 add(int index,ele);

@Test
public void test1() {
    ArrayList list = new ArrayList();
    list.add(123);
    list.add(556);
    list.add("hello");
    list.add(new person("小紅", 23, "女"));
    System.out.println(list);// [123, 556, hello, person{name='小紅', age=23, sex='女'}]
    list.add(2, new person("小黃", 22, "男"));
    System.out.println(list);
    // [123, 556, person{name='小黃', age=22, sex='男'}, hello, person{name='小紅', age=23, sex='女'}]
}

從指定索引位置開始將 eles 中的所有元素都新增進來

@Test
public void test2() {
    ArrayList list = new ArrayList();
    ArrayList list1 = new ArrayList();
    list.add(123);
    list.add(556);
    list.add("hello");
    list.add(new person("小紅", 23, "女"));
    list1.add(45);
    list1.add(36);
    list1.add(86);
    System.out.println(list1); // [45, 36, 86]
    list1.addAll(2, list);
    System.out.println(list1);// [45, 36, 123, 556, hello, person{name='小紅', age=23, sex='女'}, 86]
}

取指定索引位置的元素 get(int index)

@Test
public void test3() {
    ArrayList list = new ArrayList();
    list.add(123);
    list.add(556);
    list.add("hello");
    list.add(new person("小紅", 23, "女"));
    System.out.println(list);// [123, 556, hello, person{name='小紅', age=23, sex='女'}]
    System.out.println(list.get(3)); // person{name='小紅', age=23, sex='女'}
}

返回 obj 在集合中首次出現的位置 int indexOf(Object obj); 返回 obj 在當前集合首次出現的位置

@Test
public void test4() {
    ArrayList list = new ArrayList();
    list.add(123);
    list.add(556);
    list.add("hello");
    System.out.println(list.indexOf(556)); // 1
}

返回 obj 在集合中最後一次出現的位置

@Test
public void test5() {
    ArrayList list = new ArrayList();
    list.add(123);
    list.add(123);
    list.add(556);
    list.add(556);
    list.add(123);
    list.add("hello");
    System.out.println(list.lastIndexOf(123)); // 4
}

移除指定索引位置的元素,並返回該元素 Object remove(int index);

@Test
public void test6() {
    ArrayList list = new ArrayList();
    list.add(123);
    list.add(123);
    list.add(556);
    System.out.println("移除前:" + list); // 移除前:[123, 123, 556]
    Object remove = list.remove(2);
    System.out.println("移除的值為:" + remove); // 移除的值為:556
    System.out.println("移除後:" + list); // 移除後:[123, 123]
}

指定索引位置的元素設定為 ele

@Test
public void test7() {
    ArrayList list = new ArrayList();
    list.add(123);
    list.add(123);
    list.add(556);
    System.out.println("設定前:" + list); // 設定前:[123, 123, 556]
    list.set(0, new person("小明", 23, "男"));
    System.out.println("設定後:" + list); // 設定後:[person{name='小明', age=23, sex='男'}, 123, 556]
}

返回指定區間的集合的子集合 List subList(int fromIndex,int toIndex);

@Test
public void test8() {
    ArrayList list = new ArrayList();
    list.add(123);
    list.add(556);
    list.add("hello");
    list.add(new person("小紅", 23, "女"));
    List list1 = list.subList(1, 3);
    System.out.println(list); //[123, 556, hello, person{name='小紅', age=23, sex='女'}]
    System.out.println(list1); //[556, hello]
}

Set介面

Set 介面是 Collection 的子介面,Set 介面沒有提供額外的方法,Set 儲存無序的、不可重複的資料

  • Set 介面中不允許包含相同的元素(無序且唯一),若強行新增會使得新增操作失敗
  • Set 判斷兩個物件是否相同只能呼叫equals();

所以,Set是嚴格的

要求:
  1. 在 Set 中新增的資料一定要重寫 equals()、hashCode();
  2. 重寫的 equals( )、hashCode( ),保證相同的物件的雜湊值是相同的,即 equals( ) 與 hashCode( ) 返回值都是true
無序性以及不可重複性的理解:

底層資料的儲存依然是以陣列的形式進行儲存,但是無序不等於隨機,當我們新增陣列的時候,並不是按照陣列的索引進行新增,而是根據雜湊值進行新增。

存資料的過程
  • 如果計算的雜湊值不同,則表明資料不一樣,直接新增成功,
  • 如果計算的雜湊值相同,那麼就會呼叫其equals();進行比較,如果經過equals();比較後返回的值不是true那麼證明不一樣,新增成功。
  • 如果雜湊值相同,equals返回為false,那麼就會在對應的雜湊值的位置以連結串列的方式新增資料。以連結串列的方式新增資料的

針對上述的第三種情況又有:

規則,新的 hash 值相同的元素放在同一個位置的陣列裡,其順序在jdk7.0/8.0中有些許不同

  • 基於JDK7.0:新的元素放到陣列中,並指向原來的舊元素
  • 基於JDK8.0:原來的元素在陣列中,指向新的元素

總結:七上八下(指的是 新元素的存放位置,七、新的元素放在原來的陣列的位置(上邊),舊的元素向下移動,在連結串列中,新的元素指向舊的元素; 八、新的元素放在連結串列中,在連結串列中,舊的指向新的)

Set介面有三個實現類:
  1. HashSet 基於 HashMap(底層是 陣列 + 連結串列) 實現的,底層使用 HashMap 來儲存元素,作為 Set 介面的主要實現類,執行緒不安全;
  2. LinkedHashSet 作為 HashSet 的子類,遍歷其中的元素,可以按照新增順序來遍歷,對於頻繁的遍歷操作,效率高於 HashSet,是因為在 HashSet 的基礎上在陣列給每個元素都加上了指標,使資料變成雙向連結串列。
  3. TreeSet 可以按照物件的指定屬性進行排序,要求新增的資料是相同類的物件
Set的實現類 HashSet 的實現以及練習
@Test
public void test1() {
    Set set = new HashSet();
    set.add("hello");
    set.add(123);
    set.add("abc");
    set.add(123);
    set.add(new person("小紅", 29, "男"));
    set.add(new person("小紅", 29, "男"));
    Iterator iterator = set.iterator();

    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

輸出結果

abc
person{name='小紅', age=29, sex='男'}
hello
123

LinkedHashSet 是 HashSet 的子類,根據新增元素的順序來遍歷集合

@Test
public void test2() {
    Set set = new LinkedHashSet();
    set.add("hello");
    set.add(123);
    set.add("abc");
    set.add(123);
    set.add(new person("小紅", 29, "男"));
    set.add(new person("小紅", 29, "男"));
    Iterator iterator = set.iterator();

    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

輸出結果

hello
123
abc
person{name='小紅', age=29, sex='男'}
TreeSet,向 TreeSet 中新增資料,要求是相同類的物件,兩種排序方式:自然排序、定製排序

自然排序中,比較兩個物件是否相同的標準為:compareTo() 返回 0 ,而不再是 equals() 方法
定製排序中,比較兩個物件是否相同的標準是 compare() 但是規則是一樣的

@Test
public void test3() {
    //編寫比較規則
    Comparator comparator = new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            if (o1 instanceof person && o2 instanceof person) {
                person p1 = (person) o1;
                person p2 = (person) o2;
                return p1.getName().compareTo(p2.getName());
            } else {
                throw new RuntimeException("資料異常!");
            }
        }
    };
    //應用比價規則
    TreeSet treeSet = new TreeSet(comparator);  //在有引數的情況下,會根據引數物件中所定義的排序方式進行排序,
    // 若沒有,將會按照新增的物件中實現的comparable介面後重寫的compareTo();的規則進行排序
    treeSet.add(new person("孔乙己", 33, "女"));
    treeSet.add(new person("祥林嫂", 22, "男"));
    treeSet.add(new person("魯迅", 18, "女"));


    Iterator iterator = treeSet.iterator();//通過age進行自然排序
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

輸出結果:

person{name='孔乙己', age=33, sex='女'}
person{name='祥林嫂', age=22, sex='男'}
person{name='魯迅', age=18, sex='女'}

Map

Map,並列於 Collection 介面,用於儲存雙列資料(鍵值對 key - value)

  • HashMap 作為 Map 的主要實現類存在,與 ArrayList 的存在地位相似,執行緒不安全、但是效率高,可以儲存一個 null 的 key - value,key 所在的類要重寫equals( ) 和 hashCode( );
  • LinkedHashMap,HashMap 的子類、Map 的實現類,在 HashMap 的底層基礎上,新增了指標,構成連結串列,對於頻繁的遍歷操作,執行效率高於 HashMap
  • TreeMap 按照新增的 key-value 對進行排序,實現排序遍歷,底層的實現是紅黑樹;
  • Hashtable 作為古老的實現類,執行緒安全、效率低下,不能夠儲存空的 key-value;
  • Properties,常用來處理配置檔案,key-value 都是 String 型別;

HashMap 的底層:

陣列+連結串列(jdk7及以前)
陣列+連結串列+紅黑樹(jdk8+)

對於 Map 結構的理解:
  1. Map 中的 key:無序的、不可重複的、使用 Set 儲存所有的 key ;
  2. Map 中的 value:無序的、可重複的,使用 Collection 儲存所有的 value,value 所在的類要重寫 equals( );
  3. 一個鍵值對:key-value 構成一個 Entry 物件;
  4. Map 中的 entry:無序的、不可重複的,使用 Set 儲存所有的 entry;

對 Map 底層原理的理解

JDK7為例說明:

HashMap map = new HashMap(); 在例項化之後,底層建立了一個長度為 16 的一維陣列 Entry [ ] table。

當我們往 HashMap 中新增資料的時候 map.put(ket1,value1);

  1. 首先呼叫 key1 所在類的 hashCode() 計算雜湊值,若雜湊值所對應的 Entry 陣列的位置上的資料為空,那麼此時的(key1,value)新增成功;
    • 如果雜湊值對應的 Entry 陣列對應的位置上不為空(意味著此位置上存在一個或多個資料---以連結串列的形式存在),繼續通過equals()與其他已經存在的值進行比較,如果返回 false,那麼新增成功,如果返回值為 true,那麼會用新的的 value 替換舊的;
  • 以上雜湊值相同的情況均以連結串列的方式進行儲存(遵循七上八下)
  • 在不斷地新增過程中會涉及擴容問題,當 size 超出臨界值且要存放的位置非空,擴容為原來的兩倍,並將原來的資料複製過來;

JDK8 在底層與JDK7的不同之處

  1. new HashMap();底層沒有建立一個長度為16的陣列
  2. JDK8 底層的陣列是 Node [ ],而非 Entry[ ];
  3. 首次使用 put(),的時候,底層建立長度為 16 的陣列
  4. jdk7 底層結構只有:陣列+連結串列。而 JDK8 底層結構為 陣列+連結串列+紅黑樹
  5. 當陣列的某一個索引位置上的元素以連結串列的形式存在的資料個數 > 8,且當前陣列的長度>64時,此時此索引位置上的所有資料改為紅黑樹儲存

底層主要關鍵字

  • DEFAULT_INITIAL_CAPACITY (初始預設容量):HashMap的預設容量:16
  • DEFAULT_LOAD_FACTOR(HashMap的預設載入因子):0.75
  • threshold(擴容的臨界值):容量*擴容因子:16 X 0.75 = 12
  • TREEIFY_THRESHOLD_THRESHOLD:Bucket中連結串列長度大於該預設值,就轉化為紅黑樹:8
  • MIN_TREEIFY_CAPACITY: 桶中的Node被樹化時最小的hash表容量:64

相關文章