Java集合詳解8:Java集合類細節精講

今天咱們來探索一下Java集合類中的一些技術細節。主要是對一些比較容易被遺漏和誤解的知識點作一些講解和補充。可能不全面,還請諒解。java

本文參考:http://cmsblogs.com/?cat=5node

具體代碼在個人GitHub中能夠找到git

https://github.com/h2pl/MyTech程序員

初始容量

集合是咱們在Java編程中使用很是普遍的,它就像大海,海納百川,像萬能容器,盛裝萬物,並且這個大海,萬能容器還能夠無限變大(若是條件容許)。當這個海、容器的量變得很是大的時候,它的初始容量就會顯得很重要了,由於挖海、擴容是須要消耗大量的人力物力財力的。github

一樣的道理,Collection的初始容量也顯得異常重要。因此:對於已知的情景,請爲集合指定初始容量。編程

public static void main(String[] args) {
    StudentVO student = null;
    long begin1 = System.currentTimeMillis();
    List<StudentVO> list1 = new ArrayList<>();
    for(int i = 0 ; i < 1000000; i++){
        student = new StudentVO(i,"chenssy_"+i,i);
        list1.add(student);
    }
    long end1 = System.currentTimeMillis();
    System.out.println("list1 time:" + (end1 - begin1));
    
    long begin2 = System.currentTimeMillis();
    List<StudentVO> list2 = new ArrayList<>(1000000);
    for(int i = 0 ; i < 1000000; i++){
        student = new StudentVO(i,"chenssy_"+i,i);
        list2.add(student);
    }
    long end2 = System.currentTimeMillis();
    System.out.println("list2 time:" + (end2 - begin2));
}

上面代碼兩個list都是插入1000000條數據,只不過list1沒有沒有申請初始化容量,而list2初始化容量1000000。那運行結果以下:後端

list1 time:1638
list2 time:921

從上面的運行結果咱們能夠看出list2的速度是list1的兩倍左右。在前面LZ就提過,ArrayList的擴容機制是比較消耗資源的。咱們先看ArrayList的add方法:數組

public boolean add(E e) {  
        ensureCapacity(size + 1);   
        elementData[size++] = e;  
        return true;  
    }  

public void ensureCapacity(int minCapacity) {  
    modCount++;         //修改計數器
    int oldCapacity = elementData.length;    
    //當前須要的長度超過了數組長度,進行擴容處理
    if (minCapacity > oldCapacity) {  
        Object oldData[] = elementData;  
        //新的容量 = 舊容量 * 1.5 + 1
        int newCapacity = (oldCapacity * 3)/2 + 1;  
            if (newCapacity < minCapacity)  
                newCapacity = minCapacity;  
      //數組拷貝,生成新的數組 
      elementData = Arrays.copyOf(elementData, newCapacity);  
    }  
}

ArrayList每次新增一個元素,就會檢測ArrayList的當前容量是否已經到達臨界點,若是到達臨界點則會擴容1.5倍。然而ArrayList的擴容以及數組的拷貝生成新的數組是至關耗資源的。因此若咱們事先已知集合的使用場景,知道集合的大概範圍,咱們最好是指定初始化容量,這樣對資源的利用會更加好,尤爲是大數據量的前提下,效率的提高和資源的利用會顯得更加具備優點。微信

asList的缺陷

在實際開發過程當中咱們常用asList講數組轉換爲List,這個方法使用起來很是方便,可是asList方法存在幾個缺陷:網絡

避免使用基本數據類型數組轉換爲列表

使用8個基本類型數組轉換爲列表時會存在一個比較有味的缺陷。先看以下程序:

public static void main(String[] args) {
        int[] ints = {1,2,3,4,5};
        List list = Arrays.asList(ints);
        System.out.println("list'size:" + list.size());
    }
------------------------------------
outPut:
list'size:1

程序的運行結果並無像咱們預期的那樣是5而是逆天的1,這是什麼狀況?先看源碼:

public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

asList接受的參數是一個泛型的變長參數,咱們知道基本數據類型是沒法髮型化的,也就是說8個基本類型是沒法做爲asList的參數的, 要想做爲泛型參數就必須使用其所對應的包裝類型。可是這個這個實例中爲何沒有出錯呢?

由於該實例是將int類型的數組當作其參數,而在Java中數組是一個對象,它是能夠泛型化的。因此該例子是不會產生錯誤的。既然例子是將整個int類型的數組當作泛型參數,那麼通過asList轉換就只有一個int 的列表了。以下:

public static void main(String[] args) {
    int[] ints = {1,2,3,4,5};
    List list = Arrays.asList(ints);
    System.out.println("list 的類型:" + list.get(0).getClass());
    System.out.println("list.get(0) == ints:" + list.get(0).equals(ints));
}

