面試容器

hashmap實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu),數(shù)組、桶等。

image.png

如圖所示 JDK 1.7,是以數(shù)組+鏈表組成的,鏈表為相同hash的鍵值對(duì),表頭儲(chǔ)存在數(shù)組中,形成以拉鏈的結(jié)構(gòu),這就是他的數(shù)據(jù)結(jié)構(gòu),當(dāng)有新的鍵值對(duì)插入的時(shí)候, hash函數(shù)得到數(shù)組的位置,如果根據(jù)位置找到放入鍵值對(duì),如果該位置存在鍵值對(duì),就遍歷到表尾,進(jìn)行放入。
桶就是上述的鏈表的,數(shù)組被叫做桶數(shù)組。
在JDK1.8后,鏈表長度大于8后,會(huì)樹化變?yōu)榧t黑樹。相當(dāng)于數(shù)組+鏈表+紅黑樹組成。

hashmap是空的情況下,map.get(a) 得到的是啥

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 1. 定位鍵值對(duì)所在桶的位置
    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;
        if ((e = first.next) != null) {
            // 2. 如果 first 是 TreeNode 類型,則調(diào)用黑紅樹查找方法
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                
            // 2. 對(duì)鏈表進(jìn)行查找
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

來自JDK 1.8 get方法,內(nèi)部會(huì)調(diào)用getNode,因?yàn)槲幢怀跏蓟ǔ跏蓟瘯r(shí)在put操作時(shí)候進(jìn)行的),table==null,return null。

HashMap怎么手寫實(shí)現(xiàn)?

理解到就是數(shù)組+鏈表的實(shí)現(xiàn)
https://blog.csdn.net/it_lihongmin/article/details/76377229

hashmap如何put數(shù)據(jù)(從hashmap源碼角度講解)?

環(huán)境JDK1.8
插入操作的入口方法是 put(K,V),

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 初始化桶數(shù)組 table,table 被延遲到插入新數(shù)據(jù)時(shí)再進(jìn)行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 如果桶中不包含鍵值對(duì)節(jié)點(diǎn)引用,則將新鍵值對(duì)節(jié)點(diǎn)的引用存入桶中即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 如果鍵的值以及節(jié)點(diǎn) hash 等于鏈表中的第一個(gè)鍵值對(duì)節(jié)點(diǎn)時(shí),則將 e 指向該鍵值對(duì)
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
            
        // 如果桶中的引用類型為 TreeNode,則調(diào)用紅黑樹的插入方法
        else if (p instanceof TreeNode)  
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 對(duì)鏈表進(jìn)行遍歷,并統(tǒng)計(jì)鏈表長度
            for (int binCount = 0; ; ++binCount) {
                // 鏈表中不包含要插入的鍵值對(duì)節(jié)點(diǎn)時(shí),則將該節(jié)點(diǎn)接在鏈表的最后
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 如果鏈表長度大于或等于樹化閾值,則進(jìn)行樹化操作
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                
                // 條件為 true,表示當(dāng)前鏈表包含要插入的鍵值對(duì),終止遍歷
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        
        // 判斷要插入的鍵值對(duì)是否存在 HashMap 中
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // onlyIfAbsent 表示是否僅在 oldValue 為 null 的情況下更新鍵值對(duì)的值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 鍵值對(duì)數(shù)量超過閾值時(shí),則進(jìn)行擴(kuò)容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
  }
  • 當(dāng)桶數(shù)組 table 為空時(shí),通過擴(kuò)容的方式初始化 table
  • 查找要插入的鍵值對(duì)是否已經(jīng)存在,存在的話根據(jù)條件判斷是否用新值替換舊值
  • 如果不存在,則將鍵值對(duì)鏈入鏈表中,并根據(jù)鏈表長度決定是否將鏈表轉(zhuǎn)為紅黑樹
  • 判斷鍵值對(duì)數(shù)量是否大于閾值,大于的話則進(jìn)行擴(kuò)容操作

Resize操作的過程。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 如果 table 不為空,表明已經(jīng)初始化過了
    if (oldCap > 0) {
        // 當(dāng) table 容量超過容量最大值,則不再擴(kuò)容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        } 
        // 按舊容量和閾值的2倍計(jì)算新容量和閾值的大小
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    } else if (oldThr > 0) // initial capacity was placed in threshold
        /*
         * 初始化時(shí),將 threshold 的值賦值給 newCap,
         * HashMap 使用 threshold 變量暫時(shí)保存 initialCapacity 參數(shù)的值
         */ 
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        /*
         * 調(diào)用無參構(gòu)造方法時(shí),桶數(shù)組容量為默認(rèn)容量,
         * 閾值為默認(rèn)容量與默認(rèn)負(fù)載因子乘積
         */
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
    // newThr 為 0 時(shí),按閾值計(jì)算公式進(jìn)行計(jì)算
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    // 創(chuàng)建新的桶數(shù)組,桶數(shù)組的初始化也是在這里完成的
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        // 如果舊的桶數(shù)組不為空,則遍歷桶數(shù)組,并將鍵值對(duì)映射到新的桶數(shù)組中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    // 重新映射時(shí),需要對(duì)紅黑樹進(jìn)行拆分
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 遍歷鏈表,并將鏈表節(jié)點(diǎn)按原順序進(jìn)行分組
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 將分組后的鏈表映射到新桶中
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
  • 計(jì)算新桶數(shù)組的容量 newCap 和新閾值 newThr
  • 根據(jù)計(jì)算出的 newCap 創(chuàng)建新的桶數(shù)組,桶數(shù)組 table 也是在這里進(jìn)行初始化的
  • 將鍵值對(duì)節(jié)點(diǎn)重新映射到新的桶數(shù)組里。如果節(jié)點(diǎn)是 TreeNode 類型,則需要拆分紅黑樹。如果是普通節(jié)點(diǎn),則節(jié)點(diǎn)按原順序進(jìn)行分組。

- 簡單說說 JDK1.7 中 HashMap 什么情況下會(huì)發(fā)生擴(kuò)容?如何擴(kuò)容?

JDK1.7中HashMap擴(kuò)容比較簡單,鍵值對(duì)的數(shù)量 >=threshold = capacity * loadFactor就達(dá)到擴(kuò)容的條件,
1.先模運(yùn)算定位我們要插入這個(gè)桶的位置
2.判斷插入的桶子是否為空,為空就將鍵值對(duì)存入即可,不為空則遍歷到表的最后一個(gè)存入,或者更新Node.

- 簡單說說 JDK1.7 中 HashMap 什么情況下會(huì)發(fā)生擴(kuò)容?如何擴(kuò)容?

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);//當(dāng)size超過臨界閾值threshold,并且即將發(fā)生哈希沖突時(shí)進(jìn)行擴(kuò)容
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

通過以上代碼能夠得知,當(dāng)發(fā)生哈希沖突并且size大于閾值的時(shí)候,需要進(jìn)行數(shù)組擴(kuò)容.擴(kuò)容時(shí),需要新建一個(gè)長度為之前數(shù)組2倍的新的數(shù)組,然后將當(dāng)前的Entry數(shù)組中的元素全部傳輸過去,擴(kuò)容后的新數(shù)組長度為之前的2倍.

擴(kuò)容操作發(fā)生在resize(int newCapacity)

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    
    
void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
     //for循環(huán)中的代碼,逐個(gè)遍歷鏈表,重新計(jì)算索引位置,將老數(shù)組數(shù)據(jù)復(fù)制到新數(shù)組中去(數(shù)組不存儲(chǔ)實(shí)際數(shù)據(jù),所以僅僅是拷貝引用而已)
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
          //將當(dāng)前entry的next鏈指向新的索引位置,newTable[i]有可能為空,有可能也是個(gè)entry鏈,如果是entry鏈,直接在鏈表頭部插入。
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

這個(gè)方法就是將每個(gè)Node遍歷出來,通過key值進(jìn)行hash擾亂運(yùn)算后,與length-1取余得到新的下標(biāo),放入的流程和put一樣。
http://www.cnblogs.com/chengxiao/p/6059914.html

