爲什麼HashMap的容量爲2的指數
一. HashMap的容量揭祕
我們知道,HashMap的容量要求爲2的指數(16、32、256等),默認爲16。此外,HashMap也支持在構造器中指定初始容量initialCapacity,並會將容量設置爲大於等於initialCapacity的最小的2的指數。HashMap會基於這個容量創建table數組:
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
那麼,爲什麼HashMap一定要將容量設置爲2的指數呢?原因如下:
對於一個整數n,如果它爲2的指數,那麼對於任意正整數h,都有 h % n = h & (n - 1)
寫個程序驗證一下:
public class TestHash {
public static void main(String[] args) {
//創建一個2的指數n
int n = (int) Math.pow(2, ThreadLocalRandom.current().nextInt(1, 10));
//創建一個隨機正整數h
int h = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE);
//判斷結果
System.err.println("取模運算與位運算的結果是否相等: " + ((h % n) == (h & (n - 1))));
}
}
可以看到,結果始終是相等的。
基於這個性質,就可以使用位運算代替取模運算,在很大程度上提升性能。
HashMap在定位table中的桶時,就利用了table長度爲2的指數這個性質,通過位運算迅速地找到key所在的桶,代碼如下:
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 &&
//通過(n - 1) & hash找到桶,代替(hash % n)
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
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;
}
可以看到,上面代碼中,首先計算出key的哈希值hash,然後通過tab[(n - 1) & hash]迅速定位到了key所在的桶,十分高效。
注:該性質對ConcurrentHashMap同樣適用。
二. 位運算的小應用
在JDK底層代碼中,很多地方都用到了位運算,主要原因就是位運算效率很高。那麼位運算還有什麼其他應用呢?
下面舉了一個小例子——權限處理:將每種權限定義成一個二進制的數,那麼通過簡單、高效的位運算就可以進行權限的賦予、禁用和判斷,代碼如下:
/**
* @Author zhangshenao
* @Date 2019-09-03
* @Description 使用位運算進行權限操作
*/
public class Permission {
//權限定義
public static final int QUERY = 1 << 0;
public static final int MODIFY = 1 << 1;
public static final int DELETE = 1 << 2;
//當前權限狀態
private int state;
public static Permission valueOf(int state) {
Permission p = new Permission();
p.state = state;
return p;
}
//判斷是否有權限,採用位運算
public boolean isEnableQuery() {
return (state & QUERY) == QUERY;
}
public boolean isEnableModify() {
return (state & MODIFY) == MODIFY;
}
public boolean isEnableDelete() {
return (state & DELETE) == DELETE;
}
//增加權限,使用位運算
public void enable(int permission) {
state = state | permission;
}
//禁用權限,使用位運算
public void disable(int permission) {
state = state & ~permission;
}
public static void main(String[] args) {
Permission permission = valueOf(0);
permission.enable(QUERY);
permission.enable(MODIFY);
System.err.println("isEnableQuery: " + permission.isEnableQuery());
System.err.println("isEnableModify: " + permission.isEnableModify());
System.err.println("isEnableDelete: " + permission.isEnableDelete());
}
}
其實類似的處理在JDK代碼中也有很多體現,如Class類,使用一些二進制的值表示一個Class的相關屬性,如是否是註解、是否是枚舉等:
public final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement {
private static final int ANNOTATION= 0x00002000;
private static final int ENUM = 0x00004000;
private static final int SYNTHETIC = 0x00001000;
}
這樣就可以通過位運算很快地判斷出一個Class的相關屬性:
public boolean isAnnotation() {
return (getModifiers() & ANNOTATION) != 0;
}