outPut:
list 的類型:class [I
list.get(0) == ints:true
從這個運行結果咱們能夠充分證實list裏面的元素就是int數組。弄清楚這點了,那麼修改方法也就一目瞭然了:將int 改變爲Integer。

public static void main(String[] args) {
        Integer[] ints = {1,2,3,4,5};
        List list = Arrays.asList(ints);
        System.out.println("list'size:" + list.size());
        System.out.println("list.get(0) 的類型:" + list.get(0).getClass());
        System.out.println("list.get(0) == ints[0]:" + list.get(0).equals(ints[0]));
    }
----------------------------------------
outPut:
list'size:5
list.get(0) 的類型:class java.lang.Integer
list.get(0) == ints[0]:true

asList產生的列表不可操做

對於上面的實例咱們再作一個小小的修改:

public static void main(String[] args) {
        Integer[] ints = {1,2,3,4,5};
        List list = Arrays.asList(ints);
        list.add(6);
    }

該實例就是講ints經過asList轉換爲list 類別,而後再經過add方法加一個元素,這個實例簡單的不能再簡單了,可是運行結果呢?打出咱們所料:

Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.AbstractList.add(Unknown Source)
    at java.util.AbstractList.add(Unknown Source)
    at com.chenssy.test.arrayList.AsListTest.main(AsListTest.java:10)

運行結果盡然拋出UnsupportedOperationException異常,該異常表示list不支持add方法。這就讓咱們鬱悶了,list怎麼可能不支持add方法呢?難道jdk腦殼堵塞了?咱們再看asList的源碼:

public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

asList接受參數後,直接new 一個ArrayList,到這裏看應該是沒有錯誤的啊?別急,再往下看:

private static class ArrayList<E> extends AbstractList<E>
    implements RandomAccess, java.io.Serializable{
        private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;

        ArrayList(E[] array) {
            if (array==null)
                throw new NullPointerException();
            a = array;
        }
        //.................
    }

這是ArrayList的源碼,從這裏咱們能夠看出,此ArrayList不是java.util.ArrayList,他是Arrays的內部類。

該內部類提供了size、toArray、get、set、indexOf、contains方法,而像add、remove等改變list結果的方法從AbstractList父類繼承過來,同時這些方法也比較奇葩,它直接拋出UnsupportedOperationException異常:

public boolean add(E e) {
        add(size(), e);
        return true;
    }
    
    public E set(int index, E element) {
        throw new UnsupportedOperationException();
    }
    
    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
    
    public E remove(int index) {
        throw new UnsupportedOperationException();
    }

經過這些代碼能夠看出asList返回的列表只不過是一個披着list的外衣,它並無list的基本特性(變長)。該list是一個長度不可變的列表,傳入參數的數組有多長,其返回的列表就只能是多長。因此::不要試圖改變asList返回的列表,不然你會自食苦果。

subList的缺陷

咱們常用subString方法來對String對象進行分割處理,同時咱們也可使用subList、subMap、subSet來對List、Map、Set進行分割處理,可是這個分割存在某些瑕疵。

subList返回僅僅只是一個視圖

首先咱們先看以下實例:

public static void main(String[] args) {

List<Integer> list1 = new ArrayList<Integer>();
    list1.add(1);
    list1.add(2);
    
    //經過構造函數新建一個包含list1的列表 list2
    List<Integer> list2 = new ArrayList<Integer>(list1);
    
    //經過subList生成一個與list1同樣的列表 list3
    List<Integer> list3 = list1.subList(0, list1.size());
    
    //修改list3
    list3.add(3);
    
    System.out.println("list1 == list2:" + list1.equals(list2));
    System.out.println("list1 == list3:" + list1.equals(list3));
}

這個例子很是簡單,無非就是經過構造函數、subList從新生成一個與list1同樣的list,而後修改list3,最後比較list1 == list2?、list1 == list3?。

按照咱們常規的思路應該是這樣的:由於list3經過add新增了一個元素,那麼它確定與list1不等,而list2是經過list1構造出來的,因此應該相等,因此結果應該是:

list1 == list2:true
list1 == list3: false

首先咱們先不論結果的正確與否,咱們先看subList的源碼:

public List<E> subList(int fromIndex, int toIndex) {
        subListRangeCheck(fromIndex, toIndex, size);
        return new SubList(this, 0, fromIndex, toIndex);
}

subListRangeCheck方式是判斷fromIndex、toIndex是否合法,若是合法就直接返回一個subList對象,注意在產生該new該對象的時候傳遞了一個參數 this ,該參數很是重要,由於他表明着原始list。

/**

* 繼承AbstractList類,實現RandomAccess接口
 */
private class SubList extends AbstractList<E> implements RandomAccess {
    private final AbstractList<E> parent;    //列表
    private final int parentOffset;   
    private final int offset;
    int size;

    //構造函數
    SubList(AbstractList<E> parent,
            int offset, int fromIndex, int toIndex) {
        this.parent = parent;
        this.parentOffset = fromIndex;
        this.offset = offset + fromIndex;
        this.size = toIndex - fromIndex;
        this.modCount = ArrayList.this.modCount;
    }

    //set方法
    public E set(int index, E e) {
        rangeCheck(index);
        checkForComodification();
        E oldValue = ArrayList.this.elementData(offset + index);
        ArrayList.this.elementData[offset + index] = e;
        return oldValue;
    }

    //get方法
    public E get(int index) {
        rangeCheck(index);
        checkForComodification();
        return ArrayList.this.elementData(offset + index);
    }

    //add方法
    public void add(int index, E e) {
        rangeCheckForAdd(index);
        checkForComodification();
        parent.add(parentOffset + index, e);
        this.modCount = parent.modCount;
        this.size++;
    }

    //remove方法
    public E remove(int index) {
        rangeCheck(index);
        checkForComodification();
        E result = parent.remove(parentOffset + index);
        this.modCount = parent.modCount;
        this.size--;
        return result;
    }
}

該SubLsit是ArrayList的內部類,它與ArrayList同樣,都是繼承AbstractList和實現RandomAccess接口。同時也提供了get、set、add、remove等list經常使用的方法。可是它的構造函數有點特殊,在該構造函數中有兩個地方須要注意:

一、this.parent = parent;而parent就是在前面傳遞過來的list,也就是說this.parent就是原始list的引用。

二、this.offset = offset + fromIndex;this.parentOffset = fromIndex;。同時在構造函數中它甚至將modCount(fail-fast機制)傳遞過來了。

咱們再看get方法,在get方法中return ArrayList.this.elementData(offset + index);

這段代碼能夠清晰代表get所返回就是原列表offset + index位置的元素。一樣的道理還有add方法裏面的:

parent.add(parentOffset + index, e);
this.modCount = parent.modCount;
remove方法裏面的

E result = parent.remove(parentOffset + index);
this.modCount = parent.modCount;

誠然,到了這裏咱們能夠判斷subList返回的SubList一樣也是AbstractList的子類,同時它的方法如get、set、add、remove等都是在原列表上面作操做,它並無像subString同樣生成一個新的對象。

因此subList返回的只是原列表的一個視圖,它全部的操做最終都會做用在原列表上。

那麼從這裏的分析咱們能夠得出上面的結果應該偏偏與咱們上面的答案相反:

list1 == list2:false
list1 == list3:true

subList生成子列表後,不要試圖去操做原列表

從上面咱們知道subList生成的子列表只是原列表的一個視圖而已,若是咱們操做子列表它產生的做用都會在原列表上面表現,可是若是咱們操做原列表會產生什麼狀況呢?

public static void main(String[] args) {

List<Integer> list1 = new ArrayList<Integer>();
    list1.add(1);
    list1.add(2);
    
    //經過subList生成一個與list1同樣的列表 list3
    List<Integer> list3 = list1.subList(0, list1.size());
    //修改list1
    list1.add(3);
    
    System.out.println("list1'size:" + list1.size());
    System.out.println("list3'size:" + list3.size());
}

該實例若是不產生意外,那麼他們兩個list的大小都應該都是3,可是恰恰事與願違,事實上咱們獲得的結果是這樣的:

list1'size:3
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$SubList.checkForComodification(Unknown Source)
    at java.util.ArrayList$SubList.size(Unknown Source)
    at com.chenssy.test.arrayList.SubListTest.main(SubListTest.java:17)

list1正常輸出,可是list3就拋出ConcurrentModificationException異常,看過我另外一篇博客的同仁確定對這個異常很是,fail-fast?不錯就是fail-fast機制,在fail-fast機制中,LZ花了不少力氣來說述這個異常,因此這裏LZ就不對這個異常多講了。咱們再看size方法:

public int size() {
            checkForComodification();
            return this.size;
        }

size方法首先會經過checkForComodification驗證,而後再返回this.size。

private void checkForComodification() {
            if (ArrayList.this.modCount != this.modCount)
                throw new ConcurrentModificationException();
        }

該方法代表當原列表的modCount與this.modCount不相等時就會拋出ConcurrentModificationException。

同時咱們知道modCount 在new的過程當中 "繼承"了原列表modCount,只有在修改該列表(子列表)時纔會修改該值(先表如今原列表後做用於子列表)。

而在該實例中咱們是操做原列表,原列表的modCount固然不會反應在子列表的modCount上啦,因此纔會拋出該異常。

對於子列表視圖,它是動態生成的,生成以後就不要操做原列表了,不然必然都致使視圖的不穩定而拋出異常。最好的辦法就是將原列表設置爲只讀狀態,要操做就操做子列表:

//經過subList生成一個與list1同樣的列表 list3

List<Integer> list3 = list1.subList(0, list1.size());

//對list1設置爲只讀狀態

list1 = Collections.unmodifiableList(list1);

推薦使用subList處理局部列表

在開發過程當中咱們必定會遇到這樣一個問題:獲取一堆數據後,須要刪除某段數據。例如,有一個列表存在1000條記錄,咱們須要刪除100-200位置處的數據,可能咱們會這樣處理:

for(int i = 0 ; i < list1.size() ; i++){
   if(i >= 100 && i <= 200){
       list1.remove(i);
       /*
        * 固然這段代碼存在問題,list remove以後後面的元素會填充上來,
         * 因此須要對i進行簡單的處理,固然這個不是這裏討論的問題。
         */
   }
}

這個應該是咱們大部分人的處理方式吧,其實還有更好的方法,利用subList。在前面LZ已經講過,子列表的操做都會反映在原列表上。因此下面一行代碼所有搞定:

list1.subList(100, 200).clear();

簡單而不失華麗!!!!!

保持compareTo和equals同步

在Java中咱們常使用Comparable接口來實現排序,其中compareTo是實現該接口方法。咱們知道compareTo返回0表示兩個對象相等,返回正數表示大於,返回負數表示小於。同時咱們也知道equals也能夠判斷兩個對象是否相等,那麼他們二者之間是否存在關聯關係呢?

public class Student implements Comparable<Student>{
    private String id;
    private String name;
    private int age;
    
    public Student(String id,String name,int age){
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public boolean equals(Object obj){
        if(obj == null){
            return false;
        }
        
        if(this == obj){
            return true;
        }
        
        if(obj.getClass() != this.getClass()){
            return false;
        }
        
        Student student = (Student)obj;
        if(!student.getName().equals(getName())){
            return false;
        }
        
        return true;
    }
    
    public int compareTo(Student student) {
        return this.age - student.age;
    }

    /** 省略getter、setter方法 */
}

Student類實現Comparable接口和實現equals方法,其中compareTo是根據age來比對的,equals是根據name來比對的。

public static void main(String[] args){
        List<Student> list = new ArrayList<>();
        list.add(new Student("1", "chenssy1", 24));
        list.add(new Student("2", "chenssy1", 26));
        
        Collections.sort(list);   //排序
        
        Student student = new Student("2", "chenssy1", 26);
        
        //檢索student在list中的位置
        int index1 = list.indexOf(student);
        int index2 = Collections.binarySearch(list, student);
        
        System.out.println("index1 = " + index1);
        System.out.println("index2 = " + index2);
    }

按照常規思路來講應該二者index是一致的,由於他們檢索的是同一個對象,可是很是遺憾,其運行結果:

index1 = 0
index2 = 1

爲何會產生這樣不一樣的結果呢?這是由於indexOf和binarySearch的實現機制不一樣。

indexOf是基於equals來實現的只要equals返回TRUE就認爲已經找到了相同的元素。

而binarySearch是基於compareTo方法的,當compareTo返回0 時就認爲已經找到了該元素。

在咱們實現的Student類中咱們覆寫了compareTo和equals方法,可是咱們的compareTo、equals的比較依據不一樣,一個是基於age、一個是基於name。

比較依據不一樣那麼獲得的結果頗有可能會不一樣。因此知道了緣由,咱們就好修改了:將二者之間的比較依據保持一致便可。

對於compareTo和equals兩個方法咱們能夠總結爲:compareTo是判斷元素在排序中的位置是否相等,equals是判斷元素是否相等,既然一個決定排序位置,一個決定相等,因此咱們很是有必要確保當排序位置相同時,其equals也應該相等。

使其相等的方式就是二者應該依附於相同的條件。當compareto相等時equals也應該相等,而compareto不相等時equals不該該相等,而且compareto依據某些屬性來決定排序。

今天咱們來探索一下HashSet,TreeSet與LinkedHashSet的基本原理與源碼實現,因爲這三個set都是基於以前文章的三個map進行實現的,因此推薦你們先看一下前面有關map的文章,結合使用味道更佳。

具體代碼在個人GitHub中能夠找到

https://github.com/h2pl/MyTech

文章首發於個人我的博客:

https://h2pl.github.io/2018/0...

更多關於Java後端學習的內容請到個人CSDN博客上查看:

https://blog.csdn.net/a724888

個人我的博客主要發原創文章,也歡迎瀏覽 https://h2pl.github.io/

本文參考 http://cmsblogs.com/?p=599

HashSet

定義
public class HashSet<E>

extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable

HashSet繼承AbstractSet類,實現Set、Cloneable、Serializable接口。其中AbstractSet提供 Set 接口的骨幹實現,從而最大限度地減小了實現此接口所需的工做。 ==Set接口是一種不包括重複元素的Collection,它維持它本身的內部排序,因此隨機訪問沒有任何意義。==

本文基於1.8jdk進行源碼分析。

基本屬性

基於HashMap實現,底層使用HashMap保存全部元素
private transient HashMap<E,Object> map;

//定義一個Object對象做爲HashMap的value
private static final Object PRESENT = new Object();

構造函數
/**

* 默認構造函數
 * 初始化一個空的HashMap,並使用默認初始容量爲16和加載因子0.75。
 */
public HashSet() {
    map = new HashMap<>();
}

/**
 * 構造一個包含指定 collection 中的元素的新 set。
 */
public HashSet(Collection<? extends E> c) {
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}

/**
 * 構造一個新的空 set,其底層 HashMap 實例具備指定的初始容量和指定的加載因子
 */
public HashSet(int initialCapacity, float loadFactor) {
    map = new HashMap<>(initialCapacity, loadFactor);
}

/**
 * 構造一個新的空 set,其底層 HashMap 實例具備指定的初始容量和默認的加載因子(0.75)。
 */
public HashSet(int initialCapacity) {
   map = new HashMap<>(initialCapacity);
}

/**
 * 在API中我沒有看到這個構造函數,今天看源碼才發現(原來訪問權限爲包權限,不對外公開的)
 * 以指定的initialCapacity和loadFactor構造一個新的空連接哈希集合。
 * dummy 爲標識 該構造函數主要做用是對LinkedHashSet起到一個支持做用
 */
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
   map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

從構造函數中能夠看出HashSet全部的構造都是構造出一個新的HashMap,其中最後一個構造函數,爲包訪問權限是不對外公開,僅僅只在使用LinkedHashSet時纔會發生做用。

方法

既然HashSet是基於HashMap,那麼對於HashSet而言,其方法的實現過程是很是簡單的。
public Iterator<E> iterator() {

return map.keySet().iterator();
}

iterator()方法返回對此 set 中元素進行迭代的迭代器。返回元素的順序並非特定的。

底層調用HashMap的keySet返回全部的key,這點反應了HashSet中的全部元素都是保存在HashMap的key中,value則是使用的PRESENT對象,該對象爲static final。
public int size() {

return map.size();
}

size()返回此 set 中的元素的數量(set 的容量)。底層調用HashMap的size方法,返回HashMap容器的大小。

public boolean isEmpty() {

return map.isEmpty();
}
isEmpty(),判斷HashSet()集合是否爲空,爲空返回 true,不然返回false。

public boolean contains(Object o) {

return map.containsKey(o);

}

public boolean containsKey(Object key) {

return getNode(hash(key), key) != null;

}

//最終調用該方法進行節點查找
final Node<K,V> getNode(int hash, Object key) {

Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//先檢查桶的頭結點是否存在
if ((tab = table) != null && (n = tab.length) > 0 &&
    (first = tab[(n - 1) & hash]) != null) {
    if (first.hash == hash && // always check first node
        ((k = first.key) == key || (key != null && key.equals(k))))
        return first;
        //不是頭結點,則遍歷鏈表,若是是樹節點則使用樹節點的方法遍歷,直到找到,或者爲null
    if ((e = first.next) != null) {
        if (first instanceof TreeNode)
            return ((TreeNode<K,V>)first).getTreeNode(hash, key);
        do {
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        } while ((e = e.next) != null);
    }
}
return null;

}

contains(),判斷某個元素是否存在於HashSet()中,存在返回true,不然返回false。更加確切的講應該是要知足這種關係才能返回true:(o==null ? e==null : o.equals(e))。底層調用containsKey判斷HashMap的key值是否爲空。
public boolean add(E e) {

return map.put(e, PRESENT)==null;

}

public V put(K key, V value) {

return putVal(hash(key), key, value, false, true);

}

map的put方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;

//確認初始化
if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
    
//若是桶爲空,直接插入新元素,也就是entry
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
else {
    Node<K,V> e; K k;
    //若是衝突,分爲三種狀況
    //key相等時讓舊entry等於新entry便可
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
        e = p;
    //紅黑樹狀況
    else if (p instanceof TreeNode)
        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
        //若是key不相等,則連成鏈表
        for (int binCount = 0; ; ++binCount) {
            if ((e = p.next) == null) {
                p.next = newNode(hash, key, value, null);
                if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    treeifyBin(tab, hash);
                break;
            }
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                break;
            p = e;
        }
    }
    if (e != null) { // existing mapping for key
        V oldValue = e.value;
        if (!onlyIfAbsent || oldValue == null)
            e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
}
++modCount;
if (++size > threshold)
    resize();
afterNodeInsertion(evict);
return null;

}

這裏注意一點,hashset只是不容許重複的元素加入,而不是不容許元素連成鏈表,由於只要key的equals方法判斷爲true時它們是相等的,此時會發生value的替換,由於全部entry的value同樣,因此和沒有插入時同樣的。

而當兩個hashcode相同但key不相等的entry插入時,仍然會連成一個鏈表,長度超過8時依然會和hashmap同樣擴展成紅黑樹,看完源碼以後筆者才明白本身以前理解錯了。因此看源碼仍是蠻有好處的。hashset基本上就是使用hashmap的方法再次實現了一遍而已,只不過value全都是同一個object,讓你覺得相同元素沒有插入,事實上只是value替換成和原來相同的值而已。

當add方法發生衝突時,若是key相同,則替換value,若是key不一樣,則連成鏈表。

add()若是此 set 中還沒有包含指定元素,則添加指定元素。若是此Set沒有包含知足(e==null ? e2==null : e.equals(e2)) 的e2時,則將e2添加到Set中,不然不添加且返回false。

因爲底層使用HashMap的put方法將key = e,value=PRESENT構建成key-value鍵值對,當此e存在於HashMap的key中,則value將會覆蓋原有value,可是key保持不變,因此若是將一個已經存在的e元素添加中HashSet中,新添加的元素是不會保存到HashMap中,因此這就知足了HashSet中元素不會重複的特性。
public boolean remove(Object o) {

return map.remove(o)==PRESENT;

}

remove若是指定元素存在於此 set 中,則將其移除。底層使用HashMap的remove方法刪除指定的Entry。
public void clear() {

map.clear();

}

clear今後 set 中移除全部元素。底層調用HashMap的clear方法清除全部的Entry。
public Object clone() {

try {
        HashSet<E> newSet = (HashSet<E>) super.clone();
        newSet.map = (HashMap<E, Object>) map.clone();
        return newSet;
    } catch (CloneNotSupportedException e) {
        throw new InternalError();
    }
}

clone返回此 HashSet 實例的淺表副本:並無複製這些元素自己。

後記:

因爲HashSet底層使用了HashMap實現,使其的實現過程變得很是簡單,若是你對HashMap比較瞭解,那麼HashSet簡直是小菜一碟。有兩個方法對HashMap和HashSet而言是很是重要的,下篇將詳細講解hashcode和equals。

TreeSet

與HashSet是基於HashMap實現同樣,TreeSet一樣是基於TreeMap實現的。在《Java提升篇(二七)-----TreeMap》中LZ詳細講解了TreeMap實現機制,若是客官詳情看了這篇博文或者多TreeMap有比較詳細的瞭解,那麼TreeSet的實現對您是喝口水那麼簡單。

TreeSet定義

咱們知道TreeMap是一個有序的二叉樹,那麼同理TreeSet一樣也是一個有序的,它的做用是提供有序的Set集合。經過源碼咱們知道TreeSet基礎AbstractSet,實現NavigableSet、Cloneable、Serializable接口。

其中AbstractSet提供 Set 接口的骨幹實現,從而最大限度地減小了實現此接口所需的工做。

NavigableSet是擴展的 SortedSet,具備了爲給定搜索目標報告最接近匹配項的導航方法,這就意味着它支持一系列的導航方法。好比查找與指定目標最匹配項。Cloneable支持克隆,Serializable支持序列化。
public class TreeSet<E> extends AbstractSet<E>

implements NavigableSet<E>, Cloneable, java.io.Serializable

同時在TreeSet中定義了以下幾個變量。
private transient NavigableMap<E,Object> m;

//PRESENT會被當作Map的value與key構建成鍵值對
private static final Object PRESENT = new Object();

其構造方法:
//默認構造方法,根據其元素的天然順序進行排序

public TreeSet() {

this(new TreeMap<E,Object>());

}

//構造一個包含指定 collection 元素的新 TreeSet,它按照其元素的天然順序進行排序。
public TreeSet(Comparator<? super E> comparator) {

this(new TreeMap<>(comparator));

}

//構造一個新的空 TreeSet,它根據指定比較器進行排序。
public TreeSet(Collection<? extends E> c) {

this();
addAll(c);

}

//構造一個與指定有序 set 具備相同映射關係和相同排序的新 TreeSet。
public TreeSet(SortedSet<E> s) {

this(s.comparator());
addAll(s);

}

TreeSet(NavigableMap<E,Object> m) {

this.m = m;

}

2、TreeSet主要方法

一、add:將指定的元素添加到此 set(若是該元素還沒有存在於 set 中)。
public boolean add(E e) {

return m.put(e, PRESENT)==null;
}

public V put(K key, V value) {

Entry<K,V> t = root;
if (t == null) {
//空樹時,判斷節點是否爲空
    compare(key, key); // type (and possibly null) check

    root = new Entry<>(key, value, null);
    size = 1;
    modCount++;
    return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
//非空樹,根據傳入比較器進行節點的插入位置查找
if (cpr != null) {
    do {
        parent = t;
        //節點比根節點小,則找左子樹,不然找右子樹
        cmp = cpr.compare(key, t.key);
        if (cmp < 0)
            t = t.left;
        else if (cmp > 0)
            t = t.right;
            //若是key的比較返回值相等,直接更新值(通常compareto相等時equals方法也相等)
        else
            return t.setValue(value);
    } while (t != null);
}
else {
//若是沒有傳入比較器,則按照天然排序
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
    do {
        parent = t;
        cmp = k.compareTo(t.key);
        if (cmp < 0)
            t = t.left;
        else if (cmp > 0)
            t = t.right;
        else
            return t.setValue(value);
    } while (t != null);
}
//查找的節點爲空,直接插入,默認爲紅節點
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
    parent.left = e;
else
    parent.right = e;
    //插入後進行紅黑樹調整
fixAfterInsertion(e);
size++;
modCount++;
return null;

}

二、get:獲取元素
public V get(Object key) {

Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);

}

該方法與put的流程相似,只不過是把插入換成了查找

三、ceiling:返回此 set 中大於等於給定元素的最小元素;若是不存在這樣的元素,則返回 null。
public E ceiling(E e) {

return m.ceilingKey(e);
}

四、clear:移除此 set 中的全部元素。
public void clear() {

m.clear();
}

五、clone:返回 TreeSet 實例的淺表副本。屬於淺拷貝。
public Object clone() {

TreeSet<E> clone = null;
    try {
        clone = (TreeSet<E>) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new InternalError();
    }

    clone.m = new TreeMap<>(m);
    return clone;
}

六、comparator:返回對此 set 中的元素進行排序的比較器;若是此 set 使用其元素的天然順序,則返回 null。
public Comparator<? super E> comparator() {

return m.comparator();
}

七、contains:若是此 set 包含指定的元素,則返回 true。
public boolean contains(Object o) {

return m.containsKey(o);
}

八、descendingIterator:返回在此 set 元素上按降序進行迭代的迭代器。
public Iterator<E> descendingIterator() {

return m.descendingKeySet().iterator();
}

九、descendingSet:返回此 set 中所包含元素的逆序視圖。
public NavigableSet<E> descendingSet() {

return new TreeSet<>(m.descendingMap());
}

十、first:返回此 set 中當前第一個(最低)元素。
public E first() {

return m.firstKey();
}

十一、floor:返回此 set 中小於等於給定元素的最大元素;若是不存在這樣的元素,則返回 null。
public E floor(E e) {

return m.floorKey(e);
}

十二、headSet:返回此 set 的部分視圖,其元素嚴格小於 toElement。
public SortedSet<E> headSet(E toElement) {

return headSet(toElement, false);
}

1三、higher:返回此 set 中嚴格大於給定元素的最小元素;若是不存在這樣的元素,則返回 null。
public E higher(E e) {

return m.higherKey(e);
}

1四、isEmpty:若是此 set 不包含任何元素,則返回 true。
public boolean isEmpty() {

return m.isEmpty();
}

1五、iterator:返回在此 set 中的元素上按升序進行迭代的迭代器。
public Iterator<E> iterator() {

return m.navigableKeySet().iterator();
}

1六、last:返回此 set 中當前最後一個(最高)元素。
public E last() {

return m.lastKey();
}

1七、lower:返回此 set 中嚴格小於給定元素的最大元素;若是不存在這樣的元素,則返回 null。
public E lower(E e) {

return m.lowerKey(e);
}

1八、pollFirst:獲取並移除第一個(最低)元素;若是此 set 爲空,則返回 null。
public E pollFirst() {

Map.Entry<E,?> e = m.pollFirstEntry();
    return (e == null) ? null : e.getKey();
}

1九、pollLast:獲取並移除最後一個(最高)元素;若是此 set 爲空,則返回 null。
public E pollLast() {

Map.Entry<E,?> e = m.pollLastEntry();
    return (e == null) ? null : e.getKey();
}

20、remove:將指定的元素從 set 中移除(若是該元素存在於此 set 中)。
public boolean remove(Object o) {

return m.remove(o)==PRESENT;
}

該方法與put相似,只不過把插入換成了刪除,而且要進行刪除後調整

2一、size:返回 set 中的元素數(set 的容量)。
public int size() {

return m.size();
}

2二、subSet:返回此 set 的部分視圖
/**

* 返回此 set 的部分視圖,其元素範圍從 fromElement 到 toElement。
 */
 public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
         E toElement,   boolean toInclusive) {
         return new TreeSet<>(m.subMap(fromElement, fromInclusive,
              toElement,   toInclusive));
 }
 
 /**
  * 返回此 set 的部分視圖,其元素從 fromElement(包括)到 toElement(不包括)。
  */
 public SortedSet<E> subSet(E fromElement, E toElement) {
     return subSet(fromElement, true, toElement, false);
 }

