java集合中HashMap原理详解

本文详细解析了HashMap的设计思路与内部结构,包括其算法实现、性能特点及使用注意事项。

HashMap在Java开发中有着非常重要的角色地位,每一个Java程序员都应该了解HashMap。

主要从源码角度来解析HashMap的设计思路,并且详细地阐述HashMap中的几个概念,并深入探讨HashMap的内部结构和实现细节,讨论HashMap的性能问题。

1. HashMap设计思路以及内部结构组成


HashMap设计思路
Map<K,V>是一种以键值对存储数据的容器,而HashMap则是借助了键值Key的hashcode值来组织存储,使得可以非常快速和高效地地根据键值key进行数据的存取。

对于键值对<Key,Value>,HashMap内部会将其封装成一个对应的Entry<Key,Value>对象,即Entry<Key,Value>对象是键值对<Key,Value>的组织形式;

对于每个对象而言,JVM都会为其生成一个hashcode值。HashMap在存储键值对Entry<Key,Value>的时候,会根据Key的hashcode值,以某种映射关系,决定应当将这对键值对Entry<Key,Value>存储在HashMap中的什么位置上;
当通过Key值取数据的时候,然后根据Key值的hashcode,以及内部映射条件,直接定位到Key对应的Value值存放在什么位置,可以非常高效地将Value值取出。

为了实现上述的设计思路,在HashMap内部,采用了数组+链表的形式来组织键值对Entry<Key,Value>。

HashMap内部维护了一个Entry[] table 数组,当我们使用 new HashMap()创建一个HashMap时,Entry[] table 的默认长度为16(参见Java API)。Entry[] table的长度又被称为这个HashMap的容量(capacity);

对于Entry[] table的每一个元素而言,或为null,或为由若干个Entry<Key,Value>组成的链表。HashMap中Entry<Key,Value>的数目被称为HashMap的大小(size);

Entry[] table中的某一个元素及其对应的Entry<Key,Value>又被称为桶(bucket);

其结构如下图所示:




HashMap内部组织结构由上图所示,接下来看一下HashMap的基本工作流程:

HashMap设计的初衷,是为了尽可能地迅速根据Key的hashCode值, 直接就可以定位到对应的Entry<Key,Value>对象,然后得到Value。

考虑这样一个问题:

当我们使用 HashMap map = new HashMap()语句时,我们会创建一个HashMap对象,它内部的 Entry[] table的大小为 16,我们假定Entry[] table的大小会改变。现在,我们现在向它添加160对Key值完全不同的键值对<Key,Value>,那么,该HashMap内部有可能下面这种情况:即对于每一个桶中的由Entry<Key,Value>组成的链表的长度会非常地长!我们知道,对于查找链表操作的时间复杂度是很高的,为O(n)。这样的一个HashMap的性能会很低很低,如下图所示:



现在再来分析一下这个问题,当前的HashMap能够实现:

1. 根据Key的hashCode,可以直接定位到存储这个Entry<Key,Value>的桶所在的位置,这个时间的复杂度为O(1);

2. 在桶中查找对应的Entry<Key,Value>对象节点,需要遍历这个桶的Entry<Key,Value>链表,时间复杂度为O(n);

那么,现在,我们应该尽可能地将第2个问题的时间复杂度o(n)降到最低,读者现在是不是有想法了:我们应该要求桶中的链表的长度越短越好!桶中链表的长度越短,所消耗的查找时间就越低,最好就是一个桶中就一个Entry<Key,Value>对象节点就好了!

这样一来,桶中的Entry<Key,Value>对象节点要求尽可能第少,这就要求,HashMap中的桶的数量要多了。

我们知道,HashMap的桶数目,即Entry[] table数组的长度,由于数组是内存中连续的存储单元,它的空间代价是很大的,但是它的随机存取的速度是Java集合中最快的。我们增大桶的数量,而减少Entry<Key,Value>链表的长度,来提高从HashMap中读取数据的速度。这是典型的拿空间换时间的策略。

但是我们不能刚开始就给HashMap分配过多的桶(即Entry[] table 数组起始不能太大),这是因为数组是连续的内存空间,它的创建代价很大,况且我们不能确定给HashMap分配这么大的空间,它实际到底能够用多少,为了解决这一个问题,HashMap采用了根据实际的情况,动态地分配桶的数量。

