手撕HashMap JDK1.7

在1.7中HashMap的存储结构:数组 + 链表

数组的特点:存储空间有限且连续,扩容较麻烦,需要先new一个新数组,然后将旧数组的数据全部转移到新数组;查找较快,可直接通过索引得到对应的数据;在数组大小足够的情况下,若在非最后一个位置插入,也需要new新数组,然后转移旧数组数据,再插入

链表的特点:存储空间非连续,插入较为方便,可直接改变next指针;但是查找较慢,若需要查找的数据在链表的最后一个,则需要从头遍历到最后一个节点

HashMap综合了数组和链表的特点,通过Hash算法,将key值映射到固定的位置,利用合适的存储空间,将数据存储到特定的位置,当出现Hash冲突,会将数据添加到链表中

Hash算法:把任意长度的输入转变为固定长度的输出,输出空间要远远小于输入空间;对于输入来说,对于相同的输入,每次的计算结果都要相同,只要输入有丁点变换,计算结果都要不同;结果不可逆。

在这里插入图片描述

知道了HashMap的存储结构,那数组和链表中寸的是什么?直接存的数据吗?

并不是直接存储相应数据的,而是把对应的数据封装到一个HashMap的内部类Entry中,存放的实际是一个个Entry对象


注释中有很多内容,记得留意

一、Entry类

HashMap的数组和链表中存储的是Entry对象

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;  // 键
    V value;  // 值
    Entry<K,V> next; // 当出现hash冲突的时候,使用头插法,设置节点的next节点
    int hash;  // key的hash值
  	
  	//构造方法
    Entry(int h, K k, V v, Entry<K,V> n) {  
        value = v;  
        next = n;  
        key = k;  
        hash = h;  
    }  
  
    // 返回这个entry对象存储的key
    public final K getKey() {  
        return key;  
    }  

    // 返回这个entry对象存储的value
    public final V getValue() {  
        return value;  
    }  
  	//设置新的value
    public final V setValue(V newValue) {  
        V oldValue = value;  
        value = newValue;  
        return oldValue;  
    }  
    
   		//判断传入方法参数的entry对象和此对象是否相等
      public final boolean equals(Object o) {  
        if (!(o instanceof Map.Entry))  
            return false;  
        Map.Entry e = (Map.Entry)o;  
        Object k1 = getKey();  
        Object k2 = e.getKey();  
        //只有两者的key和value都相等时,才返回true
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {  
            Object v1 = getValue();  
            Object v2 = e.getValue();  
            if (v1 == v2 || (v1 != null && v1.equals(v2)))  
                return true;  
        }  
        return false;  
    }  
    
    //计算这个对象的hashcode
    public final int hashCode() { 
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());  
    }  
  	
    public final String toString() {  
        return getKey() + "=" + getValue();  
    }  
  
    //当向HashMap中put数据的时候,当map中的key和要添加的这个key相等的时候,会调用此方法
    void recordAccess(HashMap<K,V> m) {  
    }  
  
    //从HashMap中删除数据的时候,会调用此方法
    void recordRemoval(HashMap<K,V> m) {  
    } 

}

二、HashMap的使用

1.new一个HashMap

2.put数据

3.get数据

4.删除数据

public static void main(String[] args) {
        Map<Integer,String> map = new HashMap<Integer,String>();
        //向map中添加数据
        map.put(1,"一");
        map.put(2,"二");
        map.put(3,"三");
        //获取指定key对应的value值
        System.out.println("key = 1,value = " + map.get(1));
        System.out.println("遍历整个map");
        for(Map.Entry<Integer,String> entry : map.entrySet()){
            System.out.println("key = " + entry.getKey() + ",value = " + entry.getValue());
        }
        //根据对应的key删除map只的指定数据
        map.remove(1);
        System.out.println("删除后的数据");
        for(Map.Entry<Integer,String> entry : map.entrySet()){
            System.out.println("key = " + entry.getKey() + ",value = " + entry.getValue());
        }
    }


*******运行结果为*******
key = 1,value = 一
遍历整个map
key = 1,value = 一
key = 2,value = 二
key = 3,value = 三
删除后的数据
key = 2,value = 二
key = 3,value =

在后文中会对常用的方法进行深入的解析

三、HashMap的参数

HashMap中有几个重要的参数

