HashMap的工作原理

参考:

HashMap的工作原理
干货:HashMap的工作原理解析

前提

已经很久没有写博客了,现在有时间再重新总结一下自己学习的东西,准备从基础开始重新整理java的基础知识。总结的文章是看了很多大神的文章,摘抄他们,自己整理一下,便于自己学习。自己整理一遍比我单纯看更能加深印象,而且也为以后的学习做一个积累。感谢各位大神的文章对我的帮助。此文章不为CSDN积分。

一、HashMap的使用及特性

HashMap可以接受null键值和值,HashMap是非synchronized,HashMap储存的是键值对。

二、HashMap的工作原理

1.基本原理

HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。HashMap是在bucket中储存键对象和值对象,作为Map.Entry。

HashMap的数据结构是基于数组和链表的。以数组存储元素,如有hash相同的元素,在数组结构中,创建链表结构,再把hash相同的元素放到链表的下一个节点。

2.HashMap的主要方法

equals:是否同一个对象实例。注意,是“实例”。比如String s = new String(“test”); s.equals(s), 这就是同一个对象实例的比较;

等号(==):对比对象实例的内存地址(也即对象实例的ID),来判断是否是同一对象实例;又可以说是判断对象实例是否物理相等;

Hashcode:我觉得可以这样理解:并不是对象的内存地址,而是利用hash算法,对对象实例的一种描述符(或者说对象存储位置的hash算法映射)——对象实例的哈希码。

3.HashMap的一些问题

(1)当两个对象的hashcode相同会发生什么?

hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。

hashMap的结构类似这样
元素0–>[hashCode=0, key.value=x1的数据]
元素1–>[hashCode=1, key.value=y1的数据]
。。。。。。
元素n–>[hashCode=n, key.value=z1的数据]

假设没有hashCode=1的元素加入,但是有两个hashCode=0的数据,它的结构就变成这样
元素0–>[hashCode=0, key.value=x1的数据].next–>[hashCode=0, key.value=x2的数据]
元素1–>[null]
……
元素n–>[hashCode=n, key.value=z1的数据]

(2)如果两个键的hashcode相同,你如何获取值对象?

当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

(3)如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

hashMap默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。

(4)为什么String, Interger这样的wrapper类适合作为键?

String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

(5)我们可以使用自定义的对象作为键吗?

你可能使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入到Map中之后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。

(6)我们可以使用CocurrentHashMap来代替Hashtable吗?

Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。

(7)为什么需要使用Hashcode?

Java中的集合(Collection)有两类,一类是List,再有一类是Set。前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。那么这里就有一个比较严重的问题了:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?这就是 Object.equals方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。

于是,Java采用了哈希表的原理。哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。可以这样简单理解,hashCode方法实际上返回的就是对象存储位置的映像。

这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就能定位到它应该放置的存储位置。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就表示发生冲突了,散列表对于冲突有具体的解决办法,但最终还会将新元素保存在适当的位置。这样一来,实际调用equals方法的次数就大大降低了,几乎只需要一两次。

4.HashMap中涉及的知识点

(1)hashing的概念
(2)HashMap中解决碰撞的方法
(3)equals()和hashCode()的应用,以及它们在HashMap中的重要性
(4)不可变对象的好处
(5)HashMap多线程的条件竞争
(6)重新调整HashMap的大小

三、总结

HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。

当两个不同的键对象的hashcode相同时, 它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。

参考文章

http://www.importnew.com/7099.html (HashMap的工作原理)

http://www.importnew.com/28263.html (Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析)

http://blog.csdn.net/yanlove_jing/article/details/51637766 (hashMap的原理 深入理解)

http://www.importnew.com/28263.html (Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析)

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 点我我会动 设计师:上身试试 返回首页