HashMap的权衡策略
要动态分配桶的数量,这就要求要有一个权衡的策略了,HashMap的权衡策略是这样的:

如果 HashMap的大小> HashMap的容量(即Entry[] table的大小)*加载因子(经验值0.75)

则 HashMap中的Entry[] table 的容量扩充为当前的一倍;

然后重新将以前桶中的Entry<Key,Value>链表重新分配到各个桶中

上述的 HashMap的容量(即Entry[] table的大小) * 加载因子(经验值0.75)就是所谓的阀值(threshold):

阀值(threshold)=容量(capacity)*加载因子(load factor)
容量(capacity):是指HashMap内部Entry[] table线性数组的长度
加载因子(load factor):默认为0.75
阀值(threshold):当HashMap大小超过了阀值,HashMap将扩充2倍,并且rehash。

最后,看一个实例:

默认创建的HashMap map =new HashMap();map的容量是 16,那么,当我们往 map中添加第几个完全不同的键值对<Key,Value>时,HashMap的容量会扩充呢?

很简单的计算:由于默认的加载因子是0.75 ,那么,此时map的阀值是 16*0.75 = 12,即添加第13 个键值对<Key,Value>的时候,map的容量会扩充一倍。

这时候可能会有疑问:本来Entry[] table的容量是16,当放入12个键值对<Key,Value>后,不是至少还剩下4个Entry[] table 元素没有被使用到吗?这不是浪费了宝贵的空间了吗?! 确实如此,但是为了尽可能第减少桶中的Entry<Key,Value>链表的长度,以提高HashMap的存取性能,确定的这个经验值。如果你对存取效率要求的不是太高,想省点空间的话,你可以new HashMap(int initialCapacity, float loadFactor)构造方法将这个因子设置得大一些也无妨。

2. HashMap的算法实现解析


HashMap的算法实现最重要的两个是put() 和get() 两个方法,下面我将分析这两个方法:

public V put(K key, V value);
public V get(Object key);

另外,HashMap支持Key值为null 的情况,接下来也将做讨论。

1. 向HashMap中存储一对键值对<Key,Value>流程---put()方法实现:

put()方法-向HashMap存储键值对<Key,Value>

a. 获取这个Key的hashcode值,根据此值确定应该将这一对键值对存放在哪一个桶中,即确定要存放桶的索引;

b. 遍历所在桶中的Entry<Key,Value>链表,查找其中是否已经有了以Key值为Key存储的Entry<Key,Value>对象,

c1. 若已存在,定位到对应的Entry<Key,Value>,其中的Value值更新为新的Value值;返回旧值;

c2. 若不存在,则根据键值对<Key,Value> 创建一个新的Entry<Key,Value>对象,然后添加到这个桶的Entry<Key,Value>链表的头部。

d. 当前的HashMap的大小(即Entry<key,Value>节点的数目)是否超过了阀值,若超过了阀值(threshold),则增大HashMap的容量(即Entry[] table 的大小),并且重新组织内部各个Entry<Key,Value>排列。

