面試整理

數組的長度:    java

int[] input; 
inp.length;

字符串長度: node

String str; 
str.length();

鏈表的長度:    面試

List<String> li; 
li.size();

字符串的第N個字符:  數組

Character tmp = str.charAt(i);

用 toCharArray()方法把字符串轉成char數組,緩存

char chars[] = s.toCharArray();

完成後再用new String把char數組轉成字符串。安全

s = new String(chars);

 

java數組/Collection的尋址細節?數據結構

 

集合類存放的都是對象的引用,而非對象自己,出於表達上的便利,咱們稱集合中的對象就是指集合中對象的引用(reference)。多線程

 

ArrayList

  1. ArrayList 底層是一個動態擴容的數組結構併發

  2. 默認大小 DEFAULT_CAPACITY=10app

  3. 容許存放(不止一個) null 元素

  4. 容許存放重複數據,存儲順序按照元素的添加順序

  5. ArrayList 並非一個線程安全的集合。若是集合的增刪操做須要保證線程的安全性,能夠考慮使用 CopyOnWriteArrayList 或者使用 collections.synchronizedList(List l)函數返回一個線程安全的ArrayList類.

 

  1. 底層是數組,初始大小爲10
  2. 插入時會判斷數組容量是否足夠,不夠的話會進行擴容
  3. 所謂擴容就是新建一個新的數組,而後將老的數據裏面的元素複製到新的數組裏面
  4. 移除元素的時候也涉及到數組中元素的移動,刪除指定index位置的元素,而後將index+1至數組最後一個元素往前移動一個格
  5. 增刪擴容底層都是用System.arraycopy(),native方法,由jvm實現,效率高。

*在定義一個native method時,並不提供實現體(有些像定義一個java interface),由於其實現體是由非java語言在外面實現的。

擴容:

/**
 * 集合的最大長度 Integer.MAX_VALUE - 8 是爲了減小出錯的概率 Integer 最大值已經很大了
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * 增長容量,以確保它至少能容納最小容量參數指定的元素個數。
 * @param 知足條件的最小容量
 */
private void grow(int minCapacity) {
  //獲取當前 elementData 的大小,也就是 List 中當前的容量
   int oldCapacity = elementData.length;
   //oldCapacity >> 1 等價於 oldCapacity / 2  因此新容量爲當前容量的 1.5 倍
   int newCapacity = oldCapacity + (oldCapacity >> 1);
   //若是擴大1.5倍後仍舊比 minCapacity 小那麼直接等於 minCapacity
   if (newCapacity - minCapacity < 0)
       newCapacity = minCapacity;
    //若是新數組大小比  MAX_ARRAY_SIZE 就須要進一步比較 minCapacity 和 MAX_ARRAY_SIZE 的大小
   if (newCapacity - MAX_ARRAY_SIZE > 0)
       newCapacity = hugeCapacity(minCapacity);
   // minCapacity一般接近 size 大小
   //使用 Arrays.copyOf 構建一個長度爲 newCapacity 新數組 並將 elementData 指向新數組
   elementData = Arrays.copyOf(elementData, newCapacity);
}

/**
 * 比較 minCapacity 與 Integer.MAX_VALUE - 8 的大小若是大則放棄-8的設定,設置爲 Integer.MAX_VALUE 
 */
private static int hugeCapacity(int minCapacity) {
   if (minCapacity < 0) // overflow
       throw new OutOfMemoryError();
   return (minCapacity > MAX_ARRAY_SIZE) ?
       Integer.MAX_VALUE :
       MAX_ARRAY_SIZE;
}

由此看來 ArrayList 的擴容機制的知識點一共又兩個

  1. 每次擴容的大小爲原來大小的 1.5倍 (固然這裏沒有包含 1.5倍後大於 MAX_ARRAY_SIZE 的狀況)
  2. 擴容的過程實際上是一個將原來元素拷貝到一個擴容後數組大小的長度新數組中。因此 ArrayList 的擴容實際上是相對來講比較消耗性能的。

 

  •  ArrayList 中調用 iterator() 將會返回一個內部類對象 Itr 其實現了 Iterator 接口。
  • ListItr對象繼承自前邊分析的 Itr,也就是說他擁有 Itr 的全部方法,並在此基礎上進行擴展,其擴展了訪問當前角標前一個元素的方法。以及在遍歷過程當中添加元素和修改元素的方法previous 

modCount 變量用於標記當前集合被修改(增刪)的次數,若是併發訪問了集合那麼將會致使這個 modCount 的變化,在遍歷過程當中不正確的操做集合將會拋出 ConcurrentModificationException ,這是 Java 「fast-fail 的機制」。
 

