equals函数,顾名思义,用来比较两个对象是否相等。
往本质想,这是物质唯一性问题。
那么在这之前,我么你需要定义一下,什么叫“相等”?如下是一个equals方法的几个通用的基本性质,可以作为定义的参考:
- 自反性, x.equals(x) == true 恒成立
- 对称性,若 x.equals(y)==true ,则 y.equals(x)==true
- 传递性,若 x.equals(y)==true && y.equals(z)==true,则 x.equals(z)==true
- 一致性,若 x.equals(y)==true,则 任意次数的调用equals结果都应为true
- 非空性,若 x!=null ,则 x.equals(null) == false
大家都同意的情况下,我们继续。
到底该不该用instanceof来判断?
这里有种比较方法: http://tieba.baidu.com/p/2235863036 也是很多人常用的比较方法,不想点链接的我在这里给大家打出来:
public class Person {
……
public boolean equals(Object obj) {
if( this==obj )
return true;
if( obj!=null ) {
if( obj instanceof Person ) {
Person p = (Person)obj;
if( this.name.equals( p.name ) && this.age == p.age ) {
return true;
}
}
}
return false;
}
……
}
有的人写equals方法,连instanceof和向上转型(转换为Person类)都没有,这显然是错误的的。假设:有一个Person类和另一个Dog类,如果他们各自的对象的的name和age都一样,在this.name和this.age判断地方能够通过,可是我们就认为是一样的对象么?!显然很可笑。狗和人怎么能一样呢?
可是有了instanceof判断就能保证equals方法的正确性了么?不一定,我们继续说上面的Person类。在继承出现之前,instanceof判断的equals是OK的。如果出现了继承呢?
假设现在有个Woman类和Man类都继承于Person类。其中一个m对象和另一个w对象的名字和年龄都一样,在上述Person的equals方法通过instanceof判断,然后我们就认为他们是相等的?显然很可笑吧!所以说equals方法的instanceof判别法限制了系统的可扩展性。那我们在子类中覆盖equals方法可以么?下面会说到这个。
那怎么办呢?用getClass()方法判断他们的类型是否一样就行了!嗯,于是男人和女人的对比又OK了。如果两个男人年龄和姓名都一样,那么他们是相等的。
上述代码中,只需把上述obj instanceof Person修改为getClass()==obj.getClass()即可。
到底该不该用getClass()来判断?
我们加入了getClass() 很多人都觉得满意了,这时候有个科学家又跳出来了,他说:这不符合OO的置换原则。这里是置换原则的链接: http://en.wikipedia.org/wiki/Liskov_substitution_principle
大概描述为:如果S是T的子类,那么任意T的对象都可以用S的对象来置换。
这么说好抽象,给大家说个例子:有个AbstractSet,而HashSet和TreeSet都继承自AbstractSet类,现在有两个对象h和t(h和t在语义上可以互相置换),他们中的元素分别都相等,可是在上述的equals方法中判断getClass()的时候,这两个的runtime class显然不一样,故而判断他们集合不相等?!看来这个getClass()也不是万能的!这时候我们又要请出instanceof判别方法了,把getClass()==obj.getClass()换成obj instanceof AbstractSet即可。
到底是什么导致有的地方要用getClass()有的地方要用instanceof呢?
所有的子类是否有统一的equals语义?
如果有统一的equals语义,例如:HashSet和TreeSet的equals语义就是,只要元素相等,那么集合就想等。则需要使用instanceof判别法,这样符合“替换原则”。
如果没有统一的equals语义,例如:Man类和Woman类的equals语义不一样,即在Man里面的equals方法和Woman的equals方法的语义很有可能不一样,那么我们的父类里就需要用getClass()来判别。当然,我们也可以在子类中重写equals方法,在equals方法里面继续用instanceof判别,可是这样又会带来另一个问题。
子类中重写equals方法?
这样导致的问题就是equals方法的设计不符合equals方法的对称性。假如现在有两个类: Person、Student和Teacher,分别有对象p、s和t,而且在Student和Teacher里面重写了方法,判别方式为instanceof。那么
- s.equals(p),只要p不是Student类型(例如这个p是Teacher类型),那么就false。满足预期想法。
- t.equals(p),只要p不是Teacher类型(例如这个p是Student类型),那么就false。满足预期想法。
- p.equals(s),只要s不是Person类型,那么就false,可事实上s是Student类型,而Student继承了Person,故而s是Person类型,故而结果为true。满足预期。
- p.equals(t),只要t不是Person类型,那么就false,可事实上t是Teacher类型,而Teacher继承了Person,故而t是Person类型,故而结果为true。满足预期。
- 矛盾来了,p.equals(t)恒为true,而t.equals(p)呢?不一定。只有当p为Teacher类型,其结果才为true。这就违背了“对称性”。
- 如果对称性成立,更为要命的是:因为p.equals(s)为true,则由对称性,s.equals(p)为true,又因为p.equals(t)恒为true,则根据传递性,s.equals(t),老师和学生的equals语义一样了!这和我们设计的初衷不一样,我们设计的初衷是语义不同,所以我们才用instanceof来设计。
所以,如果语义不同,还是建议在父类中用getClass()来作为判别方式。如果语义相同,建议用instanceof判别法。
equals方法的通用判别流程
下面给出一个较好的通用判别流程:
- 显式参数命名为otherObject,稍后需要将它转换为另一个叫做other的变量
- 检测this与otherObject是否引用同一个对象。if (this == otherObject ) return true;
- 检测otherObject是否为null,如果为null,则返回false。if ( otherObject == null ) return false;
- 比较this与otherObject是否属于同一个类。(吐槽一下,OSchina的编辑器连缩进都没有)
- a) 如果父类的子类的equals方法有相同的语义,则使用instanceof判别法。 if( otherObject instanceof ParentClassName ) return false;
- b) 如果父类的子类的equals方法有不同的语义,则使用getClass()判别法。 if( getClass()!=otherObject.getClass() ) return false;
- 把otherObject转换为乡音的类类型变量。ParentClass other = (ParentClass) otherObject;
- 使用==比较primitive域,使用equals比较对象域。 return field1==other.field1 && field2 == other.field2 && .... ;
- 如果在子类中重新定义了equals,就要在其中先调用super.equals(other)来比较是否相等。
下面是一个例子:
import java.util.*;
/**
* This program demonstrates the equals method.
* @version 1.11 2004-02-21
* @author Cay Horstmann
*/
public class EqualsTest
{
public static void main(String[] args)
{
Employee alice1 = new Employee("Alice Adams", 75000, 1987, 12, 15);
Employee alice2 = alice1;
Employee alice3 = new Employee("Alice Adams", 75000, 1987, 12, 15);
Employee bob = new Employee("Bob Brandson", 50000, 1989, 10, 1);
System.out.println("alice1 == alice2: " + (alice1 == alice2));
System.out.println("alice1 == alice3: " + (alice1 == alice3));
System.out.println("alice1.equals(alice3): " + alice1.equals(alice3));
System.out.println("alice1.equals(bob): " + alice1.equals(bob));
System.out.println("bob.toString(): " + bob);
Manager carl = new Manager("Carl Cracker", 80000, 1987, 12, 15);
Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
boss.setBonus(5000);
System.out.println("boss.toString(): " + boss);
System.out.println("carl.equals(boss): " + carl.equals(boss));
System.out.println("alice1.hashCode(): " + alice1.hashCode());
System.out.println("alice3.hashCode(): " + alice3.hashCode());
System.out.println("bob.hashCode(): " + bob.hashCode());
System.out.println("carl.hashCode(): " + carl.hashCode());
}
}
class Employee
{
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
hireDay = calendar.getTime();
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public Date getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
public boolean equals(Object otherObject)
{
// a quick test to see if the objects are identical
if (this == otherObject) return true;
// must return false if the explicit parameter is null
if (otherObject == null) return false;
// if the classes don't match, they can't be equal
if (getClass() != otherObject.getClass()) return false;
// now we know otherObject is a non-null Employee
Employee other = (Employee) otherObject;
// test whether the fields have identical values
return name.equals(other.name) && salary == other.salary && hireDay.equals(other.hireDay);
}
public int hashCode()
{
return 7 * name.hashCode() + 11 * new Double(salary).hashCode() + 13 * hireDay.hashCode();
}
public String toString()
{
return getClass().getName() + "[name=" + name + ",salary=" + salary + ",hireDay=" + hireDay
+ "]";
}
private String name;
private double salary;
private Date hireDay;
}
class Manager extends Employee
{
public Manager(String n, double s, int year, int month, int day)
{
super(n, s, year, month, day);
bonus = 0;
}
public double getSalary()
{
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
public void setBonus(double b)
{
bonus = b;
}
public boolean equals(Object otherObject)
{
if (!super.equals(otherObject)) return false;
Manager other = (Manager) otherObject;
// super.equals checked that this and other belong to the same class
return bonus == other.bonus;
}
public int hashCode()
{
return super.hashCode() + 17 * new Double(bonus).hashCode();
}
public String toString()
{
return super.toString() + "[bonus=" + bonus + "]";
}
private double bonus;
}
参考文献:
- 《Core Java Vol. 1. 8th 》
- Wiki置换原则: http://en.wikipedia.org/wiki/Liskov_substitution_principle