详细流程如下列的代码所示:

  1. /**
  2. *将<Key,Value>键值对存到HashMap中,如果Key在HashMap中已经存在,那么最终返回被替换掉的Value值。
  3. *Key和Value允许为空
  4. */
  5. publicVput(Kkey,Vvalue){
  6. //1.如果key为null,那么将此value放置到table[0],即第一个桶中
  7. if(key==null)
  8. returnputForNullKey(value);
  9. //2.重新计算hashcode值,
  10. inthash=hash(key.hashCode());
  11. //3.计算当前hashcode值应当被分配到哪一个桶中,获取桶的索引
  12. inti=indexFor(hash,table.length);
  13. //4.循环遍历该桶中的Entry列表
  14. for(Entry<K,V>e=table[i];e!=null;e=e.next){
  15. Objectk;
  16. //5.查找Entry<Key,Value>链表中是否已经有了以Key值为Key存储的Entry<Key,Value>对象,
  17. //已经存在,则将Value值覆盖到对应的Entry<Key,Value>对象节点上
  18. if(e.hash==hash&&((k=e.key)==key||key.equals(k))){//请读者注意这个判定条件,非常重要!!!
  19. VoldValue=e.value;
  20. e.value=value;
  21. e.recordAccess(this);
  22. returnoldValue;
  23. }
  24. }
  25. modCount++;
  26. //6不存在,则根据键值对<Key,Value>创建一个新的Entry<Key,Value>对象,然后添加到这个桶的Entry<Key,Value>链表的头部。
  27. addEntry(hash,key,value,i);
  28. returnnull;
  29. }
  30. /**
  31. *Key为null,则将Entry<null,Value>放置到第一桶table[0]中
  32. */
  33. privateVputForNullKey(Vvalue){
  34. for(Entry<K,V>e=table[0];e!=null;e=e.next){
  35. if(e.key==null){
  36. VoldValue=e.value;
  37. e.value=value;
  38. e.recordAccess(this);
  39. returnoldValue;
  40. }
  41. }
  42. modCount++;
  43. addEntry(0,null,value,0);
  44. returnnull;
  45. }
  /**
     * 将<Key,Value>键值对存到HashMap中,如果Key在HashMap中已经存在,那么最终返回被替换掉的Value值。
     * Key 和Value允许为空
     */
    public V put(K key, V value) {
        
    	//1.如果key为null,那么将此value放置到table[0],即第一个桶中
    	if (key == null)
            return putForNullKey(value);
    	//2.重新计算hashcode值,
        int hash = hash(key.hashCode());
        //3.计算当前hashcode值应当被分配到哪一个桶中,获取桶的索引
        int i = indexFor(hash, table.length);
        //4.循环遍历该桶中的Entry列表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //5. 查找Entry<Key,Value>链表中是否已经有了以Key值为Key存储的Entry<Key,Value>对象,
            //已经存在,则将Value值覆盖到对应的Entry<Key,Value>对象节点上
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//请读者注意这个判定条件,非常重要!!!
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        //6不存在,则根据键值对<Key,Value> 创建一个新的Entry<Key,Value>对象,然后添加到这个桶的Entry<Key,Value>链表的头部。
        addEntry(hash, key, value, i);
        return null;
    }


    /**
     * Key 为null,则将Entry<null,Value>放置到第一桶table[0]中
     */
    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }
  1. /**
  2. *根据特定的hashcode重新计算hash值,
  3. *由于JVM生成的的hashcode的低字节(lowerbits)冲突概率大,(JDK只是这么一说,至于为什么我也不清楚)
  4. *为了提高性能,HashMap对Key的hashcode再加工,取Key的hashcode的高字节参与运算
  5. */
  6. staticinthash(inth){
  7. //ThisfunctionensuresthathashCodesthatdifferonlyby
  8. //constantmultiplesateachbitpositionhaveabounded
  9. //numberofcollisions(approximately8atdefaultloadfactor).
  10. h^=(h>>>20)^(h>>>12);
  11. returnh^(h>>>7)^(h>>>4);
  12. }
  13. /**
  14. *返回此hashcode应当分配到的桶的索引
  15. */
  16. staticintindexFor(inth,intlength){
  17. returnh&(length-1);
  18. }