modCount 這個變量主要用來記錄 ArrayList 被修改的次數,那麼爲何要記錄這個次數呢?是爲了防止多線程對同一集合進行修改產生錯誤,記錄了這個變量,在對 ArrayList 進行迭代的過程當中咱們能很快的發現這個變量是否被修改過,若是被修改了 ConcurrentModificationException 將會產生。下面咱們來看下例子,這個例子並非在多線程下的,而是由於咱們在同一線程中對 list 進行了錯誤操做致使的:

Iterator<SubClass> iterator = lists.iterator();

while (iterator.hasNext()) {
  SubClass next = iterator.next();
  int index = next.test;
  if (index == 3) {
      list2.remove(index);//操做1: 注意是 list2.remove 操做
      //iterator.remove();/操做2 注意是 iterator.remove 操做
  }
}
//操做1: Exception in thread "main" java.util.ConcurrentModificationException
//操做2:  [SubClass{test=1}, SubClass{test=2}]
System.out.println(list2);
複製代碼

咱們對操做1,2分別運行程序,能夠看到,操做1很快就拋出了 java.util.ConcurrentModificationException 異常,操做2 則順利運行出正常結果,若是對 modCount 注意了的話,咱們很容易理解,list.remove(index) 操做會修改List 的 modCount,而 iterator.next() 內部每次會檢驗 expectedModCount != modCount,因此當咱們使用 list.remove 下一次再調用 iterator.next() 就會報錯了,而iterator.remove爲何是安全的呢?由於其操做內部會在調用 list.remove 後從新將新的 modCount 賦值給 expectedModCount。因此咱們直接調用 list.remove 操做是錯誤的。

 

Fail-fast

在線程不安全的集合中,若是使用迭代器的過程當中,發現集合被修改,會拋出ConcurrentModificationExceptions錯誤,這就是fail-fast機制。對集合進行結構性修改時,modCount都會增長,在初始化迭代器時,modCount的值會賦給expectedModCount,在迭代的過程當中,只要modCount改變了,int expectedModCount = modCount等式就不成立了,迭代器檢測到這一點,就會拋出錯誤:currentModificationExceptions。

 

看看爲何說ArrayList查詢快而增刪慢?

        支持random access,下角標直接定位元素;可是修改時候須要對後續元素都進行移動

CopyOnWriteArrayList爲何併發安全且性能比Vector好

我知道Vector是增刪改查方法都加了synchronized,保證同步,可是每一個方法執行的時候都要去得到鎖,性能就會大大降低,而CopyOnWriteArrayList 只是在增刪改上加鎖,可是讀不加鎖,在讀方面的性能就好於Vector,CopyOnWriteArrayList支持讀多寫少的併發狀況。

 

CopyOnWriteArrayList 幾個要點

  • 實現了List接口

  • 內部持有一個ReentrantLock lock = new ReentrantLock();

  • 底層是用volatile transient聲明的數組 array

  • 讀寫分離,寫時複製出一個新的數組,完成插入、修改或者移除操做後將新數組賦值給array

 

Vector 介紹

Vector 是一個至關古老的 Java 容器類,始於 JDK 1.0,並在 JDK 1.2 時代對其進行修改,使其實現了 List 和 Collection 。從做用上來看,Vector 和 ArrayList 很類似,都是內部維護了一個能夠動態變換長度的數組。可是他們的擴容機制卻不相同。對於 Vector 的源碼大部分都和 ArrayList 差很少,這裏簡單看下 Vector 的構造函數,以及 Vector 的擴容機制。

Vector 的構造函數能夠指定內部數組的初始容量和擴容係數,若是不指定初始容量默認初始容量爲 10,可是不一樣於 ArrayList 的是它在建立的時候就分配了容量爲10的內存空間,而 ArrayList 則是在第一次調用 add 的時候才生成一個容量爲 10 數組。

Vector 的須要擴容的時候,首先會判斷 capacityIncrement 即在構造的 Vector 的時候時候指定了擴容係數,若是指定了則按照指定的係數來擴大容量,擴大後新的容量爲 oldCapacity + capacityIncrement,若是沒有指定capacityIncrement的大小,則默認擴大原來容量的一倍,這點不一樣於 ArrayList 的 0.5 倍長度。

對於 Vector 與 ArrayList 的區別最重要的一點是 Vector全部的訪問內部數組的方法都帶有synchronized ,這意味着 Vector 是線程安全的,而ArrayList 並無這樣的特性。

 

Vector 與 ArrayList 的比較

  1. Vector 與 ArrayList 底層都是數組數據結構,都維護着一個動態長度的數組。

  2. Vector 對擴容機制在沒有經過構造指定擴大系數的時候,默認增加現有數組長度的一倍。而 ArrayList 則是擴大現有數組長度的一半長度。

  3. Vector 是線程安全的, 而 ArrayList 不是線程安全的,在不涉及多線程操做的時候 ArrayList 要比 Vector效率高

  4. 對於 Vector 而言,除了 for 循環,高級 for 循環,迭代器的迭代方法外,還能夠調用 elements() 返回一個 Enumeration 來遍歷內部元素。

 