2三、tailSet:返回此 set 的部分視圖
/**

* 返回此 set 的部分視圖,其元素大於(或等於,若是 inclusive 爲 true)fromElement。
 */
public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
    return new TreeSet<>(m.tailMap(fromElement, inclusive));
}

/**
 * 返回此 set 的部分視圖,其元素大於等於 fromElement。
 */
public SortedSet<E> tailSet(E fromElement) {
    return tailSet(fromElement, true);
}

最後

因爲TreeSet是基於TreeMap實現的,因此若是咱們對treeMap有了必定的瞭解,對TreeSet那是小菜一碟,咱們從TreeSet中的源碼能夠看出,其實現過程很是簡單,幾乎全部的方法實現所有都是基於TreeMap的。

LinkedHashSet

LinkedHashSet內部是如何工做的

LinkedHashSet是HashSet的一個「擴展版本」,HashSet並無論什麼順序,不一樣的是LinkedHashSet會維護「插入順序」。HashSet內部使用HashMap對象來存儲它的元素,而LinkedHashSet內部使用LinkedHashMap對象來存儲和處理它的元素。這篇文章,咱們將會看到LinkedHashSet內部是如何運做的及如何維護插入順序的。

咱們首先着眼LinkedHashSet的構造函數。在LinkedHashSet類中一共有4個構造函數。這些構造函數都只是簡單地調用父類構造函數(如HashSet類的構造函數)。 下面看看LinkedHashSet的構造函數是如何定義的。
//Constructor - 1

