国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > Java > 正文

詳解Java HashMap實(shí)現(xiàn)原理

2019-11-26 13:17:41
字體:
供稿:網(wǎng)友

HashMap是基于哈希表的Map接口實(shí)現(xiàn),提供了所有可選的映射操作,并允許使用null值和null建,不同步且不保證映射順序。下面記錄一下研究HashMap實(shí)現(xiàn)原理。

HashMap內(nèi)部存儲(chǔ)

在HashMap內(nèi)部,通過維護(hù)一個(gè) 瞬時(shí)變量數(shù)組table (又稱:桶) 來存儲(chǔ)所有的鍵值對(duì)關(guān)系,桶 是個(gè)Entry對(duì)象數(shù)組,桶 的大小可以按需調(diào)整大小,長度必須是2的次冪。如下代碼:

/**  * 一個(gè)空的entry數(shù)組,桶 的默認(rèn)值  */ static final Entry<?,?>[] EMPTY_TABLE = {}; /**  * 桶,按需調(diào)整大小,但必須是2的次冪  */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

初始容量與負(fù)載因子

HashMap有兩個(gè)參數(shù)影響性能,初始容量和負(fù)載因子。容量是哈希表中 桶 的數(shù)量,初始容量只是哈希表在創(chuàng)建時(shí)的容量,負(fù)載因子是哈希表在其容量自動(dòng)增加之前可以達(dá)到多滿的一種尺度。當(dāng)哈希表中條目數(shù)超出了負(fù)載因子與當(dāng)前容量的乘積時(shí),則要對(duì)該Hash表進(jìn)行rehash操作(即重建內(nèi)部數(shù)據(jù)結(jié)構(gòu)),重建時(shí)以當(dāng)前容量的兩倍數(shù)目新建。可以通過構(gòu)造器設(shè)置初始容量與負(fù)載因子,默認(rèn)初始容量是16個(gè)條目,最大容量是2^30次方個(gè)條目,默認(rèn)負(fù)載因子是0.75

桶 就像一個(gè)存水的水桶,它默認(rèn)的初始存水容量是16個(gè)單位的水,默認(rèn)在灌水灌到16*0.75時(shí),在下次添加數(shù)據(jù)時(shí)會(huì)先擴(kuò)充容量,擴(kuò)充到32單位。0.75就是負(fù)載因子,初始容量與負(fù)載因子可以通過創(chuàng)建水桶的時(shí)候進(jìn)行設(shè)置。水桶最大的容量是2的30次方個(gè)單位的水。當(dāng)初始容量設(shè)置的數(shù)量大于最大容量時(shí),以最大容量為準(zhǔn)。當(dāng)擴(kuò)展時(shí)如果大于等于最大容量時(shí)則直接返回。

如下為HashMap的部分源碼,定義了默認(rèn)初始容量、負(fù)載因子及其他一些常量:

/**  * 默認(rèn)初始化容量,必須為2的次冪The default initial capacity - MUST be a power of two.  */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /**  * 最大容量,如果通過構(gòu)造函數(shù)參數(shù)中傳遞初始化容量大于該最大容量了,也會(huì)使用該容量為初始化容量  * 必須是2的次冪且小于等于2的30次方  */ static final int MAXIMUM_CAPACITY = 1 << 30; /**  * 默認(rèn)的負(fù)載因子,可以通過構(gòu)造函數(shù)指定  */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /**  * 一個(gè)空的數(shù)組表,當(dāng) 桶沒有初始化的時(shí)候  */ static final Entry<?,?>[] EMPTY_TABLE = {}; /**  * 桶 , 存儲(chǔ)所有的鍵值對(duì)條目,可以按需調(diào)整大小,長度大小必須為2的次冪   */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; /**  * Map中鍵值對(duì)的數(shù)量,在每次新增或刪除的時(shí)候都會(huì)對(duì)size進(jìn)行+1或者-1操作.  */ transient int size; /**  * 負(fù)載值,需要調(diào)整大小的臨界值,為:(capacity * load factor).在每次調(diào)整大小后會(huì)使用新的容量計(jì)算一下  * @serial  */ // If table == EMPTY_TABLE then this is the initial capacity at which the // table will be created when inflated. int threshold; /**  * 負(fù)載因子,如果構(gòu)造函數(shù)中沒有指定,則采用默認(rèn)的負(fù)載因子,  *  * @serial  */ final float loadFactor; /**  * HashMap結(jié)構(gòu)修改次數(shù),結(jié)構(gòu)修改時(shí)改變HashMap中的映射數(shù)量或修改其內(nèi)部結(jié)構(gòu)(例如,* rehash方法,重建內(nèi)部數(shù)據(jù)結(jié)構(gòu)),此字段用于在  * HashMap的集合視圖上生成的迭代器都處理成快速失敗的  */ transient int modCount;

