HashMap实现原理

HashMap的实现与优化

HashMap的优化与实践

HashMap的复杂度

如图是ArrayList/LinkedList/HashMap三个数据结构的复杂度对比,可以看出HashMap整体上性能都非常不错,但是不稳定,为O(N/Buckets),N就是以数组中没有发生碰撞的元素,Buckets是因碰撞产生的链表。

 获取查找添加/删除空间 
ArrayListO(1)O(1)O(N)O(N)
LinkedListO(N)O(N)O(1)O(N)
HashMapO(N/Bucket_size)O(N/Bucket_size)O(N/Bucket_size)O(N)

注:发生碰撞实际上是非常稀少的,所以N/Bucket_size约等于1

HashMap是对Array与Link的折衷处理,Array与Link可以说是两个速度方向的极端,Array注重于数据的获取,而处理修改(添加/删除)的效率非常低;Link由于是每个对象都保持着下一个对象的指针,查找某个数据需要遍历之前所有的数据,所以效率比较低,而在修改操作中比较快。

复杂度是如何考察的?

对于数据结构,在时间上我们需要考察Acessing ,Search, Deletion/Insertion的平均与最差的复杂度。在空间上,我们要考虑维护这个数据结构所占用的内存空间。

常见的数据结构与排序的复杂度都在这里

HashMap的实现

本文以JDK8的API实现进行分析

1. 什么是hash,什么是碰撞?

  • Hash:是一种信息摘要算法,它还叫做哈希,或者散列,但是它不是加密。我们平时使用的MD5,SHA1,SSL中的公私钥验证都属于Hash算法,通过输入key进行Hash计算,就可以获取key的HashCode(),比如我们通过校验MD5来验证文件的完整性。

  • 碰撞:好的Hash算法可以出计算几乎出独一无二的HashCode,如果出现了重复的hashCode,就称作碰撞;

就算是MD5这样优秀的算法也会发生碰撞,即两个不同的key也有可能生成相同的MD5。

2. HashMap中是如何实现写入与读取的?

HashMap实现了Map接口,保存着K-V这样的集合。我们以put操作为例

2.1. 对key进行Hash计算

在JDK8中,由于使用了红黑树来处理大的链表开销,所以hash这边可以更加省力了,只用计算hashCode并移动到低位就可以了

static final int hash(Object key) {
    int h;
    //计算hashCode,并无符号移动到低位
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

举个例子: 363771819^(363771819 >>> 16)

0001 0101 1010 1110 1011 0111 1010 1011(363771819)
0000 0000 0000 0000 0001 0101 1010 1110(5550) XOR
--------------------------------------- =
0001 0101 1010 1110 1010 0010 0000 0101(363766277)

这样做可以实现了高地位更加均匀地混到一起,详见这里

下面给出几个常用的哈希码(hashCode)的算法。

  1. Object类的hashCode. 返回对象的经过处理后的内存地址,由于每个对象的内存地址都不一样,所以哈希码也不一样。这个是native方法,取决于JVM的内部设计,一般是某种C地址的偏移。
  2. String类的hashCode. 根据String类包含的字符串的内容,根据一种特殊算法返回哈希码,只要字符串的内容相同,返回的哈希码也相同。
  3. Integer等包装类,返回的哈希码就是Integer对象里所包含的那个整数的数值,例如Integer i1=new Integer(100),i1.hashCode的值就是100 。由此可见,2个一样大小的Integer对象,返回的哈希码也一样。
  4. int,char这样的基础类,它们不需要hashCode,如果需要存储时,将进行自动装箱操作,计算方法同上。
2.2. 获取到当前的位置

计算了Hash,我们现在要把它插入数组中了

i = (tab.length - 1) & hash;

通过位运算,确定了当前的位置,因为HashMap数组的大小总是2^n,所以实际的运算就是 (0xfff...ff) & hash ,这里的tab.length-1相当于一个mask,滤掉了大于当前长度位的hash,使每个i都能插入到数组中。

2.3. 生成包装类

这个对象是一个包装类,Node<K,V>,内部有key,value,hash还有next,可以看出来它是一个链表。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        //getter and setter .etc.
}
2.4. 插入包装类到数组