1.Capacity:HashMap中的table(数组)的长度,默认为16,其值必须为2的幂,若不是,则会变为大于它并最接近它的那个2的幂;最大值为2的30次方,若传入的capacity的值大于2的30次方,将会变为2的30次方

2.load factor(加载因子):决定hashmap的阈值,默认为0.75。此值越大,可以在扩容前添加更多的元素,空间利用率更高,但是hash冲突概率会增大,链表更长,查找效率较低。此值越小,扩容更频繁,增加了不必要的性能开销,空间利用率低,hash冲突概率减小,查找效率较高。因此为了在时间和空间上得到一个更好的平衡,将此值设置为0.75较为合适

3.threshold(阈值):threshold = capacity * load factor,默认为12.当hash表的大小 >= 阈值时,就会以2倍的capacity对oldtable进行resize操作,期间会用到transfer方法进行数据的转移

4.size : 已经存储的entry的数量

四、深入HashMap源码

列出一些默认值

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final int MAXIMUM_CAPACITY = 1 << 30;
final float loadFactor;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
int threshold;
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
transient int size;

1.构造方法

//1.无参构造方法
public HashMap() {
        //调用了构造方法3,传入默认的capacity和load factor
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); 
    }

    //2.传入指定的capacity
   	public HashMap(int initialCapacity) {
        //实际调用构造方法3
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
        
    }

    //3.capacity和load factor都由自己传入
    public HashMap(int initialCapacity, float loadFactor) {
        //传入的capacity不能大于2的30次方,否则会等于被赋值为2的30次方
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        this.loadFactor = loadFactor;
      	//设置阈值,但并不是真正的阈值,最终会在第一次向map中put数据的时候,也就是初始化table的时候设置阈值
        threshold = initialCapacity;   
				//空方法
        init();
    }

	//4.传入一个map,新构造出map有传入map的所有映射
    public HashMap(Map<? extends K, ? extends V> m) {

        // 设置容量大小 & 加载因子 = 默认
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);

        // 该方法用于初始化 数组 & 阈值,下面会详细说明
        inflateTable(threshold);

        // 将传入的子Map中的全部元素逐个添加到HashMap中
        putAllForCreate(m);
    }
}

2.put

public V put(K key, V value)
				// map中的存储数组table采用了懒加载的方式,会在第一次向map中put数据的时候,对table进行初始化
        //此时的threshold实际是capacity
        if (table == EMPTY_TABLE) { 
        //inflatetable会在下面详解
        inflateTable(threshold); 
    }  
        
  			//hashmap允许key和value为空,会将所有key为null的entry存储到table[0]中
        if (key == null)
          	//putForNullKey会详解
            return putForNullKey(value);

				
				//若key不等于null,计算key的hash值
				//在hash()方法中,key先调用hashcode方法,然后再对得到的结果进行9次位运算,其意义是:使计算结果分布均匀,减少hash冲突的发生
        int hash = hash(key);
        // 根据hash值,计算最终存储在table中的索引,一般来说是用table的长度对hash值取模,但是在indexFor方法中有略微不同
				//对hash值和table.length - 1 进行&运算,结果和取模一致,但是因为位运算效率高,所以采取这个方法
				/*
					1.table.length为什么减1?
					首先hash值是一个32位的数,table长度始终为偶数,若以偶数计算,其二进制最后一位都为0,计算得到的存储位都为偶数,
					也就是说entry只会存储到偶数的位置上,一方面增加了hash冲突的概率,另一方面浪费空间
					2.为什么不采用得到的hash值作为存储位置
					hash值是一个32位的有符号的整数值,最大可达20多亿,也就是说table必须得有那么长,而实际不需要那么长,换种说法是,硬件
					层面无法满足,直接使用hash值作为存储下表,会造成数组下表越界
				*/
				//i为在table上的存储位置
        int i = indexFor(hash, table.length);
				
        //若table[i]上已经有entry,则对这条链表进行遍历,查看是否有key相同的entry
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
          	//若有key相同的entry,则用新value覆盖旧的vlaue,并返回旧的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++;
				//若table[i] 没有entry,则添加一个entry,这个方法在下面会详讲
        addEntry(hash, key, value, i);
        return null;
    }
inflateTable

这个方法只有在第一次向map中put值的时候会被调用