初始容量與負(fù)載因子性能調(diào)整

通常,默認(rèn)負(fù)載因子(0.75)在時(shí)間和空間成本上尋求一種折中。負(fù)載因子過高雖然減少了空間開銷,但同時(shí)也增加了查詢成本(在大多數(shù)HashMap類的操作中,包括get和put操作,都反映了這一點(diǎn))。在設(shè)置初始容量時(shí)應(yīng)該考慮到映射中所需的條目數(shù)及其負(fù)載因子,以便最大限度的減少rehash操作次數(shù)。如果初始容量大于最大條目數(shù)除以加載因子,則不會(huì)發(fā)生rehash操作。

如果很多映射關(guān)系要存儲(chǔ)在HashMap實(shí)例中,則相對(duì)于按需執(zhí)行自動(dòng)的rehash操作以增大表的容量來說,使用足夠大的初始容量創(chuàng)建它將使得映射關(guān)系能更有效的存儲(chǔ)。

如下為重建HashMap數(shù)據(jù)結(jié)構(gòu)的代碼:

void resize(int newCapacity) {  Entry[] oldTable = table;  int oldCapacity = oldTable.length;  if (oldCapacity == MAXIMUM_CAPACITY) { // 如果容量已達(dá)最大限制,則設(shè)置下負(fù)載值后直接返回   threshold = Integer.MAX_VALUE;   return;  }  // 創(chuàng)建新的table存儲(chǔ)數(shù)據(jù)  Entry[] newTable = new Entry[newCapacity];  // 將舊table中的數(shù)據(jù)轉(zhuǎn)存到新table中去,這一步會(huì)花費(fèi)比較多的時(shí)間  transfer(newTable, initHashSeedAsNeeded(newCapacity));  table = newTable;  // 最后設(shè)置下下次調(diào)整大小的負(fù)載值  threshold = (int) Math.min(newCapacity * loadFactor,    MAXIMUM_CAPACITY + 1);}

HashMap構(gòu)造方法

 

第四個(gè)構(gòu)造方法是以已經(jīng)存在的Map創(chuàng)建一個(gè)新的HashMap,稍后再說,前三個(gè)構(gòu)造方法,其實(shí)最終調(diào)用的都是第三個(gè)帶兩個(gè)參數(shù)的方法,如果沒有傳遞參數(shù)則使用默認(rèn)的數(shù)值,代碼如下:

/**   * Constructs an empty <tt>HashMap</tt> with the default initial capacity   * (16) and the default load factor (0.75).   */  public HashMap() {    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);  }  /**   * Constructs an empty <tt>HashMap</tt> with the specified initial   * capacity and the default load factor (0.75).   *   * @param initialCapacity the initial capacity.   * @throws IllegalArgumentException if the initial capacity is negative.   */  public HashMap(int initialCapacity) {    this(initialCapacity, DEFAULT_LOAD_FACTOR);  }  /**   * Constructs an empty <tt>HashMap</tt> with the specified initial   * capacity and load factor.   *   * @param initialCapacity the initial capacity   * @param loadFactor   the load factor   * @throws IllegalArgumentException if the initial capacity is negative   *     or the load factor is nonpositive   */  public HashMap(int initialCapacity, float loadFactor) {    if (initialCapacity < 0)      throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);    if (initialCapacity > MAXIMUM_CAPACITY)      initialCapacity = MAXIMUM_CAPACITY;    if (loadFactor <= 0 || Float.isNaN(loadFactor))      throw new IllegalArgumentException("Illegal load factor: " +loadFactor);    this.loadFactor = loadFactor;    threshold = initialCapacity;    init();  }