/**
     * 根据特定的hashcode 重新计算hash值,
     * 由于JVM生成的的hashcode的低字节(lower bits)冲突概率大,(JDK只是这么一说,至于为什么我也不清楚)
     * 为了提高性能,HashMap对Key的hashcode再加工,取Key的hashcode的高字节参与运算
     */
    static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }


    /**
     * 返回此hashcode应当分配到的桶的索引
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

当HashMap的大小大于阀值时,HashMap容量的扩充算法

当当前的HashMap的大小大于阀值时,HashMap会对此HashMap的容量进行扩充,即对内部的Entry[] table 数组进行扩充。

HashMap对容量(Entry[] table数组长度) 有两点要求:

1. 容量的大小应当是 2的N次幂;

2. 当容量大小超过阀值时,容量扩充为当前的一倍;

这里第2点很重要,如果当前的HashMap的容量为16,需要扩充时,容量就要变成16*2 = 32,接着就是32*2=64、64*2=128、128*2=256.........可以看出,容量扩充的大小是呈指数级的级别递增的。

这里容量扩充的操作可以分为以下几个步骤:

1. 申请一个新的、大小为当前容量两倍的数组;

2. 将旧数组的Entry[] table中的链表重新计算hash值,然后重新均匀地放置到新的扩充数组中;

3. 释放旧的数组;

由上述的容量扩充的步骤来看,一次容量扩充的代价非常大,所以在容量扩充时,扩充的比例为当前的一倍,这样做是尽量减少容量扩充的次数。

为了提高HashMap的性能:

1.在使用HashMap的过程中,你比较明确它要容纳多少Entry<Key,Value>,你应该在创建HashMap的时候直接指定它的容量;

2. 如果你确定HashMap的使用的过程中,大小会非常大,那么你应该控制好 加载因子的大小,尽量将它设置得大些。避免Entry[] table过大,而利用率觉很低。

  1. /**
  2. *Rehashesthecontentsofthismapintoanewarraywitha
  3. *largercapacity.Thismethodiscalledautomaticallywhenthe
  4. *numberofkeysinthismapreachesitsthreshold.
  5. *
  6. *IfcurrentcapacityisMAXIMUM_CAPACITY,thismethoddoesnot
  7. *resizethemap,butsetsthresholdtoInteger.MAX_VALUE.
  8. *Thishastheeffectofpreventingfuturecalls.
  9. *
  10. *@paramnewCapacitythenewcapacity,MUSTbeapoweroftwo;
  11. *mustbegreaterthancurrentcapacityunlesscurrent
  12. *capacityisMAXIMUM_CAPACITY(inwhichcasevalue
  13. *isirrelevant).
  14. */
  15. voidresize(intnewCapacity){
  16. Entry[]oldTable=table;
  17. intoldCapacity=oldTable.length;
  18. if(oldCapacity==MAXIMUM_CAPACITY){
  19. threshold=Integer.MAX_VALUE;
  20. return;
  21. }
  22. Entry[]newTable=newEntry[newCapacity];
  23. transfer(newTable);
  24. table=newTable;
  25. threshold=(int)(newCapacity*loadFactor);
  26. }
  27. /**
  28. *TransfersallentriesfromcurrenttabletonewTable.
  29. */
  30. voidtransfer(Entry[]newTable){
  31. Entry[]src=table;
  32. intnewCapacity=newTable.length;
  33. for(intj=0;j<src.length;j++){
  34. Entry<K,V>e=src[j];
  35. if(e!=null){
  36. src[j]=null;
  37. do{
  38. Entry<K,V>next=e.next;
  39. inti=indexFor(e.hash,newCapacity);
  40. e.next=newTable[i];
  41. newTable[i]=e;
  42. e=next;
  43. }while(e!=null);
  44. }
  45. }
  46. }
/**
     * Rehashes the contents of this map into a new array with a
     * larger capacity.  This method is called automatically when the
     * number of keys in this map reaches its threshold.
     *
     * If current capacity is MAXIMUM_CAPACITY, this method does not
     * resize the map, but sets threshold to Integer.MAX_VALUE.
     * This has the effect of preventing future calls.
     *
     * @param newCapacity the new capacity, MUST be a power of two;
     *        must be greater than current capacity unless current
     *        capacity is MAXIMUM_CAPACITY (in which case value
     *        is irrelevant).
     */
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }


        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }
    
    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;
                do {
                    Entry<K,V> next = e.next;
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

为什么JDK建议我们重写Object.equals(Object obj)方法时,需要保证对象可以返回相同的hashcode值?

Java程序员都看过JDK的API文档,该文档关于Object.equals(Object obj)方法,有这样的描述:

“注意:当此方法被重写时,通常有必要重写hashCode 方法,以维护hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。”

有的人虽然知道这个协定,但是不一定真正知道为什么会有这一个要求,现在,就来看看原因吧。

再注意看一下上述的这个put()方法实现,当遍历某个桶中的Entry<Key,Value>链表来查找Entry实例的过程中所使用的判断条件:

  1. for(Entry<K,V>e=table[i];e!=null;e=e.next){
  2. Objectk;
  3. //5.查找Entry<Key,Value>链表中是否已经有了以Key值为Key存储的Entry<Key,Value>对象,
  4. //已经存在,则将Value值覆盖到对应的Entry<Key,Value>对象节点上
  5. if(e.hash==hash&&((k=e.key)==key||key.equals(k))){
  6. VoldValue=e.value;
  7. e.value=value;
  8. e.recordAccess(this);
  9. returnoldValue;
  10. }
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //5. 查找Entry<Key,Value>链表中是否已经有了以Key值为Key存储的Entry<Key,Value>对象,
            //已经存在,则将Value值覆盖到对应的Entry<Key,Value>对象节点上
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }

对于给定的Key,Value,判断该Key是否与Entry链表中有某一个Entry对象的Key值相等使用的是(k==e.key)==key) || key.equals(k),另外还有一个判断条件:即Key经过hash函数转换后的hash值和当前Entry对象的hash属性值相等(该hash属性值和Entry内的Key经过hash方法转换后的hash值相等)。