private void inflateTable(int toSize) {  
    //capacity的值只允许为2的幂,若不是,则会按照下面规则进行转换
  	//1.转换后的值必须大于它
  	//2.转换后的值必须最接近它
  	//3.转换后的值必须是2的幂
  	//举个🌰:传入17,则是被转换为32
    int capacity = roundUpToPowerOf2(toSize);->>分析1   
    // 计算真正的阈值
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);  
    //用capacity初始化table
    table = new Entry[capacity]; 
    initHashSeedAsNeeded(capacity);  
}  
putForNullKey
      private V putForNullKey(V value) {  
        //查找以table[0]为头节点的链表中是否已经存在key为null的节点,若存在,则用新vlaue覆盖旧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++;  

    //若没有,则使用头插法将节点插入到table[0]中
    addEntry(0, null, value, 0); 
    return null;  

}   

addEntry
void addEntry(int hash, K key, V value, int bucketIndex) {//bucketIndex为要插入的数组下标
          
          /*
          	插入前先判断当前的全部entry数量是否大于等于阈值,且要插入的table[bucketIndex]不为空,
          	同时满足以上条件会进行扩容操作,resize()方法在下文详解
          */
          
          if ((size >= threshold) && (null != table[bucketIndex])) {  
            //以旧table数组长度的2倍扩容
            resize(2 * table.length); 
            //重新计算要插入的这个key的hash值
            hash = (null != key) ? hash(key) : 0;
            //重新计算要插入的数组下表,这个新下表要么是原来的下标,要么是新下标 = 原下标 + 旧数组长度
            bucketIndex = indexFor(hash, table.length); 
    }  

    // 1.2 若容量足够,则创建1个新的数组元素(Entry) 并放入到数组中--> 分析2
    createEntry(hash, key, value, bucketIndex);  
} 

resize
void resize(int newCapacity) {  
    //保存旧数组
    Entry[] oldTable = table;  
    //旧数组的长度
    int oldCapacity = oldTable.length; 
    //如果旧数组的长度已经是最大值,则将阈值设置为int的最大值,然后返回
    if (oldCapacity == MAXIMUM_CAPACITY) {  
        threshold = Integer.MAX_VALUE;  
        return;  
    }  
    //以旧数组的2倍长度创建一个新的数组
    Entry[] newTable = new Entry[newCapacity];  
    //将数据从旧数组转移到新的数组中
    transfer(newTable); 
   	//将新数组的引用指向table
    table = newTable;  
    //设置新的阈值
    threshold = (int)(newCapacity * loadFactor); 
} 

transfer

这个方法在多线程的情况下会出现链表成环,进而当插入,查询的时候会死循环,待会用图示模拟单、双线程的转移情况

void transfer(Entry[] newTable) {
      //保存旧数组的引用
      Entry[] src = table; 
      //新数组的长度               
      int newCapacity = newTable.length;
  		//开始转移整个table包括table上链表的数据
      for (int j = 0; j < src.length; j++) { 
          Entry<K,V> e = src[j];           
        	//e不等于空,说明下面有链表
          if (e != null) {
            	//环节1
              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);
         }
     }
 }


在这里插入图片描述

多线程下链表成环的原因

请记住jdk1.7中的插入采用的是头插法。通过对比单线程下tansfer后的结果,可以发现同一链表上的元素的位置前后已经发生颠倒。而其中某个线程仍然是之前链表位置的指向,最终会成环;除此之外,当链表长度大于2时,还会造成原链表第三个位置及其之后的节点丢失。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

3.get

get操作和put操作的原理基本相同,这里不再对源码进行分析,而给出流程描述

get(K key)

1.判断传入的是否为null,不为null则到流程2;为null,则到table[0]中查找,是否已有key == null的entry存在,若不存在则使用头插法添加节点;若存在key == null的节点,则用新的value覆盖旧的value,结束。

2.根据key计算hash值,再根据hash值计算在table中的下标i,遍历以table[i]为头节点的链表,找到与key相等的节点,并返回对应的value,结束。

五、总结

HashMap结合了数组和链表的特性,并利用好的Hash算法和扩容机制,减少了Hash冲突的发生,但多线程下仍不安全,会出现链表成环、数据丢失等情况,HashMap的主要内容就介绍到这。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值