在Java中,equals()方法与hashCode方法定义在java.lang.Object类中,意味着所有的类都会默认有这两个方法。我们来看一下这两个方法在Object类中的定义:
public boolean equals(Object obj) {
return (this == obj);
}
public native int hashCode();
可以看到,euqals方法是用this和参数对象obj用==做比较,在这里比较的是两个对象的引用,如果两个对象的引用相同,那么返回true,即两个对象是相等的。hashCode()方法是一个本地方法,它是根据对象的内存地址导出的一个hash值(对象的存储地址)。
在实际应用中,比较两个对象的引用是否相同来确定两个对象是否相等是没有任何意义的,考虑如下代码:
public class Person{
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
// 省略get,set方法
}
public class Test{
public static void main(String[] args){
Person sanmao = new Person("sanmao",15);
Person zhangsan = new Person("sanmao",15);
System.out.println(sanmao.equals(zhangsan));
}
}
上面我们定义了一个Person类,有两个属性,name名称和age年龄。我们在main()方法中定义了两个Person,他们的名字和年龄都是一样的,那么在实际抽象中我们认为sanmao和zhangsan指的是同一个人,但实际结果上面的运行结果却输出的是false。因为我们在Person类中没有重写继承自Object中的equals()方法,实际比较的是两个对象的引用是否相同,很明显我们定义了两个对象,当然输出的是false。为了实现我们想要的逻辑,应该修改Person类,重写equals()方法,定义自己的比较逻辑:
public class Person{
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
// 省略get,set方法
@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;
return Objects.equals(name, other.name)
&& age==other.age;
}
}
在这里为了防备name为null的情况我们使用Objects.equals方法,如果两个参数都为null,则该方法返回true,只有一个为null,返回false,两个都不为null,则调用a.equals(b)返回实际值。
euqals方法必须遵守以下几种特性:
- 自反性:对于任何非空引用x,x.euqals(x)应返回true。
- 对称性:对于任何引用x和y,当且仅当下,x.equals(y)返回true时,y.equals(x)也应该返回true。
- 传递性:对于任何引用x,y和z,x.equals(y)返回true,y.equals(z)也返回true,那x.equals(z)应该也返回true
- 一致性:如果x和y的引用没有发生变化,反复调用x.equals(y)应该返回相同的结果
- 对于任何非空引用x,x.equals(null)应该返回false
现在我们运行main方法可以看到输出了true。
难道到这里就结束了吗?NO... 我们经常听到重写euqals方法就必须重写hashCode方法,它们两者之间有什么联系吗?我们来修改一下main方法的代码:
public static void main(String[] args) {
Set<Person> set=new HashSet<>();
Person p1=new Person("sanmao",15);
Person p2=new Person("sanmao",15);
set.add(p1);
set.add(p2);
System.out.println(set.size());
}
上面的代码运行结果为2,这很奇怪,Set中的元素是不会重复的,我们的p1和p2两个对象一样,那我们期望的结果应该是1,为什么会是2呢。我们来看一下HashSet的源码:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
static final long serialVersionUID = -5024744406713321676L;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
//省略超多个方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//省略超多个方法
}
可以看到,HashSet内部是用一个HashMap来做存储的,实际调用的是HashMap的put方法,那我们继续跟踪一下HashMap的put方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以看到,在put方法中调用了一个静态的hash方法,在该静态方法中调用了该对象的hashCode方法。问题就在这里了,我们重写了Person类的equals方法,p1.equals(p2)返回了true,但是p1和p2的hashCode方法还是调用的继承自Object类的hashCode方法,即p1.hashCode()和p2.hashCode()返回的还是各自的存储地址,在往map中插入元素的时候,由于hashCode返回值不相等,则直接往map中添加元素(这里设计到map添加元素的过程,这里就不详细说了),结果就导致逻辑上相同的连个对象被添加了两次。
为了保证往Set中添加元素得到正确的结果,我们必须重写hashCode方法:
public class Person{
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
// 省略get,set方法
@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;
return Objects.equals(name, other.name)
&& age==other.age;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
}
现在Person类对象的hash值是由对象内容导出的,能够保证p1.equals(p2)==true时,p1.hashCode()==p2.hashCode。在和Set这样的散列表一起工作时得到正确的结果。
在重写euqals方法时,一定要重写hashCode方法,确保散列表能够正确结果。
如果两个对象equals返回true,则hashCode也返回相同值。
如果两个对象equals返回false,则hashCode不一定返回不同值。
如果两个对象hashCode返回相同值,equals不一定返回true。
如果两个对象hashCode返回不同值,equals一定返回false。
网友评论