hashCode和equals
hashCode和equals,hashMap相关源码解读
equals
在初学java的时候,我们可能会被告知在java中使用 ==
是地址比较,使用 equals
是值比较,那么为什么是这样的呢?
我们先看看equals
方法,equals
方法是来自Object
类中的一个方法,也就是说所有的类都有这个方法。
public boolean equals(Object obj) {
return (this == obj);
}
可以看见,默认的equals
方法实现用的也是 ==
,也就是说如果我们创建了一个类,如果不重写equals
方法,那么实际上我们使用的还是 ==
,也就是地址比较。
地址比较
在java中每个对象引用都指向对象存在于JVM堆中的内存地址,也就是不同的两个实例化对象的地址也是不相同的。
public class Test {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
System.out.println(o1 == o2); //false
System.out.println(o1.equals(o2));//false
}
}
那么为什么说equals就是值比较呢?其实在java中,在定义类时是推荐我们去重写equals
和hashCode
方法的。
equals
方法其实是让用户去自己定义类的比较方法具体实现,举个例子,在下面的代码中两个Integer
对象直接使用 ==
比较是不同的说明它们的地址不同,是堆中的两个对象。
使用equals
方法则返回的是true,那么我们可以肯定的是Integer
类一定是重写了equals
方法,不再是进行地址比较。
public class Test {
public static void main(String[] args) {
//这里我们显式的new两个不同的Integer对象进行比较
Integer i1 = new Integer(0);
Integer i2 = new Integer(0);
System.out.println(i1 == i2); //false
System.out.println(i1.equals(i2)); //true
}
}
实际上阅读源码我们可以知道,Integer
使用equals
比较比较的不再是对象地址,而是包装类的内部静态属性value
,这是装饰器模式的很好实践。
private final int value;
public int intValue() {
return value;
}
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
也就是equals
方法实际上是开放给用户实现的,让用户自己去定义两个类是否相等的比较逻辑,如果我们在设计Integer
类时,也一定是按照常用的逻辑去比较整型的值,而不是去比较地址,这对于整型equals的具体实现是没有意义的。
那么为什么常说如果要实现equals
方法就要一起把hashCode
方法也实现了?
hashCode
hashCode方法也是Object中定义的一个方法,使用hash算法来生成对象的哈希码值,是根据对象的内存地址生成的。
返回对象的哈希代码值。支持此方法是为了使用哈希表,例如HashMap提供的哈希表hashCode的一般约定是:每当在Java应用程序的执行过程中对同一对象多次调用时,hashCode方法必须始终返回相同的整数,前提是在对象的equals比较中使用的信息未被修改。
- 上面是摘录来自JDK的源码注释,简单来说就是只要
equals
方法比较的结果是相同的,那么对于的hashCode
方法生成当前对象的哈希值也需要是相同的。
在Integer
类中hashCode
的实现就是直接返回当前整型类所表示的整型值value
,因此equals
方法和hashCode
方法所使用的都是同一个value值,所以只要value值不变,两个代表不同整数的Integer
的hashCode
值也是相同的。
public class Test {
public static void main(String[] args) {
//这里我们显式的new两个不同的Integer对象进行比较
Integer i1 = new Integer(0);
Integer i2 = new Integer(0);
System.out.println(i1.hashCode());//0
System.out.println(i2.hashCode());//0
}
}
源码中还提到hashCode
是为了支持hashMap
使用的,那么我们再来看看hashMap
中是如何使用hashCode
的
我们知道HashMap底层存放数据的数据结构其实是链表/红黑树,也就是最后都是存在某个Node
上的,我们看下hashMap
中Node
的相关属性
- hash 这个是该Node的key的hash运算后的值
- key 这个就是key所代表对象的引用
- value 这个是value所代表对象的引用
- next 这个是存储该节点的下一个节点
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
我们只关注hashCode是如何使用的,也就是我们具体看hash这个属性的作用
追踪HashMap.put(key,value)方法
这是hashMap
中put
方法生成新节点的代码,它需要提供一个hash
值,根据源码可知该hash
值是hashMap
中静态hash
方法对key
的hash
运算后得到的结果。
put方法入口
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
hashMap中的hash方法
本质上还是调用了key
的hashCode
方法,然后又做了一次hash运算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
具体的putVal实现算法,省略了其它代码
这里我们只看newNode构造方法,因此我们可以确定的是Node中的hash值就是由key对象的hashCode
方法提供的
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)
//....
if ((p = tab[i = (n - 1) & hash]) == null)
//....
else {
Node<K,V> e; K k;
if (p.hash == hash &&
//....
else if (p instanceof TreeNode)
//....
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null); //定位到生成Node对象
//....
}
//....
}
}
//....
}
//....
return null;
}
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
追踪HashMap.get(key)方法
那么get方法是如何使用hashCode的,我们知道get方法其实就是拿到key对应的value对象,那么hashMap是如何确定外界传入的key值和value所在Node的key值是相同的
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里是具体的getNode算法实现,我们直接定位最核心的源码部分
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
就是下面这条语句
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
我们可以看到,比较外界key值和内部Node的key值,首先是这一句first.hash == hash
,比较的是当前节点的hash属性与外界传入key的hash值是否相同,也就是说hashMap判断两个对象是否相同,首先会判断hash值是否相同如果hash值相同则会直接返回该Node。
首先看的是该对象
hashCode()
方法返回的值是否是相同的。如果
hashCode()
不相同,则无需进行进一步判断如果相同,才会比较对象的地址或满足对象的
equals
方法也被视为相同的对象
由此我们回到刚才的问题为什么实现equals也要实现hashCode
还是刚才的Integer
为例,我们假设Integer
类只实现了equals
方法而没有实现hashCode
方法,那么在hashMap
中判断两个Integer
对象是否相同使用的就是Object
类中的hashCode
方法也就是地址。
这样子就导致不管我们equals
方法如何定义,只要对象不同,hashMap
就不会调用equals
方法,这和我们实现equals
方法的初衷是违背的。
所以一句话来说就是,hashCode
方法是hashMap
判断对象是否相同的首要依据,因此如果我们想让hashMap
等集合按照我们的预期正常的运作,我们实现equals
方法时也要记得实现hashCode
方法。
下面是实现hashCode
和equals
方法的例子以及实现equals
但未实现hashCode
的例子
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class Test {
public static void main(String[] args) {
//1.实现hashCode和equals方法的例子
//业务上我们视e1和e2是相同的
Example1 e1 = new Example1("例1");
Example1 e2 = new Example1("例1");
System.out.println(e1.equals(e2));//true
Map<Example1,Example1> hashMap = new HashMap<>();
hashMap.put(e1,e1);
System.out.println(hashMap.get(e2));
//2.未实现hashCode方法的例子
//业务上我们视e3和e4是相同的
Example2 e3 = new Example2("例2");
Example2 e4 = new Example2("例2");
System.out.println(e1.equals(e2));//true
Map<Example2,Example2> hashMap2 = new HashMap<>();
hashMap2.put(e3,e3);
System.out.println(hashMap2.get(e4));//null
}
}
class Example1{
private String id;
public Example1(String id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Example1 example1 = (Example1) o;
return Objects.equals(id, example1.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return "Example1{" +
"id='" + id + '\'' +
'}';
}
}
class Example2{
private String id;
public Example2(String id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Example2 example2 = (Example2) o;
return Objects.equals(id, example2.id);
}
@Override
public String toString() {
return "Example2{" +
"id='" + id + '\'' +
'}';
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以邮件至 1300452403@qq.com