听说你还不会实现equals方法?收藏这篇文章就够了!

本文详细探讨了Java中何时需要重写equals方法,以及重写时需要遵守的约定,包括自反性、对称性、传递性、一致性和非空性。通过示例说明了错误重写可能导致的问题,并提供了利用IDE自动生成和手动编写equals方法的建议。最后强调了重写equals方法时,务必同步实现hashCode方法的重要性。
摘要由CSDN通过智能技术生成

1、何时需要重写equals

相信javaer们应该都知道equals方法,它是基类大佬Object中的一个方法,所以java下面所有的类都“自带”这个方法。看方法名就知道,意图就是对比传入的目标对象, 跟自己是否“相等”。我们先看看这个方法在Object类中的实现:

    public boolean equals(Object obj) {
        return (this == obj);
    }

这个实现也算简单粗暴了,直接用“==”来跟目标对象作比较,这个意图就是:除非对方就是是自己本身,否则就不相等。 但就是因为这样的粗暴,造成限定得太死了。实际开发中,可能原生的equals不能满足业务的需求。所以需要重写,例如Integer中重写的equals方法,可以看到Integer并不强制目标对象是“自己本体”,而只是对比了被自己包装的int基本类型数值,这样的实现更加符合实际业务要求:

    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();//对比被包装的整型数值
        }
        return false;
    }

关于何时才需要重写equals方法,《Effective Java》中提到:

如果类具有自己特有的“逻辑相等”概念,而且超类还没有覆盖equals。这通常属于“值类”的情形。值类仅仅是一个表示值得类,例如Integer和String。程序员在利用equals方法来比较值对象的引用时,希望知道他们在逻辑上是否相等,而不是想了解它们是否指向同一个对象。

这句话翻译得有点拗口,不过意思还是明确的,何时才是需要重写equals,可以总结为以下两点:

  • **该类只代表一个具体的“值“。**例如Double、Integer、Long等这种包装类型,其实这些类再怎么复杂,它们也只是代表一个数值,只要类型和数值相等,它们对象也理应是相等的。
  • **业务逻辑相同的对象。**这个可以看作是第一点的延申,即对象间的成员变量、行为表现是一致的时候,也应该理解成是相同的对象。例如一个商品类Goods,只要它的商品ID,商品名称等关键信息是相等的,就是同一件商品。

2、重写equals方法要遵守的约定

2.1 重写equals错误示范

重写equals方法看似很简单,但是有很多重写的方式会导致意想不到的错误。举一个jdk里的一个错误例子,java.sql.Timestamp是继承自java.util.Date的,这两个类都分别重写了equals方法。我们可以来看看:

java.util.Date.equals:

  public boolean equals(Object obj) {
        return obj instanceof Date && getTime() == ((Date) obj).getTime();
  }

java.sql.Timestamp:

    public boolean equals(java.lang.Object ts) {
      if (ts instanceof Timestamp) {
        return this.equals((Timestamp)ts);
      } else {
        return false;
      }
    }

这两个类的equals方法,如果是各自单独使用起来的话,是没有任何问题的,但如果Date和Timestamp这两个类一起使用的话就有问题了,例如把这两个类的对象都存到一个集合list里,就会出问题了:

	static List<java.util.Date> dateList = new ArrayList<>();
	public static void addTimeObj(java.util.Date date) {
		if(!dateList.contains(date)) {
			dateList.add(date);
		}
	}
	
	public static void main(String[] args) {
		long currentTimeMillis = 1586669016742L;
		java.sql.Timestamp timestamp = new Timestamp(currentTimeMillis);
		Date date = new java.util.Date(currentTimeMillis);
		
		System.out.println(date.equals(timestamp));//true
		System.out.println(timestamp.equals(date));//false
		
		addTimeObj(timestamp);
		addTimeObj(date);//date对象加不进去
		
	}

由上面的代码可以看出,date.equals(timestamp)是true,timestamp.equals(date)是false,这个违反了下面将要说的对称性 ,因此有可能会产生意想不到的后果,例如第二个addTimeObj方法的调用,date是加不进去的,因为ArrayList.contains利用了对象的equals方法来进行对比的,然而timestamp.equals(date)==false。

     public boolean contains(Object o){
        //...省略一些代码
        for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))//timestamp.equals(date)==false
                    return i;           
       return false;
        //...省略一些代码
     }

2.2 重写equals要遵守的”军规“

