先说说HashMap
1:首先HashMap 是一个散列表,它存储的内容是键值对(key-value)映射,HashMap中的映射不是有序的。HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。不是线程安全的。HashMap是最常用的Map,它根据HashCode值存储数据,根绝键可以直接获取值。允许有空(null)的键值(key),最多一条记录的键为null。用containsKey判断是否存在键。可以使用Iterator进行遍历。HashMap中hash数组默认大小是16,而且一定是2的指数。HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度默认加载因子是 0.75,。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
为什么是16呢?
因为haspmap计算在数组中index位置是根绝hash&(size-1)进行计算的,如果是16 ,16-1=15它的二进制后四位都是1,在和hash做&操作就等同于hash%size。
哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。
2:存取操作
总的说:HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。”这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Entry。
2.1:向HashMap添加键值对时<key,value>时,需要经过如下步骤:
首先调用key的hashCode()方法生成一个hash值h1,如果不存在直接将<key,value>添加到hashMap中,如果已经存在找出所有HashMap中hash值为h1的key,然后分别调用key的equals()方法判断当前添加的key值是否已经存在相同的key值。如果equals方法返回true说明当前需要添加的key已经存在,那么使用新得value值覆盖旧的value值。如果返回false说明新的key在HashMap不存在,那么在HashMap中创建新的映射关系。
2.2:从hashMap中通过key查找value时,需经历步骤:
首先调用的是key的hashCode()方法判断获取 key的hash值h,这样就可以确定键为key的所有值存储的首地址。如果h对应的key值有多个那么程序会遍历所有key,通过key的equals()方法判断key的内容是否相等,相等返回true时,对应的value是正确的结果。equals()方法比较规则:当参数obj引用的对象与当前对象为同一个对象时返回true。hashCode()返回对象的内存地址。
2.3:如果两个键的hashcode相同,你如何获取值对象?
因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。
我们可以得出一个结论:如果两个对象相等,那么这两个对象有着相同的hashCode。我们可以根据需要重写hashCode和equals方法。
没有重写equals()的put
重写equals()的put
public static void main(String[] args) {
HashMap<String,String > map= new HashMap<String,String > ();
map.put(null,null);
map.put(null,"a");
map.put(null,"a");
map.put(null,"b");
map.put(null,"c");
System.out.println(map.get(null));
}
这段代码输出为c
3:当两个对象的hashcode相同会发生什么? 冲突
3.1什么是hash冲突?由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余,以找到存 放该key的桶的下标时,由于取余是通过与操作完成的,会忽略hash值的高位。因此只有hashCode()的低位参加运算,发生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。
3.2什么时候冲突? 当新增加的key的hash值已经在HasMap中存在时,就会产生冲突。
3.3冲突处理办法? 处理 hash冲突的办法有开放地址法,再hash法。HashMap采用链地址法解决冲突。
3.4避免冲突?使用不可变的声明作final的对象,并采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生提高效率。
3.5 相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。原本Map.Entry接口的实现类Entry改名为了Node。转化为红黑树时改用另一种实现TreeNode。
4:什么是拉链法(链地址法)?
拉链法又叫链地址法,Java中的HashMap在存储数据的时候就是用的拉链法来实现的,拉链发就是把具有相同散列地址的关键字(同义词)值放在同一个单链表中。拉链法的工作原理:
HashMap<String, String> map = new HashMap<>(); map.put("K1", "V1"); map.put("K2", "V2"); map.put("K3", "V3");
- 新建一个 HashMap,默认大小为 16;
- 插入 <K1,V1> 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。
- 插入 <K2,V2> 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。
- 插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 前面。
应该注意到链表的插入是以头插法方式进行的,例如上面的 <K3,V3> 不是插在 <K2,V2> 后面,而是插入在链表头部。
HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。
6:如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
扩容是是新建了一个HashMap的底层数组,而后调用transfer方法,将就HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,扩容前后,哈希桶的长度一定会是2的次方,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。。
扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。
因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量
如果追加节点后,链表数量》=8,则转化为红黑树
由迭代器的实现可以看出,遍历HashMap时,顺序是按照哈希桶从低到高,链表从前往后,依次遍历的。属于无序集合。
代码:
void resize(int newCapacity) { //传入新的容量
Entry[] oldTable = table; //引用扩容前的Entry数组
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) { //扩容前的数组大小如果已经达到最大(2^30)了
threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
return;
}
Entry[] newTable = new Entry[newCapacity]; //初始化一个新的Entry数组
transfer(newTable); //!!将数据转移到新的Entry数组里
table = newTable; //HashMap的table属性引用新的Entry数组
threshold = (int) (newCapacity * loadFactor);//修改阈值
}
void transfer(Entry[] newTable) {
Entry[] src = table; //src引用了旧的Entry数组
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
Entry<K, V> e = src[j]; //取得旧Entry数组的每个元素
if (e != null) {
src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
do {
Entry<K, V> next = e.next;
int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
e.next = newTable[i]; //标记[1]
newTable[i] = e; //将元素放在数组上
e = next; //访问下一个Entry链上的元素
} while (e != null);
}
}
}
static int indexFor(int h, int length) {
return h & (length - 1);
}
--------------------- 代码来自网上的博客
9 与HashTable的区别
与之相比HashTable是线程安全的,且不允许key、value是null。
HashTable默认容量是11。
HashTable是直接使用key的hashCode(key.hashCode())作为hash值,不像HashMap内部使用static final int hash(Object key)扰动函数对key的hashCode进行扰动后作为hash值。
HashTable取哈希桶下标是直接用模运算%.(因为其默认容量也不是2的n次方。所以也无法用位运算替代模运算)
扩容时,新容量是原来的2倍+1。int newCapacity = (oldCapacity << 1) + 1;
Hashtable是Dictionary的子类同时也实现了Map接口,HashMap是Map接口的一个实现类;
参考: https://blog.csdn.net/xx123698/article/details/60766072
原文:https://blog.csdn.net/zxt0601/article/details/77413921
版权声明:本文为博主原创文章,转载请附上博文链接!