hashmap容量為2次冪的原因。

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

每一個(gè)鍵值對(duì)必須調(diào)用這個(gè)上面的方法得到位置索引的過程,。length-1 ,比如length=16,得到就是15,為11111,這樣的低位和高位的或運(yùn)算得到得到的可能會(huì)更多,所以分布就更為均勻,從而減少了碰撞的幾率。
https://blog.csdn.net/qq_36523667/article/details/79657400
http://www.cnblogs.com/chengxiao/p/6059914.html

HashMap的實(shí)現(xiàn),與HashSet的區(qū)別

HashMap基于hashing原理,我們通過put()和get()方法儲(chǔ)存和獲取對(duì)象。當(dāng)我們將鍵值對(duì)傳遞給put()方法時(shí),它調(diào)用鍵對(duì)象的hashCode()方法來計(jì)算hashcode,讓后找到bucket位置來儲(chǔ)存值對(duì)象。當(dāng)獲取對(duì)象時(shí),通過鍵對(duì)象的equals()方法找到正確的鍵值對(duì),然后返回值對(duì)象。HashMap使用鏈表來解決碰撞問題,當(dāng)發(fā)生碰撞了,對(duì)象將會(huì)儲(chǔ)存在鏈表的下一個(gè)節(jié)點(diǎn)中。 HashMap在每個(gè)鏈表節(jié)點(diǎn)中儲(chǔ)存鍵值對(duì)對(duì)象。

當(dāng)兩個(gè)不同的鍵對(duì)象的hashcode相同時(shí)會(huì)發(fā)生什么? 它們會(huì)儲(chǔ)存在同一個(gè)bucket位置的鏈表中。鍵對(duì)象的equals()方法用來找到鍵值對(duì)。

https://www.cnblogs.com/beatIteWeNerverGiveUp/p/5709841.html
https://www.cnblogs.com/beatIteWeNerverGiveUp/p/5709841.html

HashSet與HashMap怎么判斷集合元素重復(fù)

發(fā)現(xiàn)HashSet是借助HashMap來實(shí)現(xiàn)的,存入的元素作為key,利用HashMap中Key的唯一性,來保證HashSet中不出現(xiàn)重復(fù)值,所以最終得看HashMap源碼如何判斷元素重復(fù)的。

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //!!!!!
        V oldValue = e.value;
        e.value = value;
        e.recordAccess(this);
        return oldValue;
        }
    }
}

在put可以看見先的得到下標(biāo),然后遍歷每一個(gè)元素比較hashcode()和equal()來判斷是否唯一。
注意:為了保證對(duì)象不會(huì)出現(xiàn)重復(fù)值,被存方的元素必須重寫equal()。
https://blog.csdn.net/ning109314/article/details/17354839

簡單說說 HashMap 和 LinkedHashMap 的區(qū)別

LinkedHashMap 實(shí)現(xiàn)與 HashMap 的不同之處在于,LinkedHashMap 維護(hù)著一個(gè)運(yùn)行于所有條目的雙重鏈接列表。此鏈接列表定義了迭代順序,該迭代順序可以是插入順序或者是訪問順序。該迭代順序通常就是將鍵插入到映射中的順序,如果在映射中重新插入鍵,則插入的順序?qū)⒉皇苡绊憽T诒闅v的時(shí)候會(huì)比HashMap慢,不過有種情況例外,當(dāng)HashMap容量很大,實(shí)際數(shù)據(jù)較少時(shí),遍歷起來可能會(huì)比 LinkedHashMap慢,因?yàn)長inkedHashMap的遍歷速度只和實(shí)際數(shù)據(jù)有關(guān),和容量無關(guān),而HashMap的遍歷速度和他的容量有關(guān)。

https://www.cnblogs.com/hubingxu/archive/2012/02/21/2361281.html
http://wiki.jikexueyuan.com/project/java-collection/linkedhashmap.html
https://blog.csdn.net/xiyuan1999/article/details/6198394
https://blog.csdn.net/qq_33535433/article/details/80035383
https://blog.csdn.net/lzm1340458776/article/details/37816073

說說你對(duì) LinkedHashSet 的理解及其與 HashSet 的關(guān)系?

LinkedHashSet集合同樣是根據(jù)元素的hashCode值來決定元素的存儲(chǔ)位置,但是它同時(shí)使用鏈表維護(hù)元素的次序。這樣使得元素看起 來像是以插入順序保存的,也就是說,當(dāng)遍歷該集合時(shí)候,LinkedHashSet將會(huì)以元素的添加順序訪問集合的元素。

關(guān)系LinkedHashSet繼承與HashSet,但只是在構(gòu)造函數(shù)里調(diào)用父類的構(gòu)造函數(shù)并且傳入LinkedHashMap,而LinkedHashMap內(nèi)部維護(hù)的雙向鏈表,這讓LinkedHashSet具有保存迭代順序順序的功能,LinkedHashSet在迭代訪問Set中的全部元素時(shí),性能比HashSet好,但是插入時(shí)性能稍微遜色于HashSet。
https://www.cnblogs.com/Terry-greener/archive/2011/12/02/2271707.html

HashMap是如何解決hash碰撞的?拉鏈法的優(yōu)缺點(diǎn)

利用(JDK1.7以下)拉鏈法,解決hash沖突

hash:散列,是把任意長度的輸入,通過散列算法,變成固定長度的輸出,該輸出就是散列值。這種轉(zhuǎn)換是一種 壓縮映射,也就是說,散列值的空間通常遠(yuǎn)小于輸入的空間。不同的輸入可能會(huì)散列成相同的輸出,從而不可能從散列值來唯一的確定輸入值。簡單的說,就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數(shù)。
hash沖突: 就是根據(jù)key即經(jīng)過一個(gè)哈希函數(shù)f(key)運(yùn)算得到的結(jié)果i的作為地址去存放當(dāng)前的key value鍵值對(duì)(這個(gè)是hashmap的存值方式),但是卻發(fā)現(xiàn)算出來的地址上存在另外的鍵值對(duì)(不為空),這就成為hash沖突。
鏈地址法(拉鏈法):JDK1.7 hashmap用的拉鏈法,將哈希值相同的元素以鏈表形式排列,并且表頭的地址為hash表的第i個(gè)元素
,在JDK1.8之后 則是一個(gè)混合模式,如果單鏈表的節(jié)點(diǎn)大于8,單鏈表會(huì)樹化為紅黑樹儲(chǔ)存具有相同hash的元素。
[圖片上傳失敗...(image-50d3cd-1535258600772)]

優(yōu)點(diǎn):
解決了線性探測所導(dǎo)致的太多的哈希沖突。
刪除結(jié)點(diǎn)相比于開放定址法更容易實(shí)現(xiàn)(在線性探測中如果刪除結(jié)點(diǎn),后面的結(jié)點(diǎn)無法訪問)。
由于拉鏈法中各鏈表上的結(jié)點(diǎn)空間是動(dòng)態(tài)申請(qǐng)的,故它更適合于造表前無法確定表長的情況;
在用拉鏈法構(gòu)造的散列表中,刪除結(jié)點(diǎn)的操作易于實(shí)現(xiàn)。只要簡單地刪去鏈表上相應(yīng)的結(jié)點(diǎn)即可。而對(duì)開放地址法構(gòu)造的散列表,刪除結(jié)點(diǎn)不能簡單地將被刪結(jié) 點(diǎn)的空間置為空,否則將截?cái)嘣谒筇钊松⒘斜淼耐x詞結(jié)點(diǎn)的查找路徑。這是因?yàn)楦鞣N開放地址法中,空地址單元(即開放地址)都是查找失敗的條件。因此在 用開放地址法處理沖突的散列表上執(zhí)行刪除操作,只能在被刪結(jié)點(diǎn)上做刪除標(biāo)記,而不能真正刪除結(jié)點(diǎn)