由上可以看出,在構(gòu)造函數(shù)中,如果初始容量給的大于最大容量,則直接以最大容量代替。

put方法

接下來就看看HashMap中比較重要的部分

/**   * 在此映射中關(guān)聯(lián)指定值與指定建。如果該映射以前包含了一個(gè)該鍵的映射關(guān)系,則舊值被替換   *   * @param 指定將要關(guān)聯(lián)的鍵   * @param 指定將要關(guān)聯(lián)的值   * @return 與key關(guān)聯(lián)的舊值,如果key沒有任何映射關(guān)系,則返回null(返回null還可能表示該映射之前將null與key關(guān)聯(lián))   */  public V put(K key, V value) {    if (table == EMPTY_TABLE) {      inflateTable(threshold);    }    if (key == null)      return putForNullKey(value);    int hash = hash(key);    int i = indexFor(hash, table.length);    for (Entry<K,V> e = table[i]; e != null; e = e.next) {      Object k;      if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {        V oldValue = e.value;        e.value = value;        e.recordAccess(this);        return oldValue;      }    }    modCount++;    addEntry(hash, key, value, i);    return null;  }
 

1. 首先put方法中,先判斷 桶 是否為默認(rèn)的未初始化狀態(tài),如果未初始化則調(diào)用 inflateTable 方法去初始化,然后判斷參數(shù)key是否為null,如果為null,則調(diào)用putForNullKey專門進(jìn)行放key為null的數(shù)據(jù),putForNullKey方法與下面的第3步開始其實(shí)都是一樣的,只不過key為null的數(shù)據(jù)默認(rèn)存儲(chǔ)位置就是第一個(gè),即下標(biāo)默認(rèn)為0。

 2. 如果key不是null,則調(diào)用hash()方法獲取key的hash值,可以根據(jù)hash值、桶的長度通過indexFor方法計(jì)算該key可以放到桶的位置。

 3. Entry對(duì)象中有一個(gè)屬性next,可以形成一個(gè)單向鏈表,用來存儲(chǔ)哈希值相同的元素。因此當(dāng)計(jì)算出來key的hash值重復(fù)時(shí),存儲(chǔ)位置也會(huì)重復(fù),只要判斷一下存儲(chǔ)位置的元素及該元素的next屬性鏈表中是否與給定的key和key的hash值是否完全一致就可以了。如果有完全一致的,代表已經(jīng)存在,則替換舊值,并把舊值做為返回值直接返回。

 4. 把結(jié)構(gòu)修改次數(shù)自增1

 5. 調(diào)用addEntry方法將新的鍵值對(duì)增加到HashMap中。addEntity方法首先判斷當(dāng)前條目數(shù)據(jù)是否已經(jīng)大于等于負(fù)載值(桶的容量*負(fù)載因子)且桶的指定位置不為null,如果已經(jīng)大于且指定位置不為null,則調(diào)調(diào)整桶的容量為當(dāng)前容量的2倍,調(diào)整桶的容量參照上面的初始容量與負(fù)載因子性能調(diào)整 目錄。重新計(jì)算Hash值,計(jì)算存放位置。調(diào)用createEntry方法存放到 桶 中

