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