缺陷:如果相同元素過多,元素在一個(gè)桶內(nèi)部鏈接過長,反而導(dǎo)致時(shí)間復(fù)雜度上升。解決思路是桶中元素不再指向鏈表,而指向一個(gè)紅黑樹
指針需要額外的空間,故當(dāng)結(jié)點(diǎn)規(guī)模較小時(shí),開放定址法較為節(jié)省空間,而若將節(jié)省的指針空間用來擴(kuò)大散列表的規(guī)模,可使裝填因子變小,這又減少了開放定址法中的沖突,從而提高平均查找速度。

Hash怎么防止碰撞

防止還是難以防止,但是可以減少Hash碰撞

  • 構(gòu)造hash時(shí) 增大initialCapacity,減少loadFactor
  • 重寫hashCode方法,增加得到hash的復(fù)雜度
  • 傳入類的時(shí)候按照規(guī)則重寫自定義類equal方法

hashmap的參數(shù)及影響性能的關(guān)鍵參數(shù):加載因子和初始容量。

簡單說說你對(duì) HashMap 構(gòu)造方法中 initialCapacity(初始容量)、loadFactor(加載因子)的理解

initialCapacity:初始化hashTable的長度,是影響擴(kuò)容閥值因素之一,默認(rèn)16,擴(kuò)容時(shí)候數(shù)組長度會(huì)變成2倍,不管如何內(nèi)部會(huì)調(diào)用tableSizeFor()使用位運(yùn)算 找到大于或等于該值的最小2的冪,作為Capacity儲(chǔ)存,而initialCapacity將會(huì)被棄用。
loadFactor:
在節(jié)點(diǎn)均勻分布在桶數(shù)組中的條件下,可以調(diào)節(jié)負(fù)載因子。
節(jié)負(fù)載因子低時(shí),HashMap容納的鍵值對(duì)變少了,擴(kuò)容時(shí)候,重寫將鍵值對(duì)儲(chǔ)存新的通數(shù)組里,這樣hash碰撞就降低了,鏈表變短了,增刪查改的操作效率就會(huì)變高。
節(jié)負(fù)載因子高的時(shí)候,HashMap容納的鍵值對(duì)變高,空間利用率變高,鏈表邊長,效率變低,時(shí)間和空間的互換

HashSet 和 TreeSet

HashSet、LinkedHashSet與TreeSet有什么區(qū)別,應(yīng)用場景是什么?

https://www.cnblogs.com/Terry-greener/archive/2011/12/02/2271707.html
https://blog.csdn.net/StemQ/article/details/66477615
http://www.lxweimin.com/p/14bd5d9654fe
http://www.coolblog.xyz/2018/01/11/TreeMap%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/

HashSet使用哈希表實(shí)現(xiàn)的,元素是無序的。添加、刪除操作時(shí)間復(fù)雜度都是O(1)。
TreeSet內(nèi)部結(jié)構(gòu)是一個(gè)樹結(jié)構(gòu)(紅黑樹),元素是有序的,添加、刪除操作時(shí)間復(fù)雜度為O(log(n)),并且提供了first(), last(), headSet(), tailSet()等方法來處理有序集合。?
TreeSet不允許重復(fù),不允許null值(如果有基于null的比較器,就可以允許為null),默認(rèn)按升序排列。
LinkedHashSet是HashSet的子類,是介于HashSet 和 TreeSet之間,內(nèi)部是一個(gè)雙向鏈表結(jié)構(gòu),時(shí)間復(fù)雜度是O(1)。允許null值,保留插入順序,非線程安全。

簡而言之,如何你需要的是一個(gè)快速的集合,建議你使用HashSet,如果你需要的是一個(gè)排序集合,請(qǐng)選擇TreeSet,如果你需要一套能夠存儲(chǔ)插入順序的集合,請(qǐng)使用LinkedHashSet。

請(qǐng)使用 Java 集合實(shí)現(xiàn)一個(gè)簡約優(yōu)雅的 LRU 容器

由于 LinkedHashMap 天生支持插入順序或者訪問順序的 key-value 對(duì),而 LRU 算法的核心恰巧用到它的訪問順序特性,即對(duì)一個(gè)鍵執(zhí)行 get、put 操作后其對(duì)應(yīng)的鍵值對(duì)會(huì)移到鏈表末尾,所以最末尾的是最近訪問的,最開始的是最久沒被訪問的。LinkedHashMap 有一個(gè) boolean 類型的 accessOrder 參數(shù),當(dāng)該參數(shù)為 true 時(shí)則按照元素最后訪問時(shí)間在雙向鏈表中排序,為 false 則按照插入順序排序,默認(rèn)為 false,所以這里需要的操作就是 accessOrder 為 true 的情況。

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
            private int maxEntries;//maxEntries 最大緩存?zhèn)€數(shù)

            public LRUCache(int maxEntries) {
                super(16, 0.75f, true);
                this.maxEntries = maxEntries;
            }

            //在添加元素到LinkedHashMap后會(huì)調(diào)用這個(gè)方法,傳遞的參數(shù)是最久沒被訪問的鍵值對(duì),
            // 如果這個(gè)方法返回true則這個(gè)最久的鍵值對(duì)就會(huì)被刪除,LinkedHashMap的實(shí)現(xiàn)總是返回false,
            // 所以容量沒有限制。
            @Override
            protected boolean removeEldestEntry(Entry<K, V> eldest) {
                return size() > maxEntries;
            }
        }

可以看見實(shí)現(xiàn)的簡約 LRU 容器核心優(yōu)雅點(diǎn)就是充分利用了 LinkedHashMap 的有序性特性和容量限制特性。

- List/Set/Map三者的區(qū)別.

簡單說說你理解的 List 、Map、Set、Queue 的區(qū)別和關(guān)系

List 和 Map 的實(shí)現(xiàn)方式以及存儲(chǔ)方式

List,Set,Map的區(qū)別,實(shí)現(xiàn)方式以及存儲(chǔ)方式。

集合的接口和具體實(shí)現(xiàn)類,介紹

容器類介紹以及之間的區(qū)別

集合類相關(guān)over

https://blog.csdn.net/u013825231/article/details/52027323
https://blog.csdn.net/lipeng88888888/article/details/78456047
https://blog.csdn.net/chuntiandejiaobu10/article/details/52350338
https://blog.csdn.net/ice197983/article/details/1546848
http://www.cnblogs.com/goody9807/p/6431501.html

Collection

一組"對(duì)立"的元素,通常這些元素都服從某種規(guī)則

  • List必須保持元素特定的順序
  • Set不能有重復(fù)元素
  • Queue保持一個(gè)隊(duì)列(先進(jìn)先出)的順序

2) Map

一組成對(duì)的"鍵值對(duì)"對(duì)象

Collection和Map的區(qū)別在于容器中每個(gè)位置保存的元素個(gè)數(shù):

  • Collection 每個(gè)位置只能保存一個(gè)元素(對(duì)象)
  • Map保存的是"鍵值對(duì)",就像一個(gè)小型數(shù)據(jù)庫。我們可以通過"鍵"找到該鍵對(duì)應(yīng)的"值"

List

共性:有序 可以重復(fù)
List集合特征:

  • 允許重復(fù)元素添加
  • 有序:放入的順序和取出的順序是一樣的
  • 一個(gè)List可以生成ListIterator,使用它可以從兩個(gè)方向遍歷List,也可以從List中間插入和移除元素。

List接口主要實(shí)現(xiàn)類包括:

  • ArrayList() 代表長度可以改變得數(shù)組。可以對(duì)元素進(jìn)行隨機(jī)的訪問,向ArrayList()中插入與刪除元素的速度慢。沒有線程安全,性能高
  • LinkedList()雙向鏈表式算法: 在實(shí)現(xiàn)中采用鏈表數(shù)據(jù)結(jié)構(gòu)。每次操作插入刪除,只需要修改元素的后置節(jié)點(diǎn)引用,不需要后面元素移動(dòng)位置,插入和刪除速度快,每次都要從開始往后依次查找,訪問速度慢。