上述的情况我们可以总结为;HashMap在确定Key是否在HashMap中存在的要求有两个:

1. Key值是否相等;

2. hashcode是否相等;

所以我们在定义类时,如果重写了equals()方法,但是hashcode却没有保证相等,就会导致当使用该类实例作为Key值放入HashMap中,会出现HashMap“工作异常”的问题,会出现你不希望的情况。下面让我们通过一个例子来看看这个“工作异常”情况:

例子: 定义一个简单Employee类,重写equals方法,而没有重写hashCode()方法。然后使用该类创建两个实例,放置到一个HashMap中:

  1. packagecom.hash;
  2. /**
  3. *简单EmployeeBean,重写equals方法,未重写hashCode()方法
  4. *@authorlouluan
  5. */
  6. publicclassEmployee{
  7. privateStringemployeeCode;
  8. privateStringname;
  9. publicEmployee(StringemployeeCode,Stringname){
  10. this.employeeCode=employeeCode;
  11. this.name=name;
  12. }
  13. publicStringgetEmployeeCode(){
  14. returnemployeeCode;
  15. }
  16. publicStringgetName(){
  17. returnname;
  18. }
  19. @Override
  20. publicbooleanequals(Objecto)
  21. {
  22. if(oinstanceofEmployee)
  23. {
  24. Employeee=(Employee)o;
  25. if(this.employeeCode.equals(e.getEmployeeCode())&&name.equals(e.getName()))
  26. {
  27. returntrue;
  28. }
  29. }
  30. returnfalse;
  31. }
  32. }
package com.hash;


/**
 * 简单Employee Bean,重写equals方法,未重写hashCode()方法
 * @author louluan
 */
public class Employee {
	
	private String employeeCode;
	private String name;
	
	public Employee(String employeeCode, String name) {
		this.employeeCode = employeeCode;
		this.name = name;
	}
	
	public String getEmployeeCode() {
		return employeeCode;
	}
	public String getName() {
		return name;
	}
	
	@Override
	public boolean equals(Object o)
	{
		if(o instanceof Employee)
		{
			Employee e = (Employee)o;
			if(this.employeeCode.equals(e.getEmployeeCode()) && name.equals(e.getName()))
			{
				return true;
			}
		}
		return false;
	}
}
  1. packagecom.hash;
  2. importjava.util.HashMap;
  3. publicclassTest{
  4. publicstaticvoidmain(String[]args){
  5. Employeeem1=newEmployee("123","anndy");
  6. Employeeem2=newEmployee("123","anndy");
  7. booleanequals=em1.equals(em2);
  8. System.out.println("em1equalsem2?"+equals);
  9. HashMapmap=newHashMap();
  10. map.put(em1,"test1");
  11. map.put(em2,"test2");
  12. System.out.println("mapsize:"+map.size());
  13. }
  14. }
  15. <em><u><spanstyle="font-family:CourierNew;color:#000000;background-color:rgb(240,240,240);">
  16. </span></u></em>
package com.hash;
import java.util.HashMap;


public class Test {
	
