爲什麼HashMap的容量爲2的指數

爲什麼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;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章