List接口對(duì)Collection進(jìn)行了簡單的擴(kuò)充,它的具體實(shí)現(xiàn)類常用的有ArrayList和LinkedList。你可以將任何東西放到一個(gè)List容器中,并在需要時(shí)從中取出。ArrayList從其命名中可以看出它是一種類似數(shù)組的形式進(jìn)行存儲(chǔ),因此它的隨機(jī)訪問速度極快,而LinkedList的內(nèi)部實(shí)現(xiàn)是鏈表,它適合于在鏈表中間需要頻繁進(jìn)行插入和刪除操作。在具體應(yīng)用時(shí)可以根據(jù)需要自由選擇。前面說的Iterator只能對(duì)容器進(jìn)行向前遍歷,而ListIterator則繼承了Iterator的思想,并提供了對(duì)List進(jìn)行雙向遍歷的方法。

Set集合

對(duì)象元素不能重復(fù),Set的元素必須定義equals()方法以確保對(duì)象的唯一性。
可排序
也是Collection的一種擴(kuò)展加入 ,Set與Collection有完全一樣的接口

set接口主要實(shí)現(xiàn)類包括:

  • HashSet:能快速定位一個(gè)元素,但是你放到HashSet中的對(duì)象需要實(shí)現(xiàn)hashCode()方法,它使用了前面說過的哈希碼的算法。該也是線程不安全的,它不允許出現(xiàn)重復(fù)元素;允許包含值為null的元素,但最多只能有一個(gè)null元素。不能保證元素的插入順序,添加也是通過add進(jìn)行添加的,而輸出則是通過迭代Iteratori=hash.iterator();來實(shí)現(xiàn)的。

  • 而TreeSet則將放入其中的元素按序存放,這就要求你放入其中的對(duì)象是可排序的,這就用到了集合框架提供的另外兩個(gè)實(shí)用類Comparable和Comparator。一個(gè)類是可排序的,它就應(yīng)該實(shí)現(xiàn)Comparable接口。有時(shí)多個(gè)類具有相同的排序算法,那就不需要在每分別重復(fù)定義相同的排序算法,只要實(shí)現(xiàn)Comparator接口即可。

map接口:

是一種把鍵對(duì)象和值對(duì)象進(jìn)行關(guān)聯(lián)的容器,而一個(gè)值對(duì)象又可以是一個(gè)Map,依次類推,這樣就可形成一個(gè)多級(jí)映射。一個(gè)Map容器中的鍵對(duì)象不允許重復(fù)。當(dāng)然在使用過程中,某個(gè)鍵所對(duì)應(yīng)的值對(duì)象可能會(huì)發(fā)生變化,這時(shí)會(huì)按照最后一次修改的值對(duì)象與鍵對(duì)應(yīng)。對(duì)于值對(duì)象則沒有唯一性的要求。你可以將任意多個(gè)鍵都映射到一個(gè)值對(duì)象上,這不會(huì)發(fā)生任何問題(不過對(duì)你的使用卻可能會(huì)造成不便,你不知道你得到的到底是那一個(gè)鍵所對(duì)應(yīng)的值對(duì)象)。
Map有兩種比較常用的實(shí)現(xiàn):

  • HashMap HashMap也用到了哈希碼的算法,以便快速查找一個(gè)鍵,無序
  • TreeMap。TreeMap則是對(duì)鍵按序存放,因此它便有一些擴(kuò)展的方法,比如firstKey(),lastKey()等,你還可以從TreeMap中指定一個(gè)范圍以取得其子Map。鍵和值的關(guān)聯(lián)很簡單,用pub(Object key,Object value)方法即可將一個(gè)鍵與一個(gè)值對(duì)象相關(guān)聯(lián)。用get(Object key)可得到與此key對(duì)象所對(duì)應(yīng)的值對(duì)象。

Queue

用于模擬"隊(duì)列"這種數(shù)據(jù)結(jié)構(gòu)(先進(jìn)先出 FIFO)。隊(duì)列的頭部保存著隊(duì)列中存放時(shí)間最長的元素,隊(duì)列的尾部保存著隊(duì)列中存放時(shí)間最短的元素。新元素插入(offer)到隊(duì)列的尾部,訪問元素(poll)操作會(huì)返回隊(duì)列頭部的元素,隊(duì)列不允許隨機(jī)訪問隊(duì)列中的元素。結(jié)合生活中常見的排隊(duì)就會(huì)很好理解這個(gè)概念。

  • PriorityQueue
    PriorityQueue并不是一個(gè)比較標(biāo)準(zhǔn)的隊(duì)列實(shí)現(xiàn),PriorityQueue保存隊(duì)列元素的順序并不是按照加入隊(duì)列的順序,而是按照隊(duì)列元素的大小進(jìn)行重新排序,這點(diǎn)從它的類名也可以看出來

  • Deque
    Deque接口代表一個(gè)"雙端隊(duì)列",雙端隊(duì)列可以同時(shí)從兩端來添加、刪除元素,因此Deque的實(shí)現(xiàn)類既可以當(dāng)成隊(duì)列使用、也可以當(dāng)成棧使用

  • ArrayDeque
    是一個(gè)基于數(shù)組的雙端隊(duì)列,和ArrayList類似,它們的底層都采用一個(gè)動(dòng)態(tài)的、可重分配的Object[]數(shù)組來存儲(chǔ)集合元素,當(dāng)集合元素超出該數(shù)組的容量時(shí),系統(tǒng)會(huì)在底層重新分配一個(gè)Object[]數(shù)組來存儲(chǔ)集合元素

這篇文章贊 :https://blog.csdn.net/KingCat666/article/details/75579632

List, Set, Map是否繼承自Collection接口?

  • List, Set繼承Collection接口 ,List接口對(duì)Collection進(jìn)行了簡單的擴(kuò)充,而Set也是Collection的一種擴(kuò)展加入 ,Set與Collection有完全一樣的接口,Collection 每個(gè)位置只能保存一個(gè)元素(對(duì)象)。
  • Map 是一種把鍵對(duì)象和值對(duì)象進(jìn)行關(guān)聯(lián)的容器,沒有繼承于Collection接口,Map就像一個(gè)小型數(shù)據(jù)庫。我們可以通過"鍵"找到該鍵對(duì)應(yīng)的"值"。

Collection 和 Collections的區(qū)別

  • java.util.Collection 是一個(gè)集合接口。它提供了對(duì)集合對(duì)象進(jìn)行基本操作的通用接口方法。Collection接口在Java 類庫中有很多具體的實(shí)現(xiàn)。Collection接口的意義是為各種具體的集合提供了最大化的統(tǒng)一操作方式。
  • java.util.Collections 是一個(gè)包裝類。它包含有各種有關(guān)集合操作的靜態(tài)多態(tài)方法。此類不能實(shí)例化,就像一個(gè)工具類,服務(wù)于Java的Collection框架。
    http://pengcqu.iteye.com/blog/492196

hashtable和hashmap異同。

  • HashTable 基于 Dictionary 類,而 HashMap 是基于 AbstractMap。Dictionary 是任何可將鍵映射到相應(yīng)值的類- 的抽象父類,而 AbstractMap 是基于 Map 接口的實(shí)現(xiàn),它以最大限度地減少實(shí)現(xiàn)此接口所需的工作。
  • HashMap 的 key 和 value 都允許為 null,而 Hashtable 的 key 和 value 都不允許為 null。HashMap 遇到 key 為 null 的時(shí)候,調(diào)用 putForNullKey 方法進(jìn)行處理,而對(duì) value 沒有處理;Hashtable遇到 null,直接返回 NullPointerException。
  • Hashtable 方法是同步,而HashMap則不是。我們可以看一下源碼,Hashtable 中的幾乎所有的 public 的方法都是 synchronized 的,而有些方法也是在內(nèi)部通過 synchronized 代碼塊來實(shí)現(xiàn)。所以有人一般都建議如果是涉及到多線程同步時(shí)采用 HashTable,沒有涉及就采用 HashMap,但是在 Collections 類中存在一個(gè)靜態(tài)方法:synchronizedMap(),該方法創(chuàng)建了一個(gè)線程安全的 Map 對(duì)象,并把它作為一個(gè)封裝的對(duì)象來返回。HashTable是線程安全的.

