关于HashMap的一些总结(及部分源码)


前言

本文从三个角度来讲述HashMap、在何时使用HashMap、HashMap使用的数据结构及源码、与其他集合或java版本区别


一、在何时使用HashMap?

首先,我们要知道Map的大家族都有什么实现类?

  • HashTable是一个线程安全的实现类,使用头插法插入数据,二倍扩容。HashTable实现线程安全的机制方式为在所有方法上加了synchronized来保证线程安全,这种方式带来的后果就是在多线程环境下性能特别差,k,v值均不能为null,默认长度11。
  • TreeMap是通过红黑树数据结构实现,红黑树是一种平衡二叉查找树,红黑树结构天然支持排序,TreeMap默认情况下通过Key值的自然顺序进行排序,key值不可以为null,非线程安全的实现类。
  • ConcurrentHashMap是线程安全的Map,数据结构(Node数组+链表+红黑树),在锁的结构上采用采用CAS +synchronized实现更加细粒度的锁(1.8版本),key值不可以为null,在多线程的条件下性能优于HashTable。
  • HashMap是由Node数组+链表+红黑树组成,当链表结点长度超过8时由链表转化为红黑树,当红黑树节点小于6时转化为链表。K,V值可以为null默认长度16扩展因子为0.75,当达到(长度*扩展因子时)长度加一倍。

以上四种Map的实现类是平时开发中最常使用的Map,由以上总结可以得知,当我们在单线程条件下,以及对于Key没有排序的要求时,我们可以使用HashMap。

二、HashMap使用的数据结构及源码

一、数据结构

在了解源码之前我们先了解一下HashMap中使用的数据结构?

数组:数组存储区间是连续的,占用连续内存,查找的时间复杂度为O(1)
链表:链表不需要占用连续的内存存储区间,查找的时间复杂度为O(n)
红黑树:红黑树是红黑树是一种平衡二叉查找树,查找的时间复杂度为O(lgn)

二、Node节点

在HashMap类中,维护了一个静态内部类Node,本质就是一个映射(键值对)。

代码如下(示例):

static class Node<K,V> implements Map.Entry<K,V> {
    
    
final int hash; //计算出的hash值,用来定位数组索引位置
final K key;
V value;
Node<K,V> next;//链表的下一个节点

静态内部类Node主要方法
HashMap是使用哈希表进行存储,哈希表为解决Hash冲突,有两种解决办法,开放地址法和链地址法。java中HashMap使用的解决办法为链地址法,链地址法简单来说就是数组加链表的结合,在每个数组元素上都有一个链表结构,当数据被 hash 后,得到数组下标位置,把数据放在对应数组下标元素的链表上。

Map<String,Integer> map = new HashMap<>();
map.put("a",1);

Map调用“a”这个key的hashCode()方法得到其hashCode值,然后通过 hash 算法来计算hash值,如果hash值相同的两个Key,表示发生了 Hash 碰撞,通过尾插法插入链表,来定位该键值对的存储位置。

	//默认的Node[] table数组长度是2^4=16
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
	//Node[] table 最大长度限制为2^30,设置的长度值必须为2的整数次幂
    static final int MAXIMUM_CAPACITY = 1 << 30;
	//负载因子0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
	//当链表节点超过8,将链表转化为红黑树。但是Node[] table 长度没有超过MIN_TREEIFY_CAPACITY时,对数组扩容。
    static final int TREEIFY_THRESHOLD = 8;
	//长度
	transient int size;
	//修改次数
	transient int modCount;
	//当数组长度*负载因子长度超过次阈值就会调用resize进行二倍扩容
	int threshold;

三、Put方法

HashMap是采用懒扩容的方式进行扩容的,也就是说只有调用的put方法时,才会判断是否达到阈值,从而判断是否扩容。
 public V put(K key, V value) {
    
    
        return putVal(hash(key), key, value, false, true);
    }

putVal方法
putVal方法
put方法执行逻辑图:
put执行逻辑

四、Get方法

下面为get方法源码
get方法

三、与其他集合或java版本区别

jdk1.8相对于1.7底层实现发生了一些改变。1.8主要优化减少了Hash冲突 ,提高哈希表的存、取效率。
1.7数据结构是数组+链表,1.8则是数组+链表+红黑树结构。
JDK1.8中resize()方法在表为空时,创建表;在表不为空时,扩容;而JDK1.7中resize()方法负责扩容,inflateTable()负责创建表。
1.8中没有区分键为null的情况,而1.7版本中对于键为null的情况调用putForNullKey()方法。但是两个版本中如果键为null,那么调用hash()方法得到的都将是0,所以键为null的元素都始终位于哈希表table【0】中。
1.7中链表插入方式为头插法,1.8中新增节点采用尾插法。
相较于头插法,尾插法不容易出现环形链表!
1.7中是通过更改hashSeed值修改节点的hash值从而达到rehash时的链表分散,而1.8中键的hash值不会改变,rehash时根据(hash&oldCap)==0将链表分散。
1.8rehash时保证原链表的顺序,而1.7中rehash时有可能改变链表的顺序(头插法导致)。
在扩容的时候:1.7在插入数据之前扩容,而1.8插入数据成功之后扩容。


总结

  • 扩容操作是一个性能消耗非常高的操作,在初始化的时候给入一个合适的值可以避免进行频繁的扩容。
  • 在这里插入代码片负载因子可以修改,但是默认的0.75是Oracle公司经过测试得出的最合适的值,建议不要修改!
    红黑树相比链表的查询时间复杂度优化很多!
  • 总之在多线程环境下常用ConcurrentHashMap,在非多线程环境下常用HashMap。

おすすめ

転載: blog.csdn.net/qq_50641170/article/details/121587771