public LinkedHashSet(int initialCapacity, float loadFactor)
{

super(initialCapacity, loadFactor, true);              //Calling super class constructor

}

//Constructor - 2

public LinkedHashSet(int initialCapacity)
{

super(initialCapacity, .75f, true);             //Calling super class constructor

}

//Constructor - 3

public LinkedHashSet()
{

super(16, .75f, true);                //Calling super class constructor

}

//Constructor - 4

public LinkedHashSet(Collection<? extends E> c)
{

super(Math.max(2*c.size(), 11), .75f, true);          //Calling super class constructor
    addAll(c);

}

在上面的代碼片斷中,你可能注意到4個構造函數調用的是同一個父類的構造函數。這個構造函數(父類的,譯者注)是一個包內私有構造函數(見下面的代碼,HashSet的構造函數沒有使用public公開,譯者注),它只能被LinkedHashSet使用。

這個構造函數須要初始容量,負載因子和一個boolean類型的啞值(沒有什麼用處的參數,做爲標記,譯者注)等參數。這個啞參數只是用來區別這個構造函數與HashSet的其餘擁有初始容量和負載因子參數的構造函數,下面是這個構造函數的定義,
HashSet(int initialCapacity, float loadFactor, boolean dummy)
{

map = new LinkedHashMap<>(initialCapacity, loadFactor);

}