為什么hashtable被棄用?

hashtable的線程安全的策略實(shí)現(xiàn)代價(jià)太大,而且簡單粗暴,get/put的方法操作都是synchronized,這相當(dāng)于給整個(gè)hash表加了一個(gè)大大的鎖,多線程訪問時(shí)候所有操作都得串行,在激烈的并發(fā)場景的性能會(huì)變得相當(dāng)?shù)牟睢?/p>

hashtable線程安全、synchronized加鎖

  1. hashtable線程是安全,他和hashmap類似內(nèi)部也是實(shí)現(xiàn)了拉鏈法的hash表,但是像put/get操作方法加上去synchronized關(guān)鍵詞,使得多線程操作該容器時(shí)串行,保證了線程安全。

concurrenthashmap的插入操作是直接操作數(shù)組中的鏈表嗎?

JDK1.7 版本 第一步定位Segment并且確定Segemnt已經(jīng)初始化、 2,調(diào)用segment這個(gè)加鎖的put的方法3.重新定位hash在數(shù)組的位置,如果在Buket位置上已存在鏈表就得操作鏈表進(jìn)行遍歷插入到表尾
JDK1.8 版本

  • 第一步根據(jù)hash定位索引位置,并且獲取對(duì)應(yīng)的索引位置(用Usafe.getObjectVolatile直接獲得指定內(nèi)存的數(shù)據(jù)保證拿到最新的數(shù)據(jù))
  • 如果 f ==null 用CAS直接插入Node
    • 如果CAS成功,說明已經(jīng)插入,然后判斷是不是要擴(kuò)容
    • 如果失敗,自旋嘗試在插入
  • 如果f的hash是-1 ,則說明其他線程正在擴(kuò)容,
  • 其余情況把新的Node節(jié)點(diǎn)按鏈表或紅黑樹的方式插入到合適的位置,這個(gè)過程采用同步內(nèi)置鎖實(shí)現(xiàn)并發(fā)。

說明1.7版本是先確定sgement的位置然后上鎖調(diào)用其put方法插入鏈表,而1.8版本則是直接插入,不過如果是鏈表插入上的是同步鎖

https://www.cnblogs.com/chengxiao/p/6842045.html
http://www.lxweimin.com/p/c0642afe03e0

concurrenthashmap相比于hashtable做的優(yōu)化、segment的概念、concurrenthashmap高效的原因

  • HashTable的線程安全使用的是一個(gè)單獨(dú)的全部Map范圍的鎖,ConcurrentHashMap拋棄了HashTable的單鎖機(jī)制,使用了鎖分離技術(shù),使得多個(gè)修改操作能夠并發(fā)進(jìn)行,只有進(jìn)行SIZE()操作時(shí)ConcurrentHashMap會(huì)鎖住整張表。

  • HashTable的put和get方法都是同步方法, 而ConcurrentHashMap的get方法多數(shù)情況都不用鎖,put方法需要鎖。

但是ConcurrentHashMap不能替代HashTable,因?yàn)閮烧叩牡鞯囊恢滦圆煌模琱ash table的迭代器是強(qiáng)一致性的,而concurrenthashmap是弱一致的。 ConcurrentHashMap的get,clear,iterator 都是弱一致性的。
hashtable是全局采用一個(gè)大鎖,使用synchronized來保證線程安全,但是在競爭激烈的情況下HashTable的效率非常低下,但是在concurrenthashmap中則是把桶數(shù)組分為多個(gè)sgement,并給每個(gè)segment上重入鎖ReentrantLock鎖的分段鎖形式

Segment繼承了ReentrantLock,所以它就是一種可重入鎖(ReentrantLock)。在ConcurrentHashMap,一個(gè)Segment就是一個(gè)子哈希表,Segment里維護(hù)了一個(gè)HashEntry數(shù)組,并發(fā)環(huán)境下,對(duì)于不同Segment的數(shù)據(jù)進(jìn)行操作是不用考慮鎖競爭的。(就按默認(rèn)的ConcurrentLeve為16來講,理論上就允許16個(gè)線程并發(fā)執(zhí)行,有木有很酷)

ConcurrentHashMap之所以高效是因?yàn)樗裮ap表劃分為了多個(gè)Segemnt,并且每個(gè)Segment繼承了ReentrantLock,這樣更好的降低了鎖的粒度,
http://www.cnblogs.com/ynyhl/p/9317688.html
https://blog.csdn.net/MBuger/article/details/62418754
https://blog.csdn.net/hitxueliang/article/details/24734861

SpareArray做了哪些優(yōu)化?

  • 使用int[]數(shù)組存放key,避免了HashMap中基本數(shù)據(jù)類型需要裝箱的步驟
  • 不需要額外的結(jié)構(gòu)體,單個(gè)元素的存儲(chǔ)成本更低
  • 數(shù)據(jù)量小的情況下,隨機(jī)訪問的效率更高

簡單說一說SpareArray的插入流程?

public void put(int key, E value) {
    // 首先通過二分查找去 key 數(shù)組中查找要插入的 key,返回索引
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    if (i >= 0) {
      // 如果 i>=0 說明數(shù)組中已經(jīng)有了該key,則直接覆蓋原來的值
        mValues[i] = value;
    } else {
      // 取反,這里得到的i應(yīng)該是最適合該key的插入位置,具體怎么得到的,后面會(huì)說
        i = ~i;
        // 如果索引小于當(dāng)前已經(jīng)存放的長度,并且這個(gè)位置上的值為DELETED(即被標(biāo)記為刪除的值)
        if (i < mSize && mValues[i] == DELETED) {
          // 直接賦值并返回,注意 size 不需要增加
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }
        // 到這一步說明直接賦值失敗,檢查當(dāng)前是否被標(biāo)記待回收且當(dāng)前存放的長度已經(jīng)大于或等于了數(shù)組長度
        if (mGarbage && mSize >= mKeys.length) {
            // 回收數(shù)組中應(yīng)該被干掉的值
            gc();
            // 重新再獲取一下索引,因?yàn)閿?shù)組發(fā)生了變化
            // Search again because indices may have changed.
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
        }
        // 最終在 i 位置上插入鍵與值,并且size +1
        mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
        mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
        mSize++;
    }
}

http://extremej.itscoder.com/sparsearray_source_analyse/

為什么 ArrayList 的增加或刪除操作相對(duì)來說效率比較低?能簡單解釋下為什么嗎?

只有刪除,或者是添加插入指定的位置時(shí)候才會(huì)出現(xiàn)效率變低的問題,因?yàn)椴还芴砑觿h除,都得移動(dòng)添加刪除位置后面得元素導(dǎo)致時(shí)間復(fù)雜度急劇上升。

image

image

http://www.coolblog.xyz/2018/01/28/ArrayList%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/#24-%E9%81%8D%E5%8E%86

ArrayList和Vector的區(qū)別 ,ArrayList與LinkedList有什么區(qū)別

- 簡單說說 ArrayList 和 Vector 的區(qū)別

ArrayList和Vector的區(qū)別

  • ArrayList和Vector都實(shí)現(xiàn)了List接口,底層都是基于Java數(shù)組來存儲(chǔ)集合元素

  • ArrayList使用transient修飾了elementData數(shù)組,而Vector則沒有

  • Vector是ArrayList的線程安全版本,用synchronized上同步鎖實(shí)現(xiàn)的,一個(gè)個(gè)大大的鎖了解一下

  • 容量擴(kuò)充 ArrayList為0.5倍+1,而Vector若指定了增長系數(shù),則新的容量=”原始容量+增長系數(shù)”, 否則增長為原來的1倍
    https://www.cnblogs.com/skywang12345/p/3308833.html#a4
    https://blog.csdn.net/itmyhome1990/article/details/76033175

