《Effective Java》之覆盖equals请遵守通用约定

1.java.lang.Object实现的equals()方法如下:Object类实现的equals()方法仅仅在两个变量指向同一个对象的引用时才返回true。

//JDK1.8中的Object类的equals()方法
public boolean equals(Object obj) {
        return (this == obj);
    }

2.既然Java已经为我们提供了equals()方法,我们还需要覆盖equals()方法吗?如果你的类满足下面任何一个条件,你都不需要再覆盖Object的equals()方法了

  • 关注对象实体而不是值的类。此时继承Object类的equals()方法就好了。如Enum类的每一个对象都应该是一个常量,每一个值至多存在一个对象。对于这样的类而言,逻辑相同与对象相同是同一个概念。此时Object的equals()方法就足以应付了。
//JDK1.8中的Enum类的equals()方法,相对于上面的Object类多了一个final修饰,使子类
//不能重写equals()方法,不得更改枚举类型相等的概念
public final boolean equals(Object other) {
        return this==other;
    }
  • 不关心类是否提供“逻辑相等”的方法。即equals()对于这个类是没有必要的,如Random类判断两个Random实例是否产生相同随机数序列是没有任何意义的,此时你就可以任由它继承Object类的equals()就好了
  • 超类已经覆盖了equals()方法,并且从超类继承过来的方法对子类也是适用的
    //JDK1.8中的AbstractList<E>类的equals()方法,List<E>的实现类继承
    //该类获取equals()方法,判断集合里的元素是否全部相等
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof List))
            return false;

        ListIterator<E> e1 = listIterator();
        ListIterator<?> e2 = ((List<?>) o).listIterator();
        while (e1.hasNext() && e2.hasNext()) {
            E o1 = e1.next();
            Object o2 = e2.next();
            if (!(o1==null ? o2==null : o1.equals(o2)))
                return false;
        }
        return !(e1.hasNext() || e2.hasNext());
    }
  • 类是私有的或是包级私有的,可以确定它的equals()方法永远不会被调用。
@Override
public boolean equals(Object arg0) {
	throw new AssertionError();//确保这个方法不会被调用
}

3.什么时候需要覆盖Object类的equals()方法呢?

如果类具有自己特定的“逻辑相等”概念(不同于对象等同的概念),而且超类还没有覆盖equals()方法以实现期望的行为,这时就需要我们覆盖equals()方法。

//JDK1.8中的Boolean类的equals方法,实现根据值而不是对象引用判定两个Boolean
//对象相等的equals()方法
public boolean equals(Object obj) {
        if (obj instanceof Boolean) {
            return value == ((Boolean)obj).booleanValue();
        }
        return false;
    }

4.覆盖equals方法的时候,我们需要遵守equals()方法的一些通用约定(这些约定在JDK中Object类的equals()方法上方作了说明):

  • 自反性:对于任何非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。

5.也许这些约定的定义都比较直白,但是我们有时候却会不知不觉破坏这些约定,接下来举一些破坏上面约定的例子。

  • 自反性
    违反这个约定比较难,可以假定这个约定最容易被实现,就不要反例了。
  • 对称性

CaseInsensitiveString是一个不区分大小写的字符串类,那么这个类的equal()方法在比较的时候就应该忽略字母的大小写。

public class CaseInsensitiveString{

	private final String s;//CaseInsensitiveString类存储字符串的变量
	public CaseInsensitiveString(String s){
	if(s==null)
		return new NullPointerException();
	this.s=s;
	}
	@Override
	public boolean equals(Object o){
		if(o instanceof CaseInsensitiveString)
		return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
		if(o instanceof String)//企图与普通的字符串(String)进行互操作
		return s.equalsIgnoreCase((String) o);
	}
}

此时我们就可以写测试代码了。如下:

CaseInsensitiveString cis=new CaseInsensitiveString("Polish");
String s="Polish";
cis.equals(s); //true
s.equals(cis); //false,违反了对称性

虽然CaseInsensitiveString类中的equals()方法指导普通的String对象,但是String类的equals却不知道不区分大小写的字符串。

//JDK1.8中的String类的equal()方法
public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }//判断两个字符串的逐一比较代价较高,可以先利用引用确定是否相等
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

为了不破坏对称性,只要把企图与普通的字符串(String)进行互操作的代码删掉就好了。