顯然,這個構造函數內部初始化了一個LinkedHashMap對象,這個對象剛好被LinkedHashSet用來存儲它的元素。

LinkedHashSet並無本身的方法,全部的方法都繼承自它的父類HashSet,所以,對LinkedHashSet的全部操做方式就好像對HashSet操做同樣。

惟一的不一樣是內部使用不一樣的對象去存儲元素。在HashSet中,插入的元素是被當作HashMap的鍵來保存的,而在LinkedHashSet中被看做是LinkedHashMap的鍵。

這些鍵對應的值都是常量PRESENT(PRESENT是HashSet的靜態成員變量,譯者注)。

LinkedHashSet是如何維護插入順序的

LinkedHashSet使用LinkedHashMap對象來存儲它的元素,插入到LinkedHashSet中的元素其實是被看成LinkedHashMap的鍵保存起來的。

LinkedHashMap的每個鍵值對都是經過內部的靜態類Entry<K, V>實例化的。這個 Entry<K, V>類繼承了HashMap.Entry類。

這個靜態類增長了兩個成員變量,before和after來維護LinkedHasMap元素的插入順序。這兩個成員變量分別指向前一個和後一個元素,這讓LinkedHashMap也有相似雙向鏈表的表現。
private static class Entry<K,V> extends HashMap.Entry<K,V>
{

// These fields comprise the doubly linked list used for iteration.
    Entry<K,V> before, after;

    Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
        super(hash, key, value, next);
    }

}

從上面代碼看到的LinkedHashMap內部類的前面兩個成員變量——before和after負責維護LinkedHashSet的插入順序。LinkedHashMap定義的成員變量header保存的是 這個雙向鏈表的頭節點。header的定義就像下面這樣,

接下來看一個例子就知道LinkedHashSet內部是如何工做的了。
public class LinkedHashSetExample
{

public static void main(String[] args)
{
    //Creating LinkedHashSet

    LinkedHashSet<String> set = new LinkedHashSet<String>();

    //Adding elements to LinkedHashSet

    set.add("BLUE");

    set.add("RED");

    set.add("GREEN");    

    set.add("BLACK");
}

}

更多內容請關注微信公衆號【Java技術江湖】

一位阿里 Java 工程師的技術小站。做者黃小斜,專一 Java 相關技術:SSM、SpringBoot、MySQL、分佈式、中間件、集羣、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!(關注公衆號後回覆」資料「便可領取 3T 免費技術學習資源以及我我原創的程序員校招指南、Java學習指南等資源)

在這裏插入圖片描述

相關文章
相關標籤/搜索