到底该怎么写好equals函数?

equals函数,顾名思义,用来比较两个对象是否相等。

往本质想,这是物质唯一性问题。

那么在这之前,我么你需要定义一下,什么叫“相等”?如下是一个equals方法的几个通用的基本性质,可以作为定义的参考:

  1. 自反性, x.equals(x) == true 恒成立
  2. 对称性,若 x.equals(y)==true ,则 y.equals(x)==true
  3. 传递性,若 x.equals(y)==true && y.equals(z)==true,则 x.equals(z)==true
  4. 一致性,若 x.equals(y)==true,则 任意次数的调用equals结果都应为true
  5. 非空性,若 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方法的通用判别流程

下面给出一个较好的通用判别流程:

  1. 显式参数命名为otherObject,稍后需要将它转换为另一个叫做other的变量
  2. 检测this与otherObject是否引用同一个对象。if (this == otherObject ) return true;
  3. 检测otherObject是否为null,如果为null,则返回false。if ( otherObject == null ) return false;
  4. 比较this与otherObject是否属于同一个类。(吐槽一下,OSchina的编辑器连缩进都没有)
  5. a) 如果父类的子类的equals方法有相同的语义,则使用instanceof判别法。 if( otherObject instanceof ParentClassName ) return false; 
  6. b) 如果父类的子类的equals方法有不同的语义,则使用getClass()判别法。 if( getClass()!=otherObject.getClass() ) return false; 
  7. 把otherObject转换为乡音的类类型变量。ParentClass other = (ParentClass) otherObject;
  8. 使用==比较primitive域,使用equals比较对象域。 return field1==other.field1 && field2 == other.field2 && .... ;
  9. 如果在子类中重新定义了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;
}





参考文献:


  1.  《Core Java Vol. 1. 8th 》
  2.  Wiki置换原则: http://en.wikipedia.org/wiki/Liskov_substitution_principle


 

转载于:https://my.oschina.net/xue777hua/blog/179109

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值