前言:
HashMap的包结构
*注意,此实现不是同步的。
好吧“哈希表”是啥,内部结构是什么样的?
哈希表:
什么是哈希表?
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
散列函数的构造方法: (1)直接定址法 (2)除留余数法 (3)平方取中法 (4)折叠法 (5)数值分析法
构造散列函数的目的是减少冲突,但要完全避免冲突是不可能的,只能尽可能减少冲突。 处理冲突的方法: (1)开放定址法 (2)二次探测法 (3)链地址法(拉链法)
HashMap是怎么实现散列函数和处理冲突的?
public static void main(String[] args) {
HashMap<String, String> blogMap=new HashMap<>();
blogMap.put("博文测试", "博文测试Value");
blogMap.get("博文测试");//此行打断点
}
观察Variables,如下图
在图中我们看到了,table是一个数组,看看我们数据在数组中是什么样的
数组索引为9,该位置下存储的数据节点为,看到关键字next,该结构这样看应该是一个数组加链表没错了,那么这样映射了hash表中哪种解决冲突的方法了,对比刚刚列出的几种方法,结构一致的只有拉链法了。
拉链法的简单介绍:
拉链法,我们可以理解为“链表的数组”,他是一种数组加链表的结构,其数据结构如图:
拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1。
*->具体构造过程(动画演示:
点击打开链接) 草鸡生动形象啊!,简单生动,建议耐心看完
HashMap 拉链法的实现分析
来我们看看,hashMap是如何计算key的hash的,并如何决定将value放置到具体的数组索引中的,打开源码(可配合Java6的中文API文档,文档注释是人工翻译比较准确,笔者开的是Java8的源码)Java Doc
在线地址(http://tool.oschina.net/uploads/apidocs/jdk-zh/java/util/HashMap.html)
HashMap中的m(散列表长度)和a(装载因子)
JDK源码中定义如下默认值
默认加载因子:
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;默认散列表长度:
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
HashMap中的PUT方法
我们先从HashMap最常用的两个方法(put,get)为入口对代码进行分析
.put方法源码:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);//我们看到在方法内部对key做了一次hash运算,然后调用putVal方法,我们暂且不关心后两个参数在干嘛,回头解释
}
由以上源码,我们知道,在将key和value放入到hashMap中以前,先对key做了一次hash运算,hash算法是什么样的呢
static final int hash(Object key) {//先判断key是否为null,如果为null,hash值为0,否则以hashCode方法值为准
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
*以上引出一个知识点,这就是为什么重写equals方法时,为什么强烈建议重写hashCode方法的原因了
1、集合类判断两个对象是否相等,是先判断equals是否相等,如果equals返回TRUE,还要再判断HashCode返回值是否ture,只有两者都返回ture,才认为该两个对象是相等的。
2、由于Object的hashCode返回的是对象的hash值,所以即使equals返回TRUE,集合也可能判定两个对象不等,所以必须重写hashCode方法,以保证当equals返回TRUE时,hashCode也返回Ture,这样才能使得集合中存放的对象唯一。
2、由于Object的hashCode返回的是对象的hash值,所以即使equals返回TRUE,集合也可能判定两个对象不等,所以必须重写hashCode方法,以保证当equals返回TRUE时,hashCode也返回Ture,这样才能使得集合中存放的对象唯一。
接着来,进入到putVal方法内部
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) //这一行的意思是如果table(主存储结构)没有初始话,则初始化一个数组并赋值给tab,初始化代码在resize方法中
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) //根据散列值算出数组索引((n - 1) & hash)并判断该索引下是否有值,如果没有则新建一个首节点
tab[i] = newNode(hash, key, value, null);
else {//位置下有值
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//如果该节点的key一致,那么e=p
e = p;
else if (p instanceof TreeNode) //当为红黑树时相关处理
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { //如果p的next指针为null
p.next = newNode(hash, key, value, null); //为p后面添加一个节点
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //如果链表过长时,转换成红黑树。这个值表示当某个箱子中,链表长度大于 8 时,有可能会转化成树。
treeifyBin(tab, hash);//转换方法
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value; // 新值替换旧值,并返回旧值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold) //map里的元素个数(size)大于一个阈值(threshold)时,map将自动扩容
resize();
afterNodeInsertion(evict);
return null;
}
可以发现HashMap通过键的hashCode来快速的存取元素。
当不同的对象hashCode发生碰撞时,HashMap通过单链表来解决,将新元素加入链表表头,通过next指向原有的元素。单链表在Java中的实现就是对象的引用(复合)。
当不同的对象hashCode发生碰撞时,HashMap通过单链表来解决,将新元素加入链表表头,通过next指向原有的元素。单链表在Java中的实现就是对象的引用(复合)。
综上一次put的大概逻辑是
1.计算key的hashCode
2.根据hashCode计算bucketIndex( (DEFAULT_INITIAL_CAPACITY - 1) & hash)
在bucketIndex中的处理分三种情形
3.1 BucketIndex没有元素时,直接添加节点
3.2 对象相同时会替换原有KEY
3.3 BucketIndex位置有元素时
到这里,我们了解了HashMap工作原理的一部分,那还有另一部分,如,加载因子及resize(),HashMap通常的使用规则,这些会留在下一章说明。
本文深入探讨了HashMap的工作原理,包括其内部结构、散列函数构造方法、冲突处理方式以及拉链法的具体实现。通过实例分析了put方法的过程,揭示了HashMap如何通过键的hashCode快速存取元素。
880

被折叠的 条评论
为什么被折叠?



