擼啊擼,再次擼HashMap源碼,踩坑源碼中構造方法

前言

點贊在看,養成習慣。web

點贊收藏,人生輝煌。編程

點擊關注【微信搜索公衆號:編程背鍋俠】,防止迷路。數組

HashMap系列文章

第一篇 HashMap源碼中的成員變量你還不懂? 來來來!!!整理好的成員變量源碼解析
微信

第二篇 擼啊擼,再次擼HashMap源碼,踩坑源碼中構造方法!!!每次都有收穫編輯器

構造方法

構造一個空的 HashMap ,默認初始容量(16)和默認負載因⼦(0.75)

源碼解析
// 構造一個無參數的構造方法
public HashMap() {  // 將默認的加載因子0.75賦值給loadFactor,並無建立數組  this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } 複製代碼
案例演示
@Test
public void test_hash_map_con_no(){  HashMap<Integer, String> map = new HashMap<>(); } 複製代碼

構造⼀個具備指定的初始容量和默認負載因子(0.75)HashMap

源碼解析
// 構造一個指定容量⼤⼩的構造函數
public HashMap(int initialCapacity) {  this(initialCapacity, DEFAULT_LOAD_FACTOR); } 複製代碼
案例演示
@Test
public void test_hash_map_con_in(){  HashMap<Integer, String> map = new HashMap<>(16); } 複製代碼

構造一個具備指定的初始容量和負載因⼦的HashMap

源碼解析
// 指定容量⼤小和加載因⼦的構造函數 initialCapacity: 指定的容量 loadFactor:指定的加載因⼦
public HashMap(int initialCapacity, float loadFactor) {  // 判斷初始化容量initialCapacity是否小於0  if (initialCapacity < 0)  // 若是⼩於0,則拋出非法的參數異常IllegalArgumentException  throw new IllegalArgumentException("Illegal initial capacity: " +  initialCapacity);  // 判斷初始化容量initialCapacity是否⼤於集合的最大容量MAXIMUM_CAPACITY->2的30次冪  if (initialCapacity > MAXIMUM_CAPACITY)  // 若是超過MAXIMUM_CAPACITY,會將MAXIMUM_CAPACITY賦值給initialCapacity  initialCapacity = MAXIMUM_CAPACITY;  // 判斷負載因⼦loadFactor是否小於等於0或者是不是⼀個⾮數值  if (loadFactor <= 0 || Float.isNaN(loadFactor))  // 若是知足上述其中之一,則拋出非法的參數異常IllegalArgumentException  throw new IllegalArgumentException("Illegal load factor: " +  loadFactor);  // 將指定的加載因⼦賦值給HashMap成員變量的負載因子loadFactor  this.loadFactor = loadFactor;  // tableSizeFor(initialCapacity) 判斷指定的初始化容量是不是2的n次冪,若是不是那麼會變爲⽐指定初始化容量大的最小的2的n次冪。  this.threshold = tableSizeFor(initialCapacity); } 複製代碼
案例演示
@Test
public void test_hash_map_con_in_lo(){  // 自定義初始化容量和加載因子  HashMap<Integer, String> map = new HashMap<>(16, 0.75f); } 複製代碼
疑惑代碼
this.threshold = tableSizeFor(initialCapacity);
以上代碼是源碼中的最後一句代碼,爲啥不是this.threshold = tableSizeFor(initialCapacity) * this.loadFactor ?  這樣才符合threshold的意思(當HashMap的size到達threshold這個閾值時會擴容)。 可是,請注意,在jdk8之後的構造方法中,並無對table這個成員變量進行初始化,table的初始化被推遲到了put方法中,在put⽅法中會對threshold從新計算,put⽅法的【resize()方法】具體實現我會在這個系列其餘文章會進行講解。 複製代碼
總結
若是這個構造函數的initialCapacity小於0,將會拋出非法異常IllegalArgumentException。
若是loadFactor的值是isNaN,則會拋出非法異常IllegalArgumentException。 複製代碼

構造一個包含另外一個Map的構造函數和默認負載因子(0.75)

源碼解析
public HashMap(Map<? extends K, ? extends V> m) {
 this.loadFactor = DEFAULT_LOAD_FACTOR;  // 負載因子loadFactor變爲默認的負載因子0.75  putMapEntries(m, false); } 複製代碼
案例演示
@Test
public void test_hash_map_con_map(){  HashMap<Integer, String> map = new HashMap<>();  map.put(1, "aaa");  map.put(2, "bbb");  map.put(3, "ccc");  // 使用構造方法  HashMap<Integer, String> all = new HashMap<>(map);  // 遍歷查看  all.forEach((k, v) -> System.out.println("k: " + k + " v: " + v)); } 複製代碼

tableSizeFor方法,返回比指定初始化容量大的最小的2的n次冪

static final int tableSizeFor(int cap) {
 int n = cap - 1;  n |= n >>> 1;  n |= n >>> 2;  n |= n >>> 4;  n |= n >>> 8;  n |= n >>> 16;  return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } 複製代碼
測試案例
// 搞個測試類【模擬上面的源碼】 n >>> 1右移1位 |=按位運算
@Test public void test_wei(){  int n = 14 - 1; // 13  n |= n >>> 1; // 15  n |= n >>> 2; // 15  n |= n >>> 4; // 15  n |= n >>> 8; // 15  n |= n >>> 16;// 15  int i = (n < 0) ? 1 : (n >= 128) ? 128 : n + 1; // 16  System.out.println(i);// 16 } 複製代碼
符號解析

>>>是右移符號。好比給定的值爲5【0101】,右移一位爲2【0010】。函數

|符號爲或運算。好比給定的值11 | 15,11對應的二進制【1011】,15對應的二進制【1111】,11 | 15結果爲【1111】15。post

源碼解析

當在實例化HashMap實例時,若是給定了initialCapacity(假設是5),因爲HashMap的 capacity必須都是2的冪,所以這個方法用於找到大於等於initialCapacity(假設是5)的最小的2的冪。性能

initialCapacity若是就是2的冪,則返回的仍是這個數)。測試

爲何要對cap作減1操做【int n = cap - 1】?

這是爲了防⽌,若是cap已是2的冪, ⼜沒有執行這個減1操做,則執行完後面的幾條無符號右移操做以後,返回的capacity將是這個cap的2倍。假如cap的值爲8,通過上面的計算獲得的仍是8。this

計算舉例
以方法tableSizeFor(int cap)舉例測試的數 cap = 65
 int n = cap - 1; ===>>>> n = 65 - 1 = 64  64 對應二進制 0100 0000  n >>> 1  右移10100 0000 ===>>>> 0010 0000  n |= n >>> 1 對應於 0100 0000 | 0010 0000 = 0110 000096  n >>> 2  右移20110 0000 ===>>>> 0001 1000  n |= n >>> 2 對應於 0110 0000 | 0001 1000 = 0111 1000120  n >>> 4  右移40111 1000 ===>>>> 0000 0111  n |= n >>> 4 對應於 0111 1000 | 0000 0111 = 0111 1111127  n >>> 8  右移80111 1111 ===>>>> 0000 0000  n |= n >>> 8 對應於 0111 1111 | 0000 0000 = 0111 1111127  n >>> 16  右移160111 1111 ===>>>> 0000 0000  n |= n >>> 16 對應於 0111 1111 | 0000 0000 = 0111 1111127  最後執行 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; 返回12812827次冪,加一的緣由是湊成整數次冪】 複製代碼

putMapEntries添加鍵值對到集合中

// m:給定的集合。evict:最初構造此映射時爲false。若是給定的集合爲null,將會拋出空指針異常NullPointerException
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {  // 獲取給定集合的長度  int s = m.size();  // 判斷給定的集合長度是否大於0  if (s > 0) {  // 判斷table是否已經初始化  if (table == null) { // pre-size  // 未初始化,s爲m的實際元素個數。預先計算一個容量ft。這裏爲何加1呢?有啥特殊的含義嗎?  float ft = ((float)s / loadFactor) + 1.0F;  // 上面計算的容量不小於最大值將這個值賦值給t,不然賦值給最大值  int t = ((ft < (float)MAXIMUM_CAPACITY) ?  (int)ft : MAXIMUM_CAPACITY);  // 判斷這個容量是否大於0,大於就對這個容量進行格式化,格式爲2的冪  if (t > threshold)  threshold = tableSizeFor(t);  }  // 以前的數組中有元素,判斷參數中的數組長度是否大於數組容量  else if (s > threshold)  // 擴容  resize();  // 遍歷給定的集合  for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {  // 獲取給定集合每一個鍵值對的k和v  K key = e.getKey();  V value = e.getValue();  // 將每個entry的鍵值對放到數組中  putVal(hash(key), key, value, false, evict);  }  } } 複製代碼
float ft = ((float)s / loadFactor) + 1.0F這一行代碼中爲何要加1.0F?
問題出現的源碼
float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); 複製代碼
  • s/loadFactor的結果是⼩數,加1.0F(int)ft至關因而對小數作一個向上取整以儘量的保證更大容量,更大的容量可以減小resize的調用次數。因此 + 1.0F是爲了獲取更大的容量。

  • 例如:原來集合的元素個數是6個,那麼6/0.75是8,是2的n次冪,那麼新的數組⼤小就是8了。

  • 而後原來數組的數據就會存儲到長度是8的新的數組中了,這樣會致使在存儲元素的時候,容量不夠,還得繼續擴容,那麼性能就會下降了,而若是+1呢,數組長度直接變爲16了,這樣能夠減小數組的擴容次數,從而提升效率。

謝謝點贊

  • 創做不易, 很是歡迎你們的點贊、評論和關注(^_−)☆
  • 你的點贊、評論以及關注是對我最大的支持和鼓勵,而你的支持和鼓勵
  • 我繼續創做高質量博客的動力 !!!

本文使用 mdnice 排版

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息