LinkedList 和 ArrayList 的區別:

  1. ArrayList 是底層採用數組結構,存儲空間是連續的。查詢快,增刪須要進行數組元素拷貝過程,當刪除元素位置比較靠前的時候性能較低。

  2. LinkedList 底層是採用雙向鏈表數據結構,每一個節點都包含本身的前一個節點和後一個節點的信息,存儲空間能夠不是連續的。增刪塊,查詢慢。

  3. ArrayList 和 LinkedList 都是線程不安全的。而 Vector 是線程安全的

  4. 儘可能不要使用 for 循環去遍歷一個LinkedList集合,而是用迭代器或者高級 for。

 

Stack

棧(stack)又名堆棧,它是一種運算受限的線性表。其限制是僅容許在表的一端進行插入和刪除運算。這一端被稱爲棧頂,相對地,把另外一端稱爲棧底。Stack 繼承自 Vector,也就是 Stack 擁有 Vector 全部的增刪改查方法。

通常來講對於棧有一下幾種操做:

  1. push 入棧
  2. pop 出棧
  3. peek 查詢棧頂
  4. empty 棧是否爲空

我以爲我是面試官,若是回答者只寫出了出棧入棧的操做方法應該算是不及格的,面試官關注的應該是在寫 push 操做的時候有沒有考慮過 StackOverFlow 也就是棧滿的狀況。

public class SimpleStack<E> {
    //默認容量
    private static final int DEFAULT_CAPACITY = 10;
    //棧中存放元素的數組
    private Object[] elements;
    //棧中元素的個數
    private int size = 0;
    //棧頂指針
    private int top;


    public SimpleStack() {
        this(DEFAULT_CAPACITY);
    }

    public SimpleStack(int initialCapacity) {
        elements = new Object[initialCapacity];
        top = -1;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public int size() {
        return size;
    }

    @SuppressWarnings("unchecked")
    public E pop() throws Exception {
        if (isEmpty()) {
            throw new EmptyStackException();
        }

        E element = (E) elements[top];
        elements[top--] = null;
        size--;
        return element;
    }

    @SuppressWarnings("unchecked")
    public E peek() throws Exception {
        if (isEmpty()) {
            throw new Exception("當前棧爲空");
        }
        return (E) elements[top];
    }

    public void push(E element) throws Exception {
        //添加以前確保容量是否知足條件
        ensureCapacity(size + 1);
        elements[size++] = element;
        top++;
    }

    private void ensureCapacity(int minSize) {
        if (minSize - elements.length > 0) {
            grow();
        }
    }

    private void grow() {
        int oldLength = elements.length;
        // 更新容量操做 擴充爲原來的1.5倍 這裏也能夠選擇其餘方案
        int newLength = oldLength + (oldLength >> 1);
        elements = Arrays.copyOf(elements, newLength);
    }
}

同步 vs 非同步

對於 Vector 和 Stack 從源碼上他們在對應的增刪改查方法上都使用 synchronized關鍵字修飾了方法,這也就表明這個方法是同步方法,線程安全的。而 ArrayList 和 LinkedList 並非線程安全的。不過咱們在介紹 ArrayList和 LinkedList 的時候說起到了咱們可使用Collections 的靜態方法,將一個 List 轉化爲線程同步的 List

List<Integer> synchronizedArrayList = Collections.synchronizedList(arrayList);
List<Integer> synchronizedLinkedList = Collections.synchronizedList(linkedList);
複製代碼

那麼這裏又有一道面試題是這樣問的:

請簡述一下 Vector 和 SynchronizedList 區別,

SynchronizedList與 Vector的三點差別:

  1. SynchronizedList 做爲一個包裝類,有很好的擴展和兼容功能。能夠將全部的 List 的子類轉成線程安全的類。
  2. 使用 SynchronizedList 的獲取迭代器,進行遍歷時要手動進行同步處理,而 Vector 不須要。
  3. SynchronizedList 能夠經過參數指定鎖定的對象,而 Vector 只能是對象自己。

 

 

equals 與 == 操做符的區別總結以下:

  1. 若 == 兩側都是基本數據類型,則判斷的是左右兩邊操做數據的值是否相等

  2. 若 == 兩側都是引用數據類型,則判斷的是左右兩邊操做數的內存地址是否相同。若此時返回 true , 則該操做符做用的必定是同一個對象。

