zoukankan      html  css  js  c++  java
  • 《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 第二版》

  • 相关阅读:
    MapReduce-文本输入
    MapReduce-输入分片与记录
    python 常用类库
    python leveldb 文档
    火狐插件推荐
    mweb test
    python代码风格规范
    UNICODE,GBK,UTF-8区别
    机器学习之K近邻算法(KNN)
    python中的StringIO模块——html
  • 原文地址:https://www.cnblogs.com/lizijuna/p/11907415.html
Copyright © 2011-2022 走看看