看到一个面试题: 重写equals()是否需要重写hashcode(),不重写会有什么后果。 来分析下Object类中的这两个方法。
public boolean equals(Object obj)
public int hashCode()
equals方法
equals()方法是用来判断其他的对象是否和该对象相等.
object类:
public boolean equals(Object obj) {
return (this == obj);
}
很明显是对两个对象的地址值进行的比较。但是在String 、Math、Integer、Double等这些封装类覆盖了object类的equals()方法。
String类:
public boolean equals(Object anObject) {
//如果引用相等,返回true
if (this == anObject) {
return true;
}
//如果不相等进行判断
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
hashcode
hashCode()方法给对象返回一个int值(hash code)。这个方法被用于hash tables,例如HashMap。
API注释:
-
在Java应用程序的执行过程中,无论何时在同一个对象上多次调用它,hashCode方法都必须返回相同的整数,前提是提供给equals做比较的信息没有被修改。此整数不需要在应用程序的一次执行与同一应用程序的另一次执行之间保持一致。
-
如果两个对象根据equals(Object)方法是相等的,那么调用二者各自的hashCode()方法必须产生同一个integer结果。
-
并不要求根据equals(java.lang.Object)方法不相等的两个对象,调用二者各自的hashCode()方法必须产生不同的integer结果。然而,程序员应该意识到对于不同的对象产生不同的integer结果,有可能会提高hash table的性能。
所以说
1、相等的对象必须具有相等的hashCode。
2、如果两个对象的hashCode相同,它们并不一定相同。
object类中,hashCode定义如下:
public native int hashCode();
说明是一个本地方法,它的实现是根据本地机器相关的。当然我们可以在自己写的类中覆盖hashcode()方法,比如String、Integer、Double等这些类都是覆盖了hashcode()方法的。
例如在String类中定义的hashcode()方法如下:
public int hashCode() {
int h = hash;// Default to 0
if (h == 0 && value.length > 0) {
hash = h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
}
return h;
}
API注释中写道计算方法为:s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1] (这里 s[i] 是字符串的第 i 个字符,n 是字符串的长度,^ 表示求幂)
最后
回到问题上来,重写equals()是否需要重写hashcode(),不重写会有什么后果。
要明白这个问题,首先要明白hashcode、equals的作用和联系。
总的来说,Java中的集合(Collection)有两类,一类是List,再有一类是Set。前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。这里就引出一个问题:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?
这就是Object.equals方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。
于是,Java采用了哈希表的原理。哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上,初学者可以简单理解,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。
这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。(hashCode相同,对象不一定相同)
如果只重写了equals方法而没有重写hashCode方法的话,则会违反约定的第二条:相等的对象必须具有相等的hashCode。那么在对于HashSet和HashMap这些基于散列值(hash)实现的类,可能会存入相同元素
重写equals目的是为了比较两个对象的value值是否相等。
为了当obj1.equals(obj2)为true时,obj1.hashCode() == obj2.hashCode()必须为true。所以重写equals()需要重写hashcode(),不重写可能会导致相等的对象没有相同的hashCode,在对于HashSet和HashMap这些基于散列值(hash)实现的类,可能会存入相同元素。
反过来,重写hashcode()需不需要重写equals(),当obj1.hashCode() == obj2.hashCode()为false时,obj1.equals(obj2)必须为false。如果不重写equals,那么比较的将是对象的引用是否指向同一块内存地址,重写之后目的是为了比较两个对象的value值是否相等。
所以这两个必须一起重写。
总结,自定义类要重写equals方法来进行等值比较,要重写compareTo方法来进行不同对象大小的比较,要重写hashcode方法为了将数据存入HashSet/HashMap/Hashtable类时进行比较
重写hashcode与equals
我们先模拟一下不重写会出现的问题
import java.util.HashSet;
public class User
{
String name;
int age;
User(String name, int age)
{
this.name = name;
this.age = age;
}
public static void main(String[] args) {
User user1 = new User("张三",19);
User user2 = new User("张三",19);
System.out.println(user1.equals(user2));
System.out.println("user1.hashCode():" + user1.hashCode());
System.out.println("user2.hashCode():" + user2.hashCode());
HashSet<User> hashSet = new HashSet<>();
hashSet.add(user1);
hashSet.add(user2);
System.out.println(hashSet);
}
}
输出
false
user1.hashCode():968514068
user2.hashCode():1360767589
[User@39ba5a14, User@511baa65]
可见user1和user2明明值是一样的,但是继承的Object的equals只能判断地址是否相同,所以我们要重写equals。
只重写equals
import java.util.HashSet;
public class User
{
String name;
int age;
User(String name, int age)
{
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
User user = (User)obj;
if (this.name.equals(user.name) && this.age == user.age)
return true;
return false;
}
public static void main(String[] args) {
User user1 = new User("张三",19);
User user2 = new User("张三",19);
System.out.println(user1.equals(user2));
System.out.println("user1.hashCode():" + user1.hashCode());
System.out.println("user2.hashCode():" + user2.hashCode());
HashSet<User> hashSet = new HashSet<>();
hashSet.add(user1);
hashSet.add(user2);
System.out.println(hashSet);
}
}
输出
true
user1.hashCode():968514068
user2.hashCode():1360767589
[User@39ba5a14, User@511baa65]
可见虽然两个实例相等了,但是hashcode不同,HashSet有两个值。
只重写hashcode
import java.util.HashSet;
import java.util.Objects;
public class User
{
String name;
int age;
User(String name, int age)
{
this.name = name;
this.age = age;
}
@Override
public int hashCode() {
return Objects.hash(name,age);
}
public static void main(String[] args) {
User user1 = new User("张三",19);
User user2 = new User("张三",19);
System.out.println(user1.equals(user2));
System.out.println("user1.hashCode():" + user1.hashCode());
System.out.println("user2.hashCode():" + user2.hashCode());
HashSet<User> hashSet = new HashSet<>();
hashSet.add(user1);
hashSet.add(user2);
System.out.println(hashSet);
}
}
输出
false
user1.hashCode():24022539
user2.hashCode():24022539
[User@16e8e0b, User@16e8e0b]
可见虽然二者hashcode相同,但是依然都被放在HashSet中,因为equals为false。
都重写
import java.util.HashSet;
import java.util.Objects;
public class User
{
String name;
int age;
User(String name, int age)
{
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
User user = (User)obj;
if (this.name.equals(user.name) && this.age == user.age)
return true;
return false;
}
@Override
public int hashCode() {
return Objects.hash(name,age);
}
public static void main(String[] args) {
User user1 = new User("张三",19);
User user2 = new User("张三",19);
System.out.println(user1.equals(user2));
System.out.println("user1.hashCode():" + user1.hashCode());
System.out.println("user2.hashCode():" + user2.hashCode());
HashSet<User> hashSet = new HashSet<>();
hashSet.add(user1);
hashSet.add(user2);
System.out.println(hashSet);
}
}
输出
true
user1.hashCode():24022539
user2.hashCode():24022539
[User@16e8e0b]