  3. Object 基類的 equals 默認比較兩個對象的內存地址,在構建的對象沒有重寫 equals 方法的時候,與 == 操做符比較的結果相同。

  4. equals 用於比較引用數據類型是否相等。在知足equals 判斷規則的前體系,兩個對象只要規定的屬性相同咱們就認爲兩個對象是相同的。

關於equals和hashCode

  1. hashCode 返回值不必定對象的存儲地址,好比發生哈希碰撞的時候。
  2. 調用 equals 返回 true 的兩個對象必須具備相等的哈希碼。
  3. 若是兩個對象的 hashCode 返回值相同,調用它們 equals 方法不一返回 true 。

 

Hashtable

Hashtable是java一開始發佈時就提供的鍵值映射的數據結構,而HashMap產生於JDK1.2。雖然Hashtable比HashMap出現的早一些,可是如今Hashtable基本上已經被棄用了。而HashMap已經成爲應用最爲普遍的一種數據類型了。形成這樣的緣由一方面是由於Hashtable是線程安全的,效率比較低。另外一方面多是由於Hashtable沒有遵循駝峯命名法吧。。。

繼承的父類不一樣 
HashMap和Hashtable不只做者不一樣,並且連父類也是不同的。HashMap是繼承自AbstractMap類,而HashTable是繼承自Dictionary類。不過它們都實現了同時實現了map、Cloneable(可複製)、Serializable(可序列化)這三個接口

Dictionary類是一個已經被廢棄的類(見其源碼中的註釋)。父類都被廢棄,天然而然也沒人用它的子類Hashtable了。

對外提供的接口不一樣 
Hashtable比HashMap多提供了elments() 和contains() 兩個方法。

elments() 方法繼承自Hashtable的父類Dictionnary。elements() 方法用於返回此Hashtable中的value的枚舉。

contains()方法判斷該Hashtable是否包含傳入的value。它的做用與containsValue()一致。事實上,contansValue() 就只是調用了一下contains() 方法。
對Null key 和Null value的支持不一樣 
Hashtable既不支持Null key也不支持Null value。Hashtable的put()方法的註釋中有說明。 

線程安全性不一樣 
Hashtable是線程安全的,它的每一個方法中都加入了Synchronize方法。在多線程併發的環境下,能夠直接使用Hashtable,不須要本身爲它的方法實現同步

HashMap不是線程安全的,在多線程併發的環境下,可能會產生死鎖等問題。具體的緣由在下一篇文章中會詳細進行分析。使用HashMap時就必需要本身增長同步處理,

雖然HashMap不是線程安全的,可是它的效率會比Hashtable要好不少。這樣設計是合理的。在咱們的平常使用當中,大部分時間是單線程操做的。HashMap把這部分操做解放出來了。當須要多線程操做的時候可使用線程安全的ConcurrentHashMap。ConcurrentHashMap雖然也是線程安全的,可是它的效率比Hashtable要高好多倍。由於ConcurrentHashMap使用了分段鎖,並不對整個數據進行鎖定。

遍歷方式的內部實現上不一樣 
Hashtable、HashMap都使用了 Iterator。而因爲歷史緣由,Hashtable還使用了Enumeration的方式 。

HashMap的Iterator是fail-fast迭代器。當有其它線程改變了HashMap的結構(增長,刪除,修改元素),將會拋出ConcurrentModificationException。不過,經過Iterator的remove()方法移除元素則不會拋出ConcurrentModificationException異常。但這並非一個必定發生的行爲,要看JVM。

JDK8以前的版本中,Hashtable是沒有fast-fail機制的。在JDK8及之後的版本中 ,HashTable也是使用fast-fail的, 源碼以下: 

計算hash值的方法不一樣 
爲了獲得元素的位置,首先須要根據元素的 KEY計算出一個hash值,而後再用這個hash值來計算獲得最終的位置。

Hashtable直接使用對象的hashCode。hashCode是JDK根據對象的地址或者字符串或者數字算出來的int類型的數值。而後再使用除留餘數發來得到最終的位置。 
Hashtable在計算元素的位置時須要進行一次除法運算,而除法運算是比較耗時的。 
HashMap爲了提升計算效率,將哈希表的大小固定爲了2的冪,這樣在取模預算時,不須要作除法,只須要作位運算。位運算比除法的效率要高不少。

HashMap的效率雖然提升了,可是hash衝突卻也增長了。由於它得出的hash值的低位相同的機率比較高,而計算位運算

爲了解決這個問題,HashMap從新根據hashcode計算hash值後,又對hash值作了一些運算來打散數據。使得取得的位置更加分散,從而減小了hash衝突。固然了,爲了高效,HashMap只作了一些簡單的位處理。從而不至於把使用2 的冪次方帶來的效率提高給抵消掉。
 

 

Map

  • 不能包括兩個相同的鍵,一個鍵最多能綁定一個值。
  • null能夠做爲鍵,這樣的鍵只有一個
  • 能夠有一個或多個鍵所對應的值爲null。當get()方法返回null值時,便可以表示Map中沒有該鍵,也能夠表示該鍵所對應的值爲null。所以,在Map中不能由get()方法來判斷Map中是否存在某個鍵,而應該用containsKey()方法來判斷。

 

概述

爲了方便下邊的敘述這裏須要先對幾個常見的關於 HashMap 的知識點進行下概述:

  1. HashMap 存儲數據是根據鍵值對存儲數據的,而且存儲多個數據時,數據的鍵不能相同,若是相同該鍵以前對應的值將被覆蓋。注意若是想要保證 HashMap 可以正確的存儲數據,請確保做爲鍵的類,已經正確覆寫了 equals() 方法。

  2. HashMap 存儲數據的位置與添加數據的鍵的 hashCode() 返回值有關。因此在將元素使用 HashMap 存儲的時候請確保你已經按照要求重寫了 hashCode()方法。這裏說有關係表明最終的存儲位置不必定就是 hashCode 的返回值。

  3. HashMap 最多隻容許一條存儲數據的鍵爲 null,可容許多條數據的值爲 null。

  4. HashMap 存儲數據的順序是不肯定的,而且可能會由於擴容致使元素存儲位置改變。所以遍歷順序是不肯定的。

  5. HashMap 是線程不安全的,若是須要再多線程的狀況下使用能夠用 Collections.synchronizedMap(Map map) 方法使 HashMap 具備線程安全的能力,或者使用 ConcurrentHashMap

JDK1.8中的 HashMap 存儲結構。

對於 JDK1.8 以後的HashMap底層在解決哈希衝突的時候,就不僅僅是使用數組加上單鏈表的組合了,由於當處理若是 hash 值衝突較多的狀況下,鏈表的長度就會愈來愈長,此時經過單鏈表來尋找對應 Key 對應的 Value 的時候就會使得時間複雜度達到 O(n),所以在 JDK1.8 以後,在鏈表新增節點致使鏈表長度超過 TREEIFY_THRESHOLD = 8 的時候,就會在添加元素的同時將原來的單鏈錶轉化爲紅黑樹。

對數據結構很在行的讀者應該,知道紅黑樹是一種易於增刪改查的二叉樹,他對與數據的查詢的時間複雜度是 O(logn) 級別,因此利用紅黑樹的特色就能夠更高效的對 HashMap 中的元素進行操做。

 

 

關於 HashMap 源碼中分析的文章通常都會說起幾個重要的概念:

重要參數

  1. 哈希桶(buckets):在 HashMap 的註釋裏使用哈希桶來形象的表示數組中每一個地址位置。注意這裏並非數組自己,數組是裝哈希桶的,他能夠被稱爲哈希表

  2. 初始容量(initial capacity) : 這個很容易理解,就是哈希表中哈希桶初始的數量。若是咱們沒有經過構造方法修改這個容量值默認爲DEFAULT_INITIAL_CAPACITY = 1<<4 即16。值得注意的是爲了保證 HashMap 添加和查找的高效性,HashMap 的容量老是 2^n 的形式。

  3. 加載因子(load factor):加載因子是哈希表(散列表)在其容量自動增長以前被容許得到的最大數量的度量。當哈希表中的條目數量超過負載因子和當前容量的乘積時,散列表就會被從新映射(即重建內部數據結構),從新建立的散列表容量大約是以前散列表哈系統桶數量的兩倍。默認加載因子(0.75)在時間和空間成本之間提供了良好的折衷。加載因子過大會致使很容易鏈表過長,加載因子很小又容易致使頻繁的擴容。因此不要輕易試着去改變這個默認值

  4. 擴容閾值(threshold):其實在說加載因子的時候已經提到了擴容閾值了,擴容閾值 = 哈希表容量 * 加載因子。哈希表的鍵值對總數 = 全部哈希桶中全部鏈表節點數的加和,擴容閾值比較的是是鍵值對的個數而不是哈希表的數組中有多少個位置被佔了。

  5. 樹化閥值(TREEIFY_THRESHOLD) :這個參數概念是在 JDK1.8後加入的,它的含義表明一個哈希桶中的節點個數大於該值(默認爲8)的時候將會被轉爲紅黑樹行存儲結構。

  6. 非樹化閥值(UNTREEIFY_THRESHOLD): 與樹化閾值相對應,表示當一個已經轉化爲數形存儲結構的哈希桶中節點數量小於該值(默認爲 6)的時候將再次改成單鏈表的格式存儲。致使這種操做的緣由可能有刪除節點或者擴容。

  7. 最小樹化容量(MIN_TREEIFY_CAPACITY): 通過上邊的介紹咱們只知道,當鏈表的節點數超過8的時候就會轉化爲樹化存儲,其實對於轉化還有一個要求就是哈希表的數量超過最小樹化容量的要求(默認要求是 64),且爲了不進行擴容、樹形化選擇的衝突,這個值不能小於 4 * TREEIFY_THRESHOLD);在達到該有求以前優先選擇擴容。擴容由於由於容量的變化可能會使單鏈表的長度改變。

JDK1.8 中 hash 函數的實現

JDK1.8中再次優化了這個哈希函數,把 key 的 hashCode 方法返回值右移16位,即丟棄低16位,高16位全爲0 ,而後在於 hashCode 返回值作異或運算,即高 16 位與低 16 位進行異或運算,這麼作能夠在數組 table 的 length 比較小的時候,也能保證考慮到高低Bit都參與到 hash 的計算中,同時不會有太大的開銷,擾動處理次數也從 4次位運算 + 5次異或運算 下降到 1次位運算 + 1次異或運算

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

進過上述的擾動函數只是獲得了合適的 hash 值,可是尚未肯定在 Node[] 數組中的角標,在 JDK1.7中存在一個函數,JDK1.8中雖然沒有可是隻是把這步運算放到了 put 函數中。咱們就看下這個函數實現:

static int indexFor(int h, int length) {
     return h & (length-1);  // 取模運算
}

爲了讓 hash 值可以對應到現有數組中的位置,咱們上篇文章講到一個方法爲 取模運算,即 hash % length,獲得結果做爲角標位置。可是 HashMap 就厲害了,連這一步取模運算的都優化了。咱們須要知道一個計算機對於2進制的運算是要快於10進制的,取模算是10進制的運算了,而位與運算就要更高效一些了。

咱們知道 HashMap 底層數組的長度老是 2^n ,轉爲二進制老是 1000 即1後邊多個0的狀況。此時一個數與 2^n 取模,等價於 一個數與 2^n - 1作位與運算。而 JDK 中就使用h & (length-1) 運算替代了對 length取模。

 

爲何HashMap中鏈表長度超過8會轉換成紅黑樹

HashMap在jdk1.8以後引入了紅黑樹的概念,表示若桶中鏈表元素超過8時,會自動轉化成紅黑樹;若桶中元素小於等於6時,樹結構還原成鏈表形式。

緣由:

  紅黑樹的平均查找長度是log(n),長度爲8,查找長度爲log(8)=3,鏈表的平均查找長度爲n/2,當長度爲8時,平均查找長度爲8/2=4,這纔有轉換成樹的必要;鏈表長度若是是小於等於6,6/2=3,雖然速度也很快的,可是轉化爲樹結構和生成樹的時間並不會過短。

還有選擇6和8的緣由是:

  中間有個差值7能夠防止鏈表和樹之間頻繁的轉換。假設一下,若是設計成鏈表個數超過8則鏈表轉換成樹結構,鏈表個數小於8則樹結構轉換成鏈表,若是一個HashMap不停的插入、刪除元素,鏈表個數在8左右徘徊,就會頻繁的發生樹轉鏈表、鏈表轉樹,效率會很低。

 

添加元素過程:

  1. 若是 Node[] table 表爲 null ,則表示是第一次添加元素,講構造函數也提到了,及時構造函數指定了指望初始容量,在第一次添加元素的時候也爲空。這時候須要進行首次擴容過程。
  2. 計算對應的鍵值對在 table 表中的索引位置,經過i = (n - 1) & hash 得到。
  3. 判斷索引位置是否有元素若是沒有元素則直接插入到數組中。若是有元素且key 相同,則覆蓋 value 值,這裏判斷是用的 equals 這就表示要正確的存儲元素,就必須按照業務要求覆寫 key 的 equals 方法,上篇文章咱們也說起到了該方法重要性。
  4. 若是索引位置的 key 不相同,則須要遍歷單鏈表,若是遍歷過若是有與 key 相同的節點,則保存索引,替換 Value;若是沒有相同節點,則在但單鏈表尾部插入新節點。這裏操做與1.7不一樣,1.7新來的節點老是在數組索引位置,而以前的元素做爲下個節點拼接到新節點尾部。
  5. 若是插入節點後鏈表的長度大於樹化閾值,則須要將單鏈錶轉爲紅黑樹。
  6. 成功插入節點後,判斷鍵值對個數是否大於擴容閾值,若是大於了則須要再次擴容。至此整個插入元素過程結束。

相信你們看到擴容的整個函數後對擴容機制應該有所瞭解了,總體分爲兩部分:1. 尋找擴容後數組的大小以及新的擴容閾值,2. 將原有哈希表拷貝到新的哈希表中