ArrayList與LinkedList有什么區(qū)別

  1. ArrayList是實(shí)現(xiàn)了基于動(dòng)態(tài)數(shù)組的數(shù)據(jù)結(jié)構(gòu),而LinkedList是基于鏈表的數(shù)據(jù)結(jié)構(gòu);
  2. 對(duì)于隨機(jī)訪問get和set,ArrayList要優(yōu)于LinkedList,因?yàn)長inkedList要移動(dòng)指針;
  3. 對(duì)于添加和刪除操作add和remove,一般大家都會(huì)說LinkedList要比ArrayList快,因?yàn)锳rrayList要移動(dòng)數(shù)據(jù)。但是實(shí)際情況并非這樣,對(duì)于添加或刪除,LinkedList和ArrayList并不能明確說明誰快誰慢。
  • 添加刪除情況 :從源碼可以看出來linkedhashmap耗時(shí)的地方主要是鏈表的遍歷(盡管以及判斷是否從左右邊開始了)
    而arrayList耗時(shí)的地方就是System.arraycopy,讓index后面的元素的所有元素都移動(dòng)。
  • 所以當(dāng)插入的數(shù)據(jù)量很小時(shí),兩者區(qū)別不太大,當(dāng)插入的數(shù)據(jù)量大時(shí),大約在容量的1/10之前,LinkedList會(huì)優(yōu)于ArrayList,在其后就劣與ArrayList,且越靠近后面越差。

https://blog.csdn.net/eson_15/article/details/51145788

ArrayList 與 LinkedList 使用普通 for 循環(huán)遍歷誰快誰慢?為什么?

https://blog.csdn.net/zhzzhz123456/article/details/53323093

Arraylist 的動(dòng)態(tài)擴(kuò)容機(jī)制是如何自動(dòng)增加的?簡單說說你理解的流程

add方法里面判斷是不是需要擴(kuò)容(size+1),如果是需要就開始擴(kuò)容,擴(kuò)容計(jì)算出最小的容量,一般是初始化的開始,算出當(dāng)然容量的下限,如果當(dāng)前容量減去數(shù)組的長度大于0就開始擴(kuò)容,擴(kuò)容采用位運(yùn)算得到擴(kuò)容后的容量大小,然后進(jìn)行邊界檢查,最后把老數(shù)組的元素copy到新的數(shù)組中,這樣擴(kuò)容就完成了,因?yàn)锳rrayList的底部容器本身是數(shù)組,所以位置完全相同,就不存在計(jì)算擴(kuò)容后還需要重新計(jì)算位置的問題。
http://www.coolblog.xyz/2018/01/28/ArrayList%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/
https://blog.csdn.net/u010176014/article/details/52073339

簡單說說 Array 和 ArrayList 的區(qū)別?

應(yīng)該是完美的答案:
http://blog.qianlicao.cn/translate/2016/03/09/array-vs-arraylist/

http://www.coolblog.xyz/2018/01/18/HashMap-%E6%BA%90%E7%A0%81%E8%AF%A6%E7%BB%86%E5%88%86%E6%9E%90-JDK1-8/
https://blog.csdn.net/caisini_vc/article/details/52452498

為什么現(xiàn)在都不提倡使用 Vector 了

答:因?yàn)?Vector 實(shí)現(xiàn)并發(fā)安全的原理是在每個(gè)操作方法上加鎖,這些鎖并不是必須要的,在實(shí)際開發(fā)中一般都是通過鎖一系列的操作來實(shí)現(xiàn)線程安全,也就是說將需要同步的資源放一起加鎖來保證線程安全,如果多個(gè) Thread 并發(fā)執(zhí)行一個(gè)已經(jīng)加鎖的方法,但是在該方法中又有 Vector 的存在,Vector 本身實(shí)現(xiàn)中已經(jīng)加鎖了,雙重鎖會(huì)造成額外的開銷,即 Vector 同 ArrayList 一樣有 fail-fast 問題(即無法保證遍歷安全),所以在遍歷 Vector 操作時(shí)又得額外加鎖保證安全,還不如直接用 ArrayList 加鎖性能好,所以在 JDK 1.5 之后推薦使用 java.util.concurrent 包下的并發(fā)類。此外 Vector 是一個(gè)從 JDK1.0 就有的古老集合,那時(shí)候 Java 還沒有提供系統(tǒng)的集合框架,所以在 Vector 里提供了一些方法名很長的方法(如 addElement(Object obj),實(shí)際上這個(gè)方法和 add(Object obj) 沒什么區(qū)別),從 JDK1.2 以后 Java 提供了集合框架,然后就將 Vector 改為實(shí)現(xiàn) List 接口,從而導(dǎo)致 Vector 里有一些重復(fù)的方法。

- 為什么使用 for-each 時(shí)調(diào)用 List 的 remove 方法元素會(huì)拋出ConcurrentModificationException 異常?

 int expectedModCount = modCount;
 
  final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
    
  public E remove(int index) {
    rangeCheck(index);

    modCount++;
    // 返回被刪除的元素值
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 將 index + 1 及之后的元素向前移動(dòng)一位,覆蓋被刪除值
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 將最后一個(gè)元素置空,并將 size 值減1                
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

問題出在迭代器,其實(shí)for-each循環(huán)會(huì)在編譯的時(shí)候轉(zhuǎn)換為迭代器,在迭代的過程中remove會(huì)調(diào)用使得modcount增加,而在代碼中expectedModCount的值還是原來那個(gè)modcount的值,導(dǎo)致拋出異常。
這篇文章的結(jié)尾做了具體的闡述:
http://www.coolblog.xyz/2018/01/28/ArrayList%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/

什么是 Vector 和 Stack,各有什么特點(diǎn)?

Vector 是線程安全的動(dòng)態(tài)數(shù)組,同 ArrayList 一樣繼承自 AbstractList 且實(shí)現(xiàn)了 List、RandomAccess、Cloneable、Serializable 接口,內(nèi)部實(shí)現(xiàn)依然基于數(shù)組,Vector 與 ArrayList 基本是一致的,唯一不同的是 Vector 是線程安全的,會(huì)在可能出現(xiàn)線程安全的方法前面加上 synchronized 關(guān)鍵字,其和 ArrayList 類似,隨機(jī)訪問速度快,插入和移除性能較差(數(shù)組原因),支持 null 元素,有順序,元素可以重復(fù),線程安全。

Stack 是繼承自 Vector 基于動(dòng)態(tài)數(shù)組實(shí)現(xiàn)的線程安全棧,不過現(xiàn)在已經(jīng)不推薦使用了,Stack 是并發(fā)安全的后進(jìn)先出,實(shí)現(xiàn)了一些棧基本操作的方法(其實(shí)并不是只能后進(jìn)先出,因?yàn)槔^承自 Vector,所以可以有很多操作,嚴(yán)格說不是一個(gè)棧)。其共同點(diǎn)都是使用了方法鎖(即 synchronized)來保證并發(fā)安全的。

Collections.emptyList() 與 new ArrayList() 有什么區(qū)別

在這篇文章中提到如果你想返回一個(gè)空的List最好的方式就是返回Collections.emptyList(),且支持泛型,但是如果是return new ArrayList()的話,ArrayList()在初始化時(shí)會(huì)占用一定的資源增加開銷,這不是一個(gè)好的選擇。
https://blog.csdn.net/liyuming0000/article/details/49474659

容器類中fastfail的概念

fail-fast 機(jī)制是java集合(Collection)中的一種錯(cuò)誤機(jī)制。當(dāng)多個(gè)線程對(duì)同一個(gè)集合的內(nèi)容進(jìn)行操作時(shí),就可能會(huì)產(chǎn)生fail-fast事件。

