Java 集合框架

集合結構圖

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
Tags: