1.邏輯 邏輯判斷:對於一件事情正確與否的判斷,python中用布爾類型真(True)、假(False)做區分; 根據判斷結果的不同去完成的不同操作,就是我們的業務邏輯; 對於條件是否滿足的判斷語句,就是條件語句; 一個邏輯語句是由條件語句+業務語句組成的。 2.if語句 判斷一個命題的真實性,如果 ...
在Java開發中,我們經常會像如下方式以下創建一個HashMap:
Map<String, String> map = new HashMap<String, String>();
但是上面的代碼中,我們並沒有給HashMap指定容量,那麼,這時候一個新創建的HashMap的預設容量是多少呢?為什麼呢?
一、什麼是容量?
在Java中,保存數據有兩種比較簡單的數據結構:數組和鏈表。HashMap底層就是數組+鏈表(jdk1.8後底層是數組+鏈表+紅黑樹)。 在HashMap中,有兩個比較容易混淆的關鍵欄位:size和capacity ,這其中capacity就是Map的容量,而size我們稱之為Map中的元素個數。 簡單打個比方就更容易理解了:HashMap就是一個“桶”,那麼容量(capacity)就是這個桶當前最多可以裝多少元素--也就是數組的長度,而元素個數(size)表示這個桶已經裝了多少元素。 可以用以下代碼驗證一下:Map<String, String> map = new HashMap<String, String>();
map.put("hello", "Java");
map.put("hell", "Java");
map.put("hel", "Java");
map.put("he", "Java");
/*
* 通過反射獲取
*/
Class<?> mapType = map.getClass();
Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity : " + capacity.invoke(map));
Field size = mapType.getDeclaredField("size");
size.setAccessible(true);
System.out.println("size : " + size.get(map));
通過上面的代碼,我們發現了,當我們創建一個HashMap的時候,如果沒有指定其容量,那麼會得到一個預設容量為16的Map,那麼,這個容量是怎麼來的呢?又為什麼是這個數字呢?
實際上這個數字與hash有一定的關係,下麵我們看一下hash。
二、什麼是hash?
我們知道,容量就是一個HashMap中”桶”的個數,那麼,當我們想要往一個HashMap中put一個元素的時候,需要通過一定的演算法計算出應該把他放到哪個桶中,這個過程就叫做hash。 hash的功能是根據key來確定這個key-value對在鏈表數組中的下標的。 那麼這個數組下標該怎麼確定呢?其實簡單,我們只要在hash方法中調用Object中的hashCode方法得到一個hash值,然後用這個值對HashMap的容量進行取模就可以得到數組下標了。 如果真的是這麼簡單的話,那HashMap的容量設置也就很簡單,但是考慮到效率等問題,HashMap的hash實現被設計的比較複雜,這也造成了HashMap的容量設置有一定限制。 下麵我們看一下hash的實現。三、hash的實現
hash的具體實現上,由兩個方法int hash(Object k)和int indexFor(int h, int length)(在jdk1.8後沒有這個方法了)來實現。int hash(Object k):該方法主要是將Object轉換成一個整型數。 int indexFor(int h, int length):該方法主要是將hash()生成的整型數轉換成鏈表數組中的下標。我們先來看下源碼:
static int indexFor(int h, int length) {
return h & (length-1);
}
在JDK8中無indexFor方法,變為以下源碼中的i=(n-1)&hash
hash方法中通過(h = k.hashCode()) ^ (h >>> 16)
得到hash值
indexFor方法通過h & (length-1)
得到下標。其中的兩個參數h表示元素的hash值,length表示HashMap的容量。
i=(n-1)&hash
和上面的運算一樣。其中的兩個參數n表示HashMap的容量,hash表示元素的hash值。
那麼h & (length-1) 是什麼意思呢?
其實就是取模。Java之所以使用位運算(&)來代替取模運算(%),最主要的考慮就是效率。
位運算(&)效率要比代替取模運算(%)高很多,主要原因是位運算直接對記憶體數據(二進位)進行操作,不需要轉成十進位,因此處理速度非常快。那麼,為什麼可以使用位運算(&)來實現取模運算(%)呢?實現的原理如下:
a%b在當b為2^n時可簡化為a&(b-1)
證明:
1.當b為2^n時,b-1的二進位為011..11(0後面跟n個1).此時a&(b-1)即取a二進位右面n位
2.當b為2^n時,a/b的意義就是a右移n位。而右移的n位的值,就是a%b的值
理論不好理解,就找幾個例子用計算器試一下。
6 % 8 = 6 ,6 & 7 = 6
10 % 8 = 2 ,10 & 7 = 2
17 % 8 = 1 ,17 & 7 = 1
所以,h & (length-1)只要保證length是2^n的話,就可以實現取模運算了。
所以,因為位運算要比取模運算的效率更高,所以HashMap在計算元素要存放在數組中的下標的時候,使用位運算代替了取模運算。要實現位運算代替取模運算,要求HashMap的容量一定要是2^n 。
那麼,既然是2^n ,為啥一定要是16呢?為什麼不能是4、8或者32、64等等呢?
關於這個預設容量的選擇,JDK並沒有給出官方解釋。
這應該就是個經驗值,既然一定要設置一個預設的2^n 作為初始值,那麼就需要在效率和記憶體使用上做一個權衡。
4、8太小了就有可能頻繁發生擴容,擴容就需要重建哈希表,影響效率。32、64等等太大了又浪費空間,不划算。
所以,16就作為一個經驗值被採用了。
在JDK 8中,關於預設容量的定義如上源碼 ,其故意把16寫成1<<4,就是提醒開發者,這個地方要是2的冪。註釋中的 aka 16 也是jdk1.8中新增的,Also Known As 16 -- 又名16既然我們需要把容量設置為2^n,那麼HashMap是如何保證其容量一定可以是2^n的呢? 要做到這一類型容量,HashMap在指定容量初始化時以及自動擴容時都做了處理。
四、指定容量初始化
以下是《Java開發手冊》中的一段話: 可以看到指定容量初始化是被推薦的。 當我們通過HashMap(int initialCapacity)這個構造方法設置初始容量的時候,HashMap並不一定會直接採用我們傳入的initialCapacity,而是經過計算,得到一個新initialCapacity(例如3變為4,9變為16)。 以下為初始化的源碼: 可以看到最底層就是用tableSizeFor方法得到最終的初始化容量。 可以把代碼分為Part 1:int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
和Part 2:
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
Part 2比較簡單,就是做一下判斷,然後把Part 1得到的數值+1。
Part 1怎麼理解呢?其實是對一個二進位數依次無符號右移,然後與原值取或。
拿一個二進位數,套一遍上面的公式就發現其目的:
1100 1100 1100 >>>1 = 0110 0110 0110
1100 1100 1100 | 0110 0110 0110 = 1110 1110 1110
1110 1110 1110 >>>2 = 0011 1011 1011
1110 1110 1110 | 0011 1011 1011 = 1111 1111 1111
1111 1111 1111 >>>4 = 1111 1111 1111
1111 1111 1111 | 1111 1111 1111 = 1111 1111 1111
···以此類推
通過幾次無符號右移和按位或運算,我們把1100 1100 1100轉換成了1111 1111 1111 ,再把1111 1111 1111加1,就得到了1 0000 0000 0000,次數就是大於1100 1100 1100的第一個2的冪。
可以用程式試驗一下:
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
System.out.println((n < 0) ? 1 : (n >= 1 << 30) ? 1 << 30 : n + 1);
五、自動擴容
指定容量初始化後,往集合中put元素時,元素個數超過閾值後怎麼辦呢?這時候就需要擴容了。 HashMap有擴容機制,就是當達到擴容條件時會進行擴容。擴容條件就是當HashMap中的元素個數(size)超過閾值(threshold)時就會自動擴容,通過resize方法進行擴容。 threshold = loadFactor * capacity。 loadFactor是負載因數,預設值為0.75f,設置成0.75有一個好處,那就是0.75正好是3/4,而capacity又是2的冪。所以,兩個數的乘積都是整數。 對於一個預設的HashMap來說,預設情況下,當其size大於12(16*0.75)時就會觸發擴容。 下麵是HashMap中的resize方法中的一段源碼:newCap = oldCap << 1;
newThr = oldThr << 1;
從上面兩行代碼可以看出,擴容後的capacity和threshold變為原來的兩倍。
可見,當HashMap中size>threshold時就會自動擴容,擴容成原容量的2倍,即從16擴容到32、64、128 …閾值也從12到24,48,96...
所以,通過保證初始化容量均為2的冪,並且擴容時也是擴容到之前容量的2倍,所以,保證了HashMap的容量永遠都是2的冪,從而保證了位運算代替取模運算,從而提升了效率。
可以看到初始化之後容量和閾值是一樣的,
當放入第一個元素後,重新計算閾值,新的閾值=容量X負載因數。
可以用程式實驗一下:
HashMap m = new HashMap(15);
Class<?> mapType = m.getClass();
Field threshold = mapType.getDeclaredField("threshold");
threshold.setAccessible(true);
Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("容量:" + capacity.invoke(m) + " 閾值:" + threshold.get(m) + " 元素數量:" + m.size());
for (int i = 0; i < 25; i++) {
m.put(i, i);
System.out.println("容量:" + capacity.invoke(m) + " 閾值:" + threshold.get(m) + " 元素數量:" + m.size());
}