例如:當(dāng)某一個(gè)線程A通過iterator去遍歷某集合的過程中,若該集合的內(nèi)容被其他線程所改變了;那么線程A訪問集合時(shí),就會(huì)拋出ConcurrentModificationException異常,產(chǎn)生fail-fast事件。
解決方法:改換成線程安全的類。
若 “modCount 不等于 expectedModCount”,則拋出ConcurrentModificationException異常,產(chǎn)生fail-fast事件。

https://blog.csdn.net/coslay/article/details/44891035

CopyOnWriteArrayList的了解。

CopyOnWriteArrayList這是一個(gè)ArrayList的線程安全的變體,其原理大概可以通俗的理解為:初始化的時(shí)候只有一個(gè)容器,很常一段時(shí)間,這個(gè)容器數(shù)據(jù)、數(shù)量等沒有發(fā)生變化的時(shí)候,大家(多個(gè)線程),都是讀取(假設(shè)這段時(shí)間里只發(fā)生讀取的操作)同一個(gè)容器中的數(shù)據(jù),所以這樣大家讀到的數(shù)據(jù)都是唯一、一致、安全的,但是后來有人往里面增加了一個(gè)數(shù)據(jù),這個(gè)時(shí)候CopyOnWriteArrayList 底層實(shí)現(xiàn)添加的原理是先copy出一個(gè)容器(可以簡稱副本),再往新的容器里添加這個(gè)新的數(shù)據(jù),最后把新的容器的引用地址賦值給了之前那個(gè)舊的的容器地址,但是在添加這個(gè)數(shù)據(jù)的期間,其他線程如果要去讀取數(shù)據(jù),仍然是讀取到舊的容器里的數(shù)據(jù)。
https://blog.csdn.net/likailonghaha/article/details/53405895
https://blog.csdn.net/hua631150873/article/details/51306021

請(qǐng)使用 LinkedList 模擬一個(gè)堆棧或隊(duì)列的數(shù)據(jù)結(jié)構(gòu)

https://blog.csdn.net/shineflowers/article/details/41746777

簡單說說 Iterator 和 ListIterator 的區(qū)別?

https://blog.csdn.net/yueying521314/article/details/80919095
https://www.nowcoder.com/questionTerminal/9dbbd35ff35e4e008fdd792c2b539940

為什么說集合的不同列表應(yīng)該選擇不同的遍歷方式,舉例談?wù)勀愕恼J(rèn)識(shí)?

https://blog.csdn.net/zhzzhz123456/article/details/53323093
https://blog.csdn.net/u012552052/article/details/45008237

并發(fā)集合了解哪些

之前聊到的hashtable concurrentHashmap CopyOnWriteArrayList Vector ..都可以聊
http://youyu4.iteye.com/blog/2352846

簡單說說 Comparable 和 Comparator 的區(qū)別和場景?

區(qū)別

Comparable可以認(rèn)為是一個(gè)內(nèi)比較器,實(shí)現(xiàn)了Comparable接口的類有一個(gè)特點(diǎn),就是這些類是可以和自己比較的,至于具體和另一個(gè)實(shí)現(xiàn)了Comparable接口的類如何比較,則依賴compareTo方法的實(shí)現(xiàn),compareTo方法也被稱為自然比較方法。如果開發(fā)者add進(jìn)入一個(gè)Collection的對(duì)象想要Collections的sort方法幫你自動(dòng)進(jìn)行排序的話,那么這個(gè)對(duì)象必須實(shí)現(xiàn)Comparable接口。
Comparator可以認(rèn)為是是一個(gè)外比較器,個(gè)人認(rèn)為有兩種情況可以使用實(shí)現(xiàn)Comparator接口的方式:
1、一個(gè)對(duì)象不支持自己和自己比較(沒有實(shí)現(xiàn)Comparable接口),但是又想對(duì)兩個(gè)對(duì)象進(jìn)行比較
2、一個(gè)對(duì)象實(shí)現(xiàn)了Comparable接口,但是開發(fā)者認(rèn)為compareTo方法中的比較方式并不是自己想要的那種比較方式

場景

  1. 如果實(shí)現(xiàn)類沒有實(shí)現(xiàn)Comparable接口,又想對(duì)兩個(gè)類進(jìn)行比較(或者實(shí)現(xiàn)類實(shí)現(xiàn)了Comparable接口,但是對(duì)compareTo方法內(nèi)的比較算法不滿意),那么可以實(shí)現(xiàn)Comparator接口,自定義一個(gè)比較器,寫比較算法
  2. 如果比較的方法只要用在一個(gè)類中,用該類實(shí)現(xiàn)Comparable接口就可以。
  3. 如果比較的方法在很多類中需要用到,就自己寫個(gè)類實(shí)現(xiàn)Comparator接口,這樣當(dāng)要比較的時(shí)候把實(shí)現(xiàn)了Comparator接口的類傳過去就可以,省得重復(fù)造輪子。這也是為什么Comparator會(huì)在java.util包下的原因。

https://blog.csdn.net/u011240877/article/details/53399019
https://www.cnblogs.com/linbingdong/p/5300720.html

簡單說說 EnumMap 的實(shí)現(xiàn)原理

簡單談?wù)勀銓?duì) EnumMap 的理解及其特點(diǎn)與應(yīng)用場景?

簡單談?wù)勀銓?duì) EnumMap 的理解及其特點(diǎn)與應(yīng)用場景?

簡單說說 EnumMap 的實(shí)現(xiàn)原理

http://www.lxweimin.com/p/d842893c4cb2
https://mp.weixin.qq.com/s?__biz=MzIxOTI1NTk5Nw==&mid=2650047360&idx=1&sn=129ffbc5b963b5d6a692aae595e2b402&chksm=8fde2652b8a9af446be044953353fc89ea69627e472969fc7b4e9f32d10565c80b584d785ff5&scene=21#wechat_redirect

說說 EnumSet 怎么用,其基本原理是什么

https://www.cnblogs.com/swiftma/p/6044718.html
https://blog.csdn.net/bolink5/article/details/4201384

簡單說說什么是 Deque 以及 ArrayDeque 與 LinkedList 的區(qū)別及特點(diǎn)

Deque 是一個(gè)雙端隊(duì)列接口,Deque 擴(kuò)展了 Queue,有隊(duì)列的所有方法,還可以看做棧,有棧的基本方法 push/pop/peek,還有明確的操作兩端的方法 addFirst/removeLast 等,主要如下:

//將指定元素插入雙向隊(duì)列開頭
        void addFirst (Object e );
        // 將指定元素插入雙向隊(duì)列末尾
        void addLast (Object e );
        // 返回對(duì)應(yīng)的迭代器,以逆向順序來迭代隊(duì)列中的元素
        Iterator descendingIterator ();
        // 獲取但不刪除雙向隊(duì)列的第一個(gè)元素
        Object getFirst ();
        // 獲取但不刪除雙向隊(duì)列的最后一個(gè)元素 
        Object getLast ();
        // 將指定元素插入雙向隊(duì)列開頭 
        boolean offerFirst (Object e );
        // 將指定元素插入雙向隊(duì)列結(jié)尾 
        boolean offerLast (Object e );
        // 獲取但不刪除雙向隊(duì)列的第一個(gè)元素,如果雙端隊(duì)列為空則返回 null 
        Object peekFirst ();
        // 獲取但不刪除雙向隊(duì)列的最后一個(gè)元素,如果此雙端隊(duì)列為空則返回 null 
        Object peekLast ();
        // 獲取并刪除雙向隊(duì)列的第一個(gè)元素,如果此雙端隊(duì)列為空則返回 null
        Object pollFirst ();
        // 獲取并刪除雙向隊(duì)列的最后一個(gè)元素,如果此雙端隊(duì)列為空則返 null 
        Object pollLast ();
        // 退棧出該雙向隊(duì)列中的第一個(gè)元素 
        Object pop ();
        // 將元素入棧進(jìn)雙向隊(duì)列棧中
        void push (Object e );
        // 獲取并刪除該雙向隊(duì)列的第一個(gè)元素 
        Object removeFirst ();
        // 刪除雙向隊(duì)列第一次出現(xiàn)的元素 e 
        Object removeFirstOccurrence (Object e );
        // 獲取并刪除雙向隊(duì)列的最后一個(gè)元素 
        removeLast();
        // 刪除雙向隊(duì)列最后一次出現(xiàn)的元素 e 
        removeLastOccurrence(Object e);