第一部分沒的說,可是第二部分我看的有點懵逼了,可是踩在巨人的肩膀上老是比較容易的,美團的大佬們早就寫過一些有關 HashMap 的源碼分析文章,給了我很大的幫助。在文章的最後我會放出參考連接。下面說下個人理解:

JDK 1.8 不像 JDK1.7中會從新計算每一個節點在新哈希表中的位置,而是經過 (e.hash & oldCap) == 0是否等於0 就能夠得出原來鏈表中的節點在新哈希表的位置。爲何能夠這樣高效的得出新位置呢?

由於擴容是容量翻倍,因此原鏈表上的每一個節點,可能存放新哈希表中在原來的下標位置, 或者擴容後的原位置偏移量爲 oldCap 的位置上,下邊舉個例子

圖(a)表示擴容前的key1和key2兩種key肯定索引位置的示例,圖(b)表示擴容後key1和key2兩種key肯定索引位置的示例,其中hash1是key1對應的哈希與高位運算結果。

 

 

元素在從新計算hash以後,由於n變爲2倍,那麼n-1的mask範圍在高位多1bit(紅色),所以新的index就會發生這樣的變化:

 

 

因此在 JDK1.8 中擴容後,只須要看看原來的hash值新增的那個bit是1仍是0就行了,是0的話索引沒變,是1的話索引變成「原索引+oldCap

另外還須要注意的一點是 HashMap 在 1.7的時候擴容後,鏈表的節點順序會倒置,1.8則不會出現這種狀況。

Map的迭代器都是經過,遍歷 table 表來獲取下個節點,來遍歷的,遍歷過程能夠理解爲一種深度優先遍歷,即優先遍歷鏈表節點(或者紅黑樹),而後在遍歷其餘數組位置。

 

HashTable 的區別

面試的時候面試官老是問完 HashMap 後會問 HashTable 其實 HashTable 也算是比較古老的類了。翻看 HashTable 的源碼能夠發現有以下區別:

  1. HashMap 是線程不安全的,HashTable是線程安全的。

  2. HashMap 容許 key 和 Vale 是 null,可是隻容許一個 key 爲 null,且這個元素存放在哈希表 0 角標位置。 HashTable 不容許key、value 是 null

  3. HashMap 內部使用hash(Object key)擾動函數對 key 的 hashCode 進行擾動後做爲 hash 值。HashTable 是直接使用 key 的 hashCode() 返回值做爲 hash 值。

  4. HashMap默認容量爲 2^4 且容量必定是 2^n ; HashTable 默認容量是11,不必定是 2^n

  5. HashTable 取哈希桶下標是直接用模運算,擴容時新容量是原來的2倍+1。HashMap 在擴容的時候是原來的兩倍,且哈希桶的下標使用 &運算代替了取模。

 

HashMap的常見面試題

1.何時會使用HashMap?他有什麼特色?

  • 基於Map接口實現的Key-Value容器,容許null值,同時非有序,非同步。

2.你知道HashMap的工做原理嗎?

  • 參見概括
  • 在Java 8中,若是一個bucket中碰撞衝突的元素超過某個限制(默認是8),則使用紅黑樹來替換鏈表,從而提升速度

3.你知道get和put的原理嗎?equals()和hashCode()的都有什麼做用?

  • 經過對key的hashCode()進行hashing,並計算下標( n-1 & hash),從而得到buckets的位置。若是產生碰撞,則利用key.equals()方法去鏈表或樹中去查找對應的節點

4.你知道hash的實現嗎?爲何要這樣實現?

  • 在Java 1.8的實現中,是經過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這麼作能夠在bucket的n比較小的時候,也能保證考慮到高低bit都參與到hash的計算中,同時不會有太大的開銷。
  • 使用hash還有一個好處就是 儘量確保每一個鏈表中的長度一致

5. 若是HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?

  • 若是超過了負載因子(默認0.75),則會從新resize一個原來長度兩倍的HashMap,而且從新調用hash方法;同時此時極可能出現一系列問題:參見問題6

6. 你瞭解從新調整HashMap大小存在什麼問題嗎?

  • 當數據過多時,極可能出現性能瓶頸(包括rehash時間) 
    使用HashMap時必定保證數量有限
  • 多線程狀況下可能產生條件競競爭從而形成死循環(具體表如今CPU接近100%)。多線程同時試着調整大小,可能致使存儲在鏈表中的元素的次序顛倒,由於移動到新的bucket位置的時候,HashMap並不會將元素放在鏈表的尾部,而是放在頭部,這是爲了不尾部遍歷。具體死循環代碼參見transfer(newTable) (線程1在遍歷,nodeA.next -> nodeB ,此時線程2移動了位置,且有倒置,致使nodeB.next ->nodeA ?)
  • 多線程環境下推薦使用ConcurrentHashMap