void addEntry(int hash, K key, V value, int bucketIndex) {    if ((size >= threshold) && (null != table[bucketIndex])) {      resize(2 * table.length);      hash = (null != key) ? hash(key) : 0;      bucketIndex = indexFor(hash, table.length);    }    createEntry(hash, key, value, bucketIndex);  }  void createEntry(int hash, K key, V value, int bucketIndex) {    Entry<K,V> e = table[bucketIndex];    table[bucketIndex] = new Entry<>(hash, key, value, e);    size++;  }  /**  * Entry構(gòu)造方法,創(chuàng)建一個(gè)新的Entry.  */  Entry(int h, K k, V v, Entry<K,V> n) {    value = v;    next = n;    key = k;    hash = h;  }

 6. 在 createEntry 方法中,首先獲取指定位置的entry,然后新生成一個(gè)entry,在生成entry時(shí)把原有的entry存儲(chǔ)到新生成的entry的next屬性中(參考Entry的構(gòu)造方法),并把指定位置的entry替換成新生成的。

因?yàn)樾略鰲l目的時(shí)候,需要計(jì)算hash值,長度不夠時(shí)需要調(diào)整長度,當(dāng)計(jì)算的存儲(chǔ)位置已有元素的時(shí)候需要進(jìn)行鏈表式的存儲(chǔ),所以使用HashMap新增操作的效率并不是太高。

get方法

首先看下get方法的源碼:

/**   * 返回指定鍵所映射的值;如果對(duì)于該鍵來說,此映射不包含任何映射關(guān)系,則返回null   * 返回null值并不一定表明該映射不包含該鍵的映射,也可能改映射將該鍵顯示的映射為null,可使用containsKey操作來區(qū)分這兩種情況   * @see #put(Object, Object)   */  public V get(Object key) {    if (key == null)      return getForNullKey();    Entry<K,V> entry = getEntry(key);    return null == entry ? null : entry.getValue();  }  final Entry<K,V> getEntry(Object key) {    if (size == 0) {      return null;    }    int hash = (key == null) ? 0 : hash(key);    for (Entry<K,V> e = table[indexFor(hash, table.length)];       e != null;       e = e.next) {      Object k;      if (e.hash == hash &&        ((k = e.key) == key || (key != null && key.equals(k))))        return e;    }    return null;}

get方法實(shí)現(xiàn)較簡(jiǎn)單,以下是幾個(gè)步驟:

  1. 首先判斷key是否為null,如果為null,則調(diào)用 getForNullKey 方法來獲取,如果不為null則調(diào)用 getEntry 方法來獲取。getForNullKey方法與getEntity基本上一致,只不過少了一個(gè)步驟,就是默認(rèn)的key為null的存儲(chǔ)位置在第一個(gè),即下標(biāo)為0,沒有去計(jì)算位置而已。
  2. getEntity方法根據(jù)key計(jì)算哈希值,然后用哈希值、桶的長度計(jì)算存儲(chǔ)位置。
  3. getEntity以獲取指定位置的entry作為遍歷的開始,遍歷entry的next單鏈表,如果entry的哈希值與計(jì)算的哈希值一致且entry的key與指定的相等則返回entry
  4. 根據(jù)getEntity返回的值,get方法返回對(duì)應(yīng)的值。

通過查看get的源碼可以發(fā)現(xiàn),get方法通過key的哈希值與桶的長度計(jì)算存儲(chǔ)位置,基本上一下就能定位到要找的元素,即使再遍歷幾個(gè)重復(fù)哈希值的key,也是很快速的,因?yàn)楣V迪鄬?duì)唯一,所以HashMap對(duì)于查找性能是非常快的。

自定義對(duì)象作為HashMap的鍵