LinkedList 是一個(gè)比較奇怪的類,其即實(shí)現(xiàn)了 List 接口又實(shí)現(xiàn)了 Deque 接口(Deque 是 Queue 的子接口),而 LinkedList 的實(shí)現(xiàn)是基于雙向鏈表結(jié)構(gòu)的,其容量沒有限制,是非并發(fā)安全的隊(duì)列,所以不僅可以當(dāng)成列表使用,還可以當(dāng)做雙向隊(duì)列使用,同時(shí)也可以當(dāng)成棧使用(因?yàn)檫€實(shí)現(xiàn)了 pop 和 push 方法)。此外 LinkedList 的元素可以為 null 值。

ArrayDeque 是一個(gè)用數(shù)組實(shí)現(xiàn)的雙端隊(duì)列 Deque,為滿足可以同時(shí)在數(shù)組兩端插入或刪除元素的需求,該數(shù)組還必須是循環(huán)的,即循環(huán)數(shù)組(circular array),也就是說數(shù)組的任何一點(diǎn)都可能被看作起點(diǎn)或者終點(diǎn),ArrayDeque 是非線程安全的,當(dāng)多個(gè)線程同時(shí)使用的時(shí)候需要手動(dòng)同步,此外該容器不允許放 null 元素,同時(shí)與 ArrayList 和 LinkedList 不同的是沒有索引位置的概念,不能根據(jù)索引位置進(jìn)行操作。
http://www.lxweimin.com/p/d842893c4cb2

簡單說說 ArrayDeque 與 LinkedList 的適用場景?

ArrayDeque 和 LinkedList 都實(shí)現(xiàn)了 Deque 接口,如果只需要 Deque 接口且從兩端進(jìn)行操作則一般來說 ArrayDeque 效率更高一些,如果同時(shí)需要根據(jù)索引位置進(jìn)行操作或經(jīng)常需要在中間進(jìn)行插入和刪除操作則應(yīng)該優(yōu)先選 LinkedList 效率高些,因?yàn)?ArrayDeque 實(shí)現(xiàn)是循環(huán)數(shù)組結(jié)構(gòu)(即一個(gè)動(dòng)態(tài)擴(kuò)容數(shù)組,默認(rèn)容量 16,然后通過一個(gè) head 和 tail 索引記錄首尾相連),而 LinkedList 是基于雙向鏈表結(jié)構(gòu)的。

http://www.lxweimin.com/p/d842893c4cb2

簡單說說什么是 Queue 以及 PriorityQueue 與 LinkedList 的區(qū)別及特點(diǎn)?

首先 Queue 是一種模擬 FIFO 隊(duì)列的數(shù)據(jù)結(jié)構(gòu),新元素插入(offer)到隊(duì)列的尾部,訪問元素(poll)返回隊(duì)列頭部,一般隊(duì)列不允許隨機(jī)訪問隊(duì)列中的元素。Queue 接口主要定義了如下幾個(gè)方法:

//將指定元素加入隊(duì)列尾部 
        void add (Object e );
// 獲取隊(duì)列頭部元素,但是不刪除該元素,如果隊(duì)列為空拋出 NoSuchElementException 異常
        Object element ();
// 將指定元素加入隊(duì)列尾部(當(dāng)使用有容量限制的隊(duì)列時(shí)此方法比 add(Object e) 更好)
        boolean offer (Object e );
// 獲取隊(duì)列頭部元素,但是不刪除該元素,如果隊(duì)列為空返回 null 
        Object peek ();
// 獲取隊(duì)列頭部元素并刪除該元素,如果隊(duì)列為空則返回 null 
        Object poll ();
// 獲取隊(duì)列頭部元素并刪除該元素,如果隊(duì)列為空拋出 NoSuchElementException 異常 
        Object remove ();

LinkedList 是一個(gè)比較奇怪的類,其即實(shí)現(xiàn)了 List 接口又實(shí)現(xiàn)了 Deque 接口(Deque 是 Queue 的子接口),而 LinkedList 的實(shí)現(xiàn)是基于雙向鏈表結(jié)構(gòu)的,其容量沒有限制,是非并發(fā)安全的隊(duì)列,所以不僅可以當(dāng)成列表使用,還可以當(dāng)做雙向隊(duì)列使用,同時(shí)也可以當(dāng)成棧使用(因?yàn)檫€實(shí)現(xiàn)了 pop 和 push 方法)。此外 LinkedList 的元素可以為 null 值。

PriorityQueue 是一個(gè)優(yōu)先級(jí)列表隊(duì)列,因?yàn)?PriorityQueue 保存隊(duì)列元素的順序并不是按加入隊(duì)列的順序,而是按隊(duì)列元素的大小進(jìn)行重新排序,所以當(dāng)調(diào)用 peek 或者 pull 方法從隊(duì)列頭取元素時(shí)并不是取出最先進(jìn)入隊(duì)列的元素,而是取出隊(duì)列中最小的元素(默認(rèn)順序),所以說 PriorityQueue 實(shí)質(zhì)違反了 FIFO 隊(duì)列的基本原則,從而成了優(yōu)先級(jí)列表實(shí)現(xiàn),同時(shí) PriorityQueue 的實(shí)現(xiàn)是基于動(dòng)態(tài)擴(kuò)容數(shù)組的二叉樹堆結(jié)構(gòu),其最大容量長度為 Int 大小,是非并發(fā)安全的隊(duì)列。此外 PriorityQueue 的元素不可為 null 值。

PriorityQueue 是怎么確定哪一個(gè)元素的優(yōu)先級(jí)最高的?

PriorityQueue 確定最高優(yōu)先級(jí)元素使用的是堆數(shù)據(jù)結(jié)構(gòu),因?yàn)槎咽且豢猛耆珮洌阎心硞€(gè)節(jié)點(diǎn)的值總是不大于或不小于其父節(jié)點(diǎn)值的,常見的堆有二叉堆、斐波那契堆等,二叉堆是完全二叉樹或者是近似完全二叉樹的一種特殊堆,其分為最大堆和最小堆,最大堆中父結(jié)點(diǎn)值總是大于或等于任何一個(gè)子節(jié)點(diǎn)值,最小堆中父結(jié)點(diǎn)值總是小于或等于任何一個(gè)子節(jié)點(diǎn)值,由于二叉堆一直是自左向右自上向下一層層填充的,所以其可以用數(shù)組來表示而不是用鏈表,PriorityQueue 就是采用了基于動(dòng)態(tài)數(shù)組的二叉堆來確定優(yōu)先級(jí)。

談?wù)勀銓?duì)二叉堆數(shù)據(jù)結(jié)構(gòu)的理解及在 PriorityQueue 中的實(shí)現(xiàn)?

http://www.lxweimin.com/p/b308d23f3775

談你對(duì) ArrayDeque 主要方法實(shí)現(xiàn)原理的認(rèn)識(shí)?

http://www.lxweimin.com/p/5763d9c1c321

http://www.cnblogs.com/chengxiao/p/6059914.html
https://blog.csdn.net/qq_27093465/article/details/52269862
https://blog.csdn.net/justloveyou_/article/details/52464440
http://www.lxweimin.com/p/4d3cb99d7580
http://www.lxweimin.com/p/550cea8c25ef

nextTable 是新的兩倍數(shù)組
ForwardingNode

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評(píng)論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,615評(píng)論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,826評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,227評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,447評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,992評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,807評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,001評(píng)論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評(píng)論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,243評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評(píng)論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,709評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,996評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容