public boolean equals(Object o){
	return	o instanceof CaseInsensitiveString &&
		((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
	}
  • 传递性
    如果父类已经实现了一个equals()方法,子类在添加了新的成员变量之后,显然父类的equals()方法并没有子类的成员变量的比较,而子类的这个成员变量在判断相等关系的时候显然不能忽视,那么我们应该如何实现子类的equals()方法吶?下面先来看一个我们容易想到的一个子类的equals()实现。
//ColorPoint类的equals()方法,ColorPoint类继承自Point类
@Override 
public boolean equals(Object o){
	
	 if(!(o instanceof Point))//Point(x,y),两个int变量
		 return false;//如果不是Point和ColorPoint类的对象,返回false
	 if(!(o instanceof ColorPoint))//ColorPoint(x,y,color),两个int变量和一个Color对象
		 return o.equals(this); //调用Point类的equals()方法比较两个参数
	 return super.equals(o)&&((ColorPoint)o).color==color;
	 //调用父类方法比较前两个参数,再自行实现比较第三个参数
}
ColorPoint p1=new ColorPoint(1,2,Color.RED);
Point p2=new Point(1,2);
ColorPoint p3=new ColorPoint(1,2,Color.BLUE);
p1.equals(p2);//true,调用父类仅比较两个参数
p2.equals(p3);//true,调用父类仅比较两个参数
p1.equals(p3);//false,子类比较三个参数,失去了传递性

面对这样的继承,看看Java里面是如何解决的?先来测试一下Java遵守传递性了吗?

//timestamp继承自Date类,Timestamp比Date类多了一个纳秒成员变量
Timestamp timestamp1=new Timestamp(1000L);
Date date=new Date(1000L);
Timestamp timestamp3=new Timestamp(1000L);
timestamp1.equals(date);//false
date.equals(timestamp3);//true
timestamp1.equals(timestamp3);//true

看来Java的实现里面也是没有遵守对称性的。在前面的例子中,我们期望子类调用的equals()方法能够在方法里面传入父类参数来实现父类和子类的比较,这个有些不现实。Timestamp类的equals()放弃在子类中对父类进行判断,传入父类对象直接返回False。Timestamp有一个免责声明:告诫程序员不要混合使用Timestamp和Date对象。

//JDK1.8中的Timestamp类的equals()方法
public boolean equals(java.lang.Object ts) {
      if (ts instanceof Timestamp) {//传入Date类(父类)直接返回False
        return this.equals((Timestamp)ts);
      } else {
        return false;
      }
    }

按照Tava的实现,我们可以改写如下:

@Override 
public boolean equals(Object o){	
	 if(!(o instanceof ColorPoint))//不对父类对象进行比较
		 return false;
	 ColorPoint cp=(ColorPoint) o;
	 return super.equals(o)&&((ColorPoint)o).color==color;
	 //调用父类方法比较前两个参数,再自行实现比较第三个参数

java中告诫不要子类和父类一起混用多了成员变量的equals()方法,如果父类是一个抽象类,就不存在父类的对象,前面所述的种种问题就不会发生了。

  • 一致性
    对于不可变的类,相等的对象一直相等,不相等的对象一直相等。无论类是否是可变的,都不要使equals()方法依赖于不可靠的资源。例如,java.net.URL的equals()方法依赖于对URL中主机IP地址的比较。将一个主机名转化为IP地址需要访问网络,随着时间的推移,不确保会产生相同的结果(DHCP协议:DHCP服务器分配给DHCP客户的IP地址是临时的,因此DHCP客户只能在一段有限的时间内使用这个分配到的IP地址。(计算机网络第六版))。IP地址就是不可靠的资源。
  • 非空性
    所有的对象都必须不等于null。下面我们先来看一下排除null的判断:
@Override
public boolean equals(Object o){
	if(o==null)
	return false;
	...
}

实际上面代码中测试非null的语句多余了,equals()方法中总是需要对参数进行类型判断(instanceof)以确定传入参数是属于某个类的对象进而调用它的访问方法,或者访问它的域。类型判断其实已经包含了对null的检查。

@Override
public boolean equals(Object o){
	if(!(o instanceof MyType))//包含了对null的检验
	return false;//传入null的话,会返回false
	MyType mt=(MyType) o;
	...
}

6.在前面的这些要求之下,得出了以下实现高质量equals()方法的诀窍

  • 使用==操作符检查“参数是否为这个对象的引用”。如果是,则返回true。这只不过是一种性能优化,如果比较操作有可能很昂贵,就值得这么做。
	//JDK1.8中的String类的equals()方法
    public boolean equals(Object anObject) {
        if (this == anObject) {//性能优化
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
  • 使用instanceof操作符检查"参数是否为正确的类型";一般来说,所谓"正确的类型"就是equals()方法所在的那个类,有些情况下,是指该类所实现的某个接口。集合接口如Set,List,Map允许实现了改接口的类进行比较。
  • 把参数转化为正确的类型。因为转换之前进行过instabceof测试,所以确保会成功。
  • 对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配。

对于基本数据类型,使用==。
对于对象引用类型,调用equals()方法。
对于float,double,可调用Float.compare(),Double.compare()方法,防止Float.NAN,-0.0f等特殊的常量。
为了获取最佳的性能,优先比较最有可能不一样的域。冗余域(可由关键域得到的域)不需要比较。

  • 覆盖equals()方法时总要覆盖hashCode()。
  • 不要企图让equals()方法过于智能。
  • 不要将equals()方法声明中的Object对象替换为其它的类型。

参考资料:
《Effective Java 第二版》

posted @ 2018-04-03 14:38  李子君啊  阅读(136)  评论(0编辑  收藏  举报