	public static void main(String[] args) {
		Employee em1= new Employee("123","anndy");
		Employee em2= new Employee("123","anndy");
		boolean equals= em1.equals(em2);
		System.out.println("em1 equals em2 ? " +equals);
		
		HashMap map = new HashMap();
		map.put(em1, "test1");
		map.put(em2, "test2");
		System.out.println("map size:"+map.size());
	}
}
<em><u><span style="font-family:Courier New;color:#000000;background-color: rgb(240, 240, 240);">
</span></u></em>

运行结果:

em1 equals em2 ? true
map size:2

结果分析:

上述的例子中,我们使用了new Employee("123","anndy"); 语句创建了两个完全一样的对象em1,em2,对我们来说,它们就是相同的对象,然后,我们将这两个我们认为相等的对象作为Key值放入HashMap中,我们想要的结果是:HashMap中的Entry<Key,Value>键值对数目应该就一个,并且Entry对象的Value值应该是由"test1" 替换成"test2",但是实际的结果是:HashMap的大小为2,即HashMap中有两个Entry<Key,Value>键值对!!!

原因现在清晰了:因为em1和em2对象的hashCode()继承自Object,它们返回两个不同的值,即em1 和em2的hashcode值不相同。

从上面的这个例子可以看出:

我们重写Object.equals(Object obj)方法时,需要保证对象可以返回相同的hashcode。否则,HashMap工作的时候会有不可控的异常情况出现。

2.get() 方法的实现:

根据特定的Key值从HashMap中取Value的结果就比较简单了:

get()方法-根据Key从HashMap中取Value

a. 获取这个Key的hashcode值,根据此hashcode值决定应该从哪一个桶中查找;

b. 遍历所在桶中的Entry<Key,Value>链表,查找其中是否已经有了以Key值为Key存储的Entry<Key,Value>对象;

c1. 若已存在,定位到对应的Entry<Key,Value>,返回value;

c2. 若不存在,返回null;

具体算法如下:

  1. /**
  2. *Returnsthevaluetowhichthespecifiedkeyismapped,
  3. *or{@codenull}ifthismapcontainsnomappingforthekey.
  4. *返回key对应的Value值,如果HashMap中没有,则返回null;
  5. *支持Key为null情况
  6. *<p>Moreformally,ifthismapcontainsamappingfromakey
  7. *{@codek}toavalue{@codev}suchthat{@code(key==null?k==null:
  8. *key.equals(k))},thenthismethodreturns{@codev};otherwise
  9. *itreturns{@codenull}.(Therecanbeatmostonesuchmapping.)
  10. *
  11. *<p>Areturnvalueof{@codenull}doesnot<i>necessarily</i>
  12. *indicatethatthemapcontainsnomappingforthekey;it'salso
  13. *possiblethatthemapexplicitlymapsthekeyto{@codenull}.
  14. *The{@link#containsKeycontainsKey}operationmaybeusedto
  15. *distinguishthesetwocases.
  16. *
  17. *@see#put(Object,Object)
  18. */
  19. publicVget(Objectkey){
  20. if(key==null)
  21. returngetForNullKey();
  22. inthash=hash(key.hashCode());
  23. //遍历列表
  24. for(Entry<K,V>e=table[indexFor(hash,table.length)];
  25. e!=null;
  26. e=e.next){
  27. Objectk;
  28. if(e.hash==hash&&((k=e.key)==key||key.equals(k)))
  29. returne.value;
  30. }
  31. returnnull;
  32. }
  /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *  返回key对应的Value值,如果HashMap中没有,则返回null;
     *  支持Key为null情况
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        //遍历列表
        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.equals(k)))
                return e.value;
        }
        return null;
    }

3.HashMap对Key为null情况的支持