transfer方法

/**
  * Transfers all entries from current table to newTable.
  */
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    //注意:若是在新表的數組索引位置相同,則鏈表元素會倒置,也就是先插入最近的
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                //從新計算hash null的位置仍是tab[0]不變
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //從新計算下標索引(主要是由於容量變化成2倍)
            int i = indexFor(e.hash, newCapacity);
            //注意:多線程環境可能因爲執行次序非有序形成next引用變動賦值出錯致使環形連接出現,從而形成死循環
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

7. 爲何String, Interger這樣的wrapper類適合做爲鍵?

  • class具備final屬性,同時重寫equals()和hashCode()
  • hashCode變更會致使讀取失效
  • final同時保證線程安全 
    對象推薦重寫equals和hashCode方法,主要用於Map存取時的對比,同時有利於減小碰撞

8.咱們可使用自定義的對象做爲鍵嗎?

  • 這是前一個問題的延伸。固然你可能使用任何對象做爲鍵,只要它遵照了equals()和hashCode()方法的定義規則,而且當對象插入到Map中以後將不會再改變了。若是這個自定義對象時不可變的,那麼它已經知足了做爲鍵的條件,由於當它建立以後就已經不能改變了
  • 典型實例就是ThreadLocal,讀者可參見筆者的 併發番@ThreadLocal一文通(1.7版)

9.如何對HashMap進行排序?

  • 轉換:Map -> Set -> LinkedList(存key)
  • 排序:LinkedList自行sort
  • 存儲:存入有序LinkedHashMap

10.HashMap的remove陷阱?

  • 經過Iterator方式可正確遍歷完成remove操做
  • 直接調用list的remove方法就會拋異常

11.爲何只容許經過iterator進行remove操做?

  • HashMap和keySet的remove方法均可以經過傳遞key參數刪除任意的元素
  • 而iterator只能刪除當前元素(current),一旦刪除的元素是iterator對象中next所正在引用的,若是沒有經過modCount、 expectedModCount的比較實現快速失敗拋出異常,下次循環該元素將成爲current指向,此時iterator就遍歷了一個已移除的過時數據
  • 之因此推薦迭代器remove的根本緣由在於只有迭代器的remove方法中實現了變動時於modCount的同步工做 
    expectedModCount = modCount;

12.若是是遍歷過程當中增長或修改數據呢?

  • 增長或修改數據只能經過Map的put方法實現,在遍歷過程當中修改數據能夠,但若是增長新key就會在下次循環時拋異常,由於在添加新key時modCount也會自增(迭代器只實現了remove方法也是緣由之一)

 

LinkedHashMap

LinkedHashMap 繼承自 HashMap,在 HashMap 基礎上,經過維護一條雙向鏈表,解決了 HashMap 不能隨時保持遍歷順序和插入順序一致的問題。除此以外,LinkedHashMap 對訪問順序也提供了相關支持。在一些場景下,該特性頗有用,好比緩存。在實現上,LinkedHashMap 不少方法直接繼承自 HashMap,僅爲維護雙向鏈表覆寫了部分方法。

 

LinkedHashMap 在上面結構的基礎上,增長了一條雙向鏈表,使得上面的結構能夠保持鍵值對的插入順序。同時經過對鏈表進行相應的操做,實現了訪問順序相關邏輯。其結構可能以下圖:

上圖中,淡藍色的箭頭表示前驅引用,紅色箭頭表示後繼引用。每當有新鍵值對節點插入,新節點最終會接在 tail 引用指向的節點後面。而 tail 引用則會移動到新的節點上,這樣一個雙向鏈表就創建起來了。

 

LinkedHashMap 插入操做的調用過程。以下:

我把 newNode 方法紅色背景標註了出來,這一步比較關鍵。LinkedHashMap 覆寫了該方法。在這個方法中,LinkedHashMap 建立了 Entry,並經過 linkNodeLast 方法將 Entry 接在雙向鏈表的尾部,實現了雙向鏈表的創建。

 

刪除的過程並不複雜,上面這麼多代碼其實就作了三件事:

  1. 根據 hash 定位到桶位置
  2. 遍歷鏈表或調用紅黑樹相關的刪除方法
  3. 從 LinkedHashMap 維護的雙鏈表中移除要刪除的節點

舉個例子說明一下,假如咱們要刪除下圖鍵值爲 3 的節點。

根據 hash 定位到該節點屬於3號桶,而後在對3號桶保存的單鏈表進行遍歷。找到要刪除的節點後,先從單鏈表中移除該節點。以下:

而後再雙向鏈表中移除該節點:

相關文章
相關標籤/搜索