class User {  // 身份證號(hào)碼  protected int idNumber;  public User(int id){    idNumber = id;  }}public class TestUser{  public static void main(String[] args) {    Map<User, String> map = new HashMap<User, String>();    for (int i=0; i<5; i++) {      map.put(new User(i), "姓名: " + i);    }    System.out.println("User3 的姓名:" + map.get(new User(3)));  }}輸出:User3 的姓名:null

如上代碼,通過自定義的User類實(shí)例作為HashMap的對(duì)象時(shí),在打印的時(shí)候是無法找到User3的姓名的,因?yàn)閁ser類自動(dòng)繼承基類Object,所以這里會(huì)自動(dòng)使用Object的hashCode方法生成哈希值,而它默認(rèn)是使用對(duì)象的地址計(jì)算哈希值的。因此new User(3)生成的第一個(gè)實(shí)例的哈希值與生成的第二個(gè)實(shí)例的哈希值是不一樣的。但是如果只需要簡(jiǎn)單的覆蓋hashCode方法,也是無法正常運(yùn)作的,除非同時(shí)覆蓋equals方法,它也是Object的一部分。HashMap使用equals()判斷當(dāng)前的鍵是否與表中存在的鍵相同,可以參考上面的get或put方法。

正確equals()方法必須滿足下列5個(gè)條件:---參考《Java編程思想》―489頁

  1. 自反性。對(duì)任意x,x.equals(x)一定返回true
  2. 對(duì)稱性。對(duì)任意x和y,如果有y.equals(x)返回true,則x.equals(y)也返回true
  3. 傳遞性。對(duì)任意x,y,z,如果有x.equals(y)返回true,y.equals(z)返回true,則x.equals(z)一定返回true
  4. 一致性,對(duì)任意x和y,如果對(duì)象中用于等價(jià)比較的信息沒有改變,那么無論調(diào)用x.equals(y)多少次,返回的結(jié)果應(yīng)該保持一致,要么一致是true,要么一致是false.
  5. 對(duì)任何不是null的x,x.equals(null)一定返回false

再次強(qiáng)調(diào):默認(rèn)的Object.equals()只是比較對(duì)象的地址,所以一個(gè)new User(3)并不等于另一個(gè)new User(3)。因此,如果要使用自己的類作為HashMap的鍵,必須同時(shí)重載hashCode()和equals().

如下代碼可以正常運(yùn)作:

class User {  // 身份證號(hào)碼  protected int idNumber;  public User(int id){    idNumber = id;  }  @Override  public int hashCode() {    return idNumber;  }  @Override  public boolean equals(Object obj) {    return obj instanceof User && (idNumber==((User)obj).idNumber);  }}public class TestUser{  public static void main(String[] args) {    Map<User, String> map = new HashMap<User, String>();    for (int i=0; i<5; i++) {      map.put(new User(i), "姓名: " + i);    }    System.out.println("User3 的姓名:" + map.get(new User(3)));  }}輸出:User3 的姓名:姓名: 3

上面只是簡(jiǎn)單的在hashCode中返回了idNumber作為唯一的判別,用戶也可以根據(jù)自己的業(yè)務(wù)實(shí)現(xiàn)自己的方法。在equals方法中,instanceof會(huì)悄悄的檢查對(duì)象是否為null,如果instanceof左邊的參數(shù)為null,則會(huì)返回false,如果equals()的參數(shù)不為null且類型正確,則基于每個(gè)對(duì)象中的實(shí)際的idNumber進(jìn)行比較。從輸出可以看出,現(xiàn)在的方式是正確的。

參考:

   《Java編程思想》

    JDK 1.6 中文幫助手冊(cè)

以上就是本文的全部?jī)?nèi)容,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來一定的幫助,同時(shí)也希望多多支持武林網(wǎng)!

發(fā)表評(píng)論 共有條評(píng)論
用戶名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 东安县| 武川县| 鄂伦春自治旗| 淮滨县| 泸西县| 陆川县| 明光市| 广州市| 雅江县| 上林县| 五家渠市| 繁昌县| 祥云县| 若羌县| 富顺县| 仲巴县| 兴仁县| 古田县| 太谷县| 景泰县| 阿合奇县| 泾阳县| 太原市| 英吉沙县| 平安县| 苏尼特左旗| 和田县| 临海市| 永善县| 建水县| 大余县| 福清市| 东平县| 闽侯县| 乌兰县| 益阳市| 镇坪县| 黄大仙区| 沽源县| 黄浦区| 汾西县|