HashMap允许Key以null的形式存取,Hashmap会将Key为null组成的Entry<null,Value>放置到table[0],即第一个桶中,在put()和get()操作时,会先对Key 为null的值特殊处理:

  1. /**
  2. *Offloadedversionofget()tolookupnullkeys.Nullkeysmap
  3. *toindex0.Thisnullcaseissplitoutintoseparatemethods
  4. *forthesakeofperformanceinthetwomostcommonlyused
  5. *operations(getandput),butincorporatedwithconditionalsin
  6. *others.
  7. *getćä˝
  8. */
  9. privateVgetForNullKey(){
  10. for(Entry<K,V>e=table[0];e!=null;e=e.next){
  11. if(e.key==null)
  12. returne.value;
  13. }
  14. returnnull;
  15. }
 /**
     * Offloaded version of get() to look up null keys.  Null keys map
     * to index 0.  This null case is split out into separate methods
     * for the sake of performance in the two most commonly used
     * operations (get and put), but incorporated with conditionals in
     * others.
     * get ćä˝
     */
    private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }
  1. /**
  2. *Key为null,则将Entry<null,Value>放置到第一桶table[0]中
  3. */
  4. privateVputForNullKey(Vvalue){
  5. for(Entry<K,V>e=table[0];e!=null;e=e.next){
  6. if(e.key==null){
  7. VoldValue=e.value;
  8. e.value=value;
  9. e.recordAccess(this);
  10. returnoldValue;
  11. }
  12. }
  13. modCount++;
  14. addEntry(0,null,value,0);
  15. returnnull;
  16. }
 /**
     * Key 为null,则将Entry<null,Value>放置到第一桶table[0]中
     */
    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

4. 键值对Entry<Key,Value>的移除----remove(key)方法的实现

根据key值移除键值对的操作也比较简单,内部关键的流程分为两个:

1. 根据Key的hashcode 值和Key定位到Entry<key,Value> 对象在HashMap中的位置;

2. 由于Entry<Key,Value>是一个链表元素,之后便是链表删除节点的操作了;

  1. /**
  2. *Removesthemappingforthespecifiedkeyfromthismapifpresent.
  3. *
  4. *@paramkeykeywhosemappingistoberemovedfromthemap
  5. *@returnthepreviousvalueassociatedwith<tt>key</tt>,or
  6. *<tt>null</tt>iftherewasnomappingfor<tt>key</tt>.
  7. *(A<tt>null</tt>returncanalsoindicatethatthemap
  8. *previouslyassociated<tt>null</tt>with<tt>key</tt>.)
  9. */
  10. publicVremove(Objectkey){
  11. Entry<K,V>e=removeEntryForKey(key);
  12. return(e==null?null:e.value);
  13. }
  14. /**
  15. *Removesandreturnstheentryassociatedwiththespecifiedkey
  16. *intheHashMap.ReturnsnulliftheHashMapcontainsnomapping
  17. *forthiskey.
  18. */
  19. finalEntry<K,V>removeEntryForKey(Objectkey){
  20. inthash=(key==null)?0:hash(key.hashCode());
  21. inti=indexFor(hash,table.length);
  22. Entry<K,V>prev=table[i];
  23. Entry<K,V>e=prev;
  24. while(e!=null){
  25. Entry<K,V>next=e.next;
  26. Objectk;
  27. if(e.hash==hash&&
  28. ((k=e.key)==key||(key!=null&&key.equals(k)))){
  29. modCount++;
  30. size--;
  31. if(prev==e)
  32. table[i]=next;
  33. else
  34. prev.next=next;
  35. e.recordRemoval(this);
  36. returne;
  37. }
  38. prev=e;
  39. e=next;
  40. }
  41. returne;
  42. }
/**
     * Removes the mapping for the specified key from this map if present.
     *
     * @param  key key whose mapping is to be removed from the map
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }


    /**
     * Removes and returns the entry associated with the specified key
     * in the HashMap.  Returns null if the HashMap contains no mapping
     * for this key.
     */
    final Entry<K,V> removeEntryForKey(Object key) {
        int hash = (key == null) ? 0 : hash(key.hashCode());
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;


        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }


        return e;
    }

3、HashMap的特点总结:


1. HashMap是线程不安全的,如果想使用线程安全的,可以使用Hashtable;它提供的功能和Hashmap基本一致。HashMap实际上是一个Hashtable的轻量级实现;

2. 允许以Key为null的形式存储<null,Value>键值对;

3. HashMap的查找效率非常高,因为它使用Hash表对进行查找,可直接定位到Key值所在的桶中;

4. 使用HashMap时,要注意HashMap容量和加载因子的关系,这将直接影响到HashMap的性能问题。加载因子过小,会提高HashMap的查找效率,但同时也消耗了大量的内存空间,加载因子过大,节省了空间,但是会导致HashMap的查找效率降低。




评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值