(1). 如果输入当前的位置是空的,就插进去,如图,左为插入前,右为插入后

0           0
|           |
1 -> null   1 - > null
|           |
2 -> null   2 - > null
|           | 
..-> null   ..- > null
|           | 
i -> null   i - > new node
|           |
n -> null   n - > null

(2). 如果当前位置已经有了node,且它们发生了碰撞,则新的放到前面,旧的放到后面,这叫做链地址法处理冲突。

0           0
|           |
1 -> null   1 - > null
|           |
2 -> null   2 - > null
|           | 
..-> null   ..- > null
|           | 
i -> old    i - > new - > old
|           |
n -> null   n - > null

我们可以发现,失败的hashCode算法会导致HashMap的性能由数组下降为链表,所以想要避免发生碰撞,就要提高hashCode结果的均匀性。当然,在JDK8中,采用了红黑二叉树进行了处理,这个我们后面详细介绍。

>

什么是Hash攻击?

通过请求大量key不同,但是hashCode相同的数据,让HashMap不断发生碰撞,硬生生的变成了SingleLinkedList

0
|
1 -> a ->b -> c -> d(撞!撞!撞!复杂度由O(1)变成了O(N))
|
2 -> null(本应该均匀分布,这里却是空的)
|
3 -> null
|
4 -> null

这样put/get性能就从O(1)变成了O(N),CPU负载呈直线上升,形成了放大版DDOS的效果,这种方式就叫做hash攻击。在Java8中通过使用TreeMap,提升了处理性能,可以一定程度的防御Hash攻击。

3. 扩容

如果当表中的75%已经被占用,即视为需要扩容了

(threshold = capacity * load factor ) < size

它主要有两个步骤:

1. 容量加倍

左移1位,就是扩大到两倍,用位运算取代了乘法运算

newCap = oldCap << 1;
newThr = oldThr << 1;
2. 遍历计算Hash
for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //如果发现当前有Bucket
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //如果这里没有碰撞
                    if (e.next == null)
                        //重新计算Hash,分配位置
                        newTab[e.hash & (newCap - 1)] = e;
                    //这个见下面的新特性介绍,如果是树,就填入树
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //如果是链表,就保留顺序....目前就看懂这点
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }

由此可以看出扩容需要遍历并重新赋值,成本非常高,所以选择一个好的初始容量非常重要。

如何提升性能?

  1. 解决扩容损失:如果知道大致需要的容量,把初始容量设置好以解决扩容损失;
    比如我现在有1000个数据,需要 1000/0.75 = 1333 个坑位,又 1024 < 1333 < 2048,所以最好使用2048作为初始容量。

  2. 解决碰撞损失:使用高效的HashCode与loadFactor,这个...由于JDK8的高性能出现,这儿问题也不大了。

  3. 解决数据结构选择的错误:在大型的数据与搜索中考虑使用别的结构比如TreeMap,这个就是知识积累了。一般需要key排序时,建议使用TreeMap,本文暂不讨论;

HashMap与HashTable的主要区别

在很多的Java基础书上都已经说过了,他们的主要区别其实就是Table全局加了线程同步保护

  • HashTable线程更加安全,代价就是因为它粗暴的添加了同步锁,所以会有性能损失。
  • 其实有更好的concurrentHashMap可以替代HashTable,一个是方法级,一个是Class级

JDK8中HashMap的新特性

如果某个桶中的链表记录过大的话(当前是TREEIFY_THRESHOLD = 8),就会把这个链动态变成红黑二叉树,使查询最差复杂度由O(N)变成了O(logN)。

//e 为临时变量,p为当前的链
for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == 


HashMap的整体结构如下

  

  简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度依然为O(1),因为最新的Entry会插入链表头部,只需要简单改变引用链即可,而对于查找操作来讲,此时就需要遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

转自:http://www.cnblogs.com/chengxiao/p/6059914.html


所以最终存储位置的确定流程是这样的:

h=hash()怎么计算的?为什么下面例子中(17,“www”)算出的hash=16,存储位置放到table[0]中?

解答:hash()函数实现源码:

    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // 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);
    }

有上述:

hash(16)=1;

hash(17)=0;

public static void main(String[] args){
		HashMap<Integer, String> hashMap=new HashMap<Integer, String>();
		hashMap.put(0, "000");
		hashMap.put(1, "aaa");
		hashMap.put(2, "bbb");
		hashMap.put(3, "ccc");
		hashMap.put(1, "ddd");
		hashMap.put(16, "qqq");
		hashMap.put(17, "www");
		for (Map.Entry<Integer, String> entry:hashMap.entrySet()) {
			System.out.println(entry.getKey()+" "+entry.getValue());
		}


	} 

hash










内容概要:本文档提供了关于“微型车间生产线的设计与生产数据采集试验研究”的毕业设计复现代码,涵盖从论文结构生成、机械结构设计、PLC控制系统设计、生产数据采集与分析系统、有限元分析、进度管理、文献管理和论文排版系统的完整实现。通过Python代码和API调用,详细展示了各个模块的功能实现和相互协作。例如,利用SolidWorks API设计机械结构,通过PLC控制系统模拟生产流程,使用数据分析工具进行生产数据的采集和异常检测,以及利用进度管理系统规划项目时间表。 适合人群:具有机械工程、自动化控制或计算机编程基础的学生或研究人员,尤其是从事智能制造领域相关工作的人员。 使用场景及目标:①帮助学生或研究人员快速搭建和理解微型车间生产线的设计与实现;②提供完整的代码框架,便于修改和扩展以适应不同的应用场景;③作为教学或科研项目的参考资料,用于学习和研究智能制造技术。 阅读建议:此资源不仅包含详细的代码实现,还涉及多个学科领域的知识,如机械设计、电气控制、数据分析等。因此,在学习过程中,建议读者结合实际操作,逐步理解每个模块的功能和原理,并尝试调整参数以观察不同设置下的系统表现。同时,可以参考提供的文献资料,深入研究相关理论和技术背景。
本次的学生体质健康信息管理网站,按照用户的角色可以分为教师与学生,后台设置管理员角色来对学生的信息进行管理。,设计如下: 1、后台管理系统 后台管理系统主要是为该系统的管理员提供信息管理服务的系统,具体包括的功能模块如下: (1)管理员信息管理 (2)教师信息管理 (3)学生信息管理 (4)健康信息统计(图形化进行健康,亚健康等学生的信息数量统计) 2、教师角色的功能模块设计 教师角色所需要的功能模块主要包括了如下的一些内容: (1)个人资料修改 (2)学生体质健康管理:录入相关数据,包括但不限于身高、体重、肺活量、视力等生理指标以及运动能力、身体成分、骨密度等健康指标,并且设置健康,亚健康状态 (3)学生健康建议:根据体质信息,进行学生健康的建议 (4)健康预警:对健康出问题的学生,进行健康预警 (5)饮食和锻炼情况管理,查看 3、学生角色 学生角色可以通过该信息网站看到个人的基本信息,能够看到教师给与学生的健康建议等,功能模块设计如下: (1)个人资料修改 (2)我的健康建议查看 (3)我的健康预警 (4)饮食和锻炼情况管理,记录平时的饮食和锻炼情况 完整前后端源码,部署后可正常运行! 环境说明 开发语言:Java后端 框架:ssm,mybatis JDK版本:JDK1.8+ 数据库:mysql 5.7+ 数据库工具:Navicat11+ 开发软件:eclipse/idea Maven包:Maven3.3+ 部署容器:tomcat7.5+
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值