Java中HashMap的实现原理
Java中的hashCode和equals
关于hashCode
- hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的
- 如果两个对象相同,就是适用于equals(java.lang.Object) 方法,那么这两个对象的hashCode一定要相同
- 如果对象的equals方法被重写,那么对象的hashCode也要重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点
- 两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里“
再归纳一下就是hashCode是用于查找使用的,而equals是用于比较两个对象的是否相等的。
以下对hashCode的解读摘自其他博客:
1.hashcode是用来查找的。
例如内存中有这样的位置
0 1 2 3 4 5 6 7
而我有个类,这个类有个字段叫ID,我要把这个类存放在以上8个位置之一,如果不用hashcode而任意存放,那么当查找时就需要到这八个位置里挨个去找,或者用二分法一类的算法。
但如果用hashcode那就会使效率提高很多。
我们这个类中有个字段叫ID,那么我们就定义我们的hashcode为ID%8,然后把我们的类存放在取得得余数那个位置。比如我们的ID为9,9除8的余数为1,那么我们就把该类存在1这个位置,如果ID是13,求得的余数是5,那么我们就把该类放在5这个位置。这样,以后在查找该类时就可以通过ID除 8求余数直接找到存放的位置了。
2.但是如果两个类有相同的hashcode怎么办那(我们假设上面的类的ID不是唯一的),例如9除以8和17除以8的余数都是1,那么这是不是合法的,回答是:可以这样。那么如何判断呢?在这个时候就需要定义 equals了。
也就是说,我们先通过 hashcode来判断两个类是否存放某个桶里,但这个桶里可能有很多类,那么我们就需要再通过 equals 来在这个桶里找到我们要的类。
那么。重写了equals(),为什么还要重写hashCode()呢?
想想,你要在一个桶里找东西,你必须先要找到这个桶啊,你不通过重写hashcode()来找到桶,光重写equals()有什么用啊
关于equals
1.equals和==
== 用于比较引用和比较基本数据类型时具有不同的功能:
比较基本数据类型,如果两个值相同,则结果为true
而在比较引用时,如果引用指向内存中的同一对象,结果为true;
equals()作为方法,实现对象的比较。由于 == 运算符不允许我们进行覆盖,也就是说它限制了我们的表达。因此我们重写equals()方法,达到比较对象内容是否相同的目的。而这些通过 == 运算符是做不到的。
2.object类的equals()方法的比较规则为:如果两个对象的类型一致,并且内容一致,则返回true,这些类有:
java.io.file,java.util.Date,java.lang.string,包装类(Integer,Double等)
String s1=new String(“abc”);
String s2=new String(“abc”);
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
运行结果为false true
3.为了实现在向HashMap中添加键值对,可以根据对象的内容来判断两个对象是否相等,这就需要重写hashCode()方法和equals()方法,实例如下:
package com.base.hashmap;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class hashmap {
public static void main(String[] args) {
HashMap<Person, String> hm = new HashMap<Person, String>();
Person p1 = new Person("111", "name1");
Person p2 = new Person("111", "name1");
hm.put(p1, "address1");
hm.put(p2, "address2");
Iterator iter = hm.entrySet().iterator();
while(iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Person key = (Person) entry.getKey();
String val = (String) entry.getValue();
System.out.println("key="+key+" value:"+val);
}
}
}
class Person{
String id;
String name;
public Person(String id,String name) {
this.id = id;
this.name = name;
}
//hashCode,equals和toString方法是source-》自动生成的
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
@Override
public String toString() {
return "Person [" + (id != null ? "id=" + id + ", " : "") + (name != null ? "name=" + name : "") + "]";
}
}
输出结果如下:
key=Person [id=111, name=name1] value:address2
HashMap的节点
HashMap是一个集合,键值对的集合,源码中每个节点用Node<K,V>表示
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
...
}
Node是一个内部类,这里的key为键,value为值,next指向下一个元素,可以看出HashMap中的元素不是一个单纯的键值对,还包含下一个元素的引用。
HashMap的数据结构
HashMap的数据结构为 数组+(链表或红黑树),上图:
为什么采用这种结构来存储元素呢?
数组的特点:查询效率高,插入,删除效率低。
链表的特点:查询效率低,插入删除效率高。
在HashMap底层使用数组加(链表或红黑树)的结构完美的解决了数组和链表的问题,使得查询和插入,删除的效率都很高。
HashMap存储元素的过程
有这样一段代码:
HashMap<String,String> map = new HashMap<String,String>();
map.put("刘德华","张惠妹");
map.put("张学友","大S");
现在我要把键值对 “刘德华”,”张惠妹”存入map:
**第一步:**计算出键“刘德华”的hashcode,该值用来定位要将这个元素存放到数组中的什么位置。
调用这个方法会生成一个int型的整数,我们叫它哈希码,哈希码和调用它的对象地址和内容有关。
通过hashcode值和数组长度取模我们可以得到元素存储的下标。
刘德华的hashcode为33,数组长度为16,则要存储在数组索引为 33%16=1的地方。
可以分两种情况:
-
数组索引为1的地方是空的,这种情况很简单,直接将元素放进去就好了。
-
已经有元素占据了索引为1的位置,这种情况下我们需要判断一下该位置的元素和当前元素是否相等,使用equals来比较。
如果使用默认的规则是比较两个对象的地址。也就是两者需要是同一个对象才相等,当然我们也可以重写equals方法来实现我们自己的比较规则最常见的是通过比较属性值来判断是否相等。
如果两者相等则直接覆盖,如果不等则在原元素下面使用链表的结构存储该元素。
每个元素节点都有一个next属性指向下一个节点,这里由数组结构变成了数组+链表结构,红黑树又是怎么回事呢?
因为链表中元素太多的时候会影响查找效率,所以当链表的元素个数达到8的时候使用链表存储就转变成了使用红黑树存储,原因就是红黑树是平衡二叉树,在查找性能方面比链表要高。
HashMap中的两个重要的参数
HashMap中有两个重要的参数:初始容量大小和加载因子。初始容量大小是创建时给数组分配的容量大小,默认值为16,用数组容量大小乘以加载因子得到一个值,一旦数组中存储的元素个数超过该值就会调用resize方法将数组容量增加到原来的两倍,专业术语叫做扩容。
那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为216=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.751000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。
在做扩容的时候会生成一个新的数组,原来的所有数据需要重新计算哈希码值重新分配到新的数组,所以扩容的操作非常消耗性能。
创建HashMap时我们可以通过合理的设置初始容量大小来达到尽量少的扩容的目的。加载因子也可以设置,但是除非特殊情况不建议设置。
总结
总结:HashMap的实现原理:
- 利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
- 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
- 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
- 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。