所以,重写equals时,是有一定的约束的,《Effective Java》中提到了一些重写必须遵循的”军规“:

  • 自反性:对于任何非null的引用值x,x.equals(x)必须返回true;
  • 对称性:对于任何非null的引用x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true;
  • 传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,则x.equals(z)返回true;
  • 一致性:对于任何非null的引用值x和y,只要equals的比较操作对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false;
  • 对于任何非null的引用x,x.equals(null)必须返回false。

以上几条规定,如果违反了其中一条,就有可能会产生意想不到的后果,程序也许会表现得不正常,甚至崩溃。就像上面java.util.Date和java.sql.Timestamp的例子,这样的bug是很难排查出来的。

3、如何正确快速的重写equals方法

我们得知重写equals方法的一些约束后,会觉得,实现一个equals方法怎么那么难?其实,要实现equals方法,并不难,在实际开发中,有以下两个快速稳当的方法。

3.1 利用IDE的自动生成equals

目前的IDE都有equals方法的快捷生成方法,比如eclipse,”Alt+Shift+S“组合键—>Generate hashCode() and equals(),即可同时生成hashCode方法和equals方法*(由此也可以看出equals方法和hashCode方法是捆绑一起的,实现equals方法,必须也得实现hashCode方法)*

public class Graph {

	int n;
	
	LinkedList<Integer> [] table;

   //....此处省略了hashCode方法的实现

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Graph other = (Graph) obj;
		if (n != other.n)
			return false;
		if (!Arrays.equals(table, other.table))
			return false;
		return true;
	}
}

上面的equals方法是eclipse生成的(篇幅原因省略了hashCode的代码),粗略的看起来还挺全的,其实不一定符合我们实际开发中的业务需求,而且略显臃肿。所以在平时开发中用IDE生成的这种代码,往往都是需要调整一下,以适应实际业务,当然,前提是要遵循上面说到的几个军规

3.2 重写equals方法的几个诀窍(步骤)

IDE生成equals方法非常便捷,但是生成的代码有时是比较”鸡肋“。因此,我们是否可以在遵循军规的基础上,自己快速手写一个equals方法呢?当然是可以的,可以按照以下几个步骤来一步步实现。

  1. 使用**==**操作符检查参数是否为这个对象的引用。如果是,则返回true。
  2. 使用instanceof操作符检查参数是否是正确的类型。如果不是,返回false。
  3. 把参数转成正确的类型。因为已经经过第二步的ininstanceof 校验,所以类型会转换成功。
  4. 对于该类中每个”关键域“,检查参数中的域是否与该对象中的对应的域相匹配。在检查域的时候,应该先检查性能开销最低的域,或者逻辑最可能不一致的域,按照这样的有序安排可以尽可能减少比较的次数,从而提高性能。
  5. 编写玩equals之后,应该考虑并检验三个问题:它是否是对称的、传递的、一致的?

根据这五个步骤,写出来的equals才是安全可靠的,下面构造了一个Goods类,并实现了它的equals方法和hashCode方法,最后再来检测下它的对称性、传递性、一致性,都是可以的!

public class Goods {

	int id;	
	String goodsName;
	
	@Override
	public boolean equals(Object obj) {
		//使用==操作符检查参数是否为这个对象的引用。如果是,则返回true。
		if(this == obj) 
			return true;
		
		//使用instanceof操作符检查参数是否是正确的类型。如果不是,返回false。
		if(!(obj instanceof Goods)) 
			return false;
		
		//把参数转成正确的类型。
		Goods target = (Goods)obj;
		
		/*
		 * 对于该类中每个”关键域“,检查参数中的域是否与该对象中的对应的域相匹配。
		 * 在检查域的时候,应该先检查性能开销最低的域,或者逻辑最可能不一致的域,
		 * 按照这样的有序安排可以尽可能减少比较的次数,从而提高性能。
		 */
		if(this.id != target.id)
			return false;
		if(target.goodsName == null || !target.goodsName.equals(this.goodsName))
			return false;
		
		return true;
	}
    
    @Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((goodsName == null) ? 0 : goodsName.hashCode());
		result = prime * result + id;
		return result;
	}
    
    
}

4、写在最后

覆盖equals方法是要比较严谨的,如果不是迫不得已,就不要轻易去重写equals方法。如果要重写,则一定要比较这个类的所有关键域,并且查看它们是否遵循equals的几条军规!而且还有另外一个很重要的是,重写equals方法后,必须同步实现hashCode方法,否则就会违反了hashCode的通用约定,而关于hashCode 如何重写,请看姐妹篇《如何实现高效的hashCode方法》

equals方法,你今天掌握了吗?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值