第8条 覆盖equals时请遵守通用约定

1. 什么是equals方法

equals是Object类中一个非final的方法,是设计成要被覆盖的。

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

上述代码可以看出,Object的equals方法比较的是对象的内存地址,从这个角度来讲,类的每个对象本质上都是唯一的、因为每个对象的内存地址是不一样的。
这里也有必要提到与equals形影不离的hashCode方法,我们知道Object中的hashCode方法是将对象的内存地址转换为整数之后返回的。所以equals方法本质上是比较hashCode方法返回的值,因此一旦我们在实际使用中覆盖了equals方法,那么也必须同时覆盖hashCode方法,具体原因会在第9条中展开阐述。

public native int hashCode();

2. 什么时候应该覆盖equals方法

如果类需要有自己特有的“逻辑相等”概念(不同于对象相等),并且父类还没有覆盖equals以实现期望的行为,这时就需要覆盖equals方法。如果你的类不关心类的“逻辑相等”功能,那么你完全可以不用覆盖equals方法。上面所说的“逻辑相等”,即是值相等,对应到对象就是对象内部的关键成员值相等,值相等使得集合可以直接使用对象作为其元素或map中的key。

 public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
 }
    
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

上面这段代码是HashMap的containsKey源码,可以看出原理就是先判断hash值是否相等(hashCode方法后面我们会讲到),若相等则判断对象是否相等,若对象不相等则判断对象的是否逻辑相等即key.equals(k)。

3. 覆盖equals的通用约定有哪些

1)自反性(reflexive)	

对于任何非null的引用值x,x.equals(x)必须返回true。一般不容易违反

2)对称性(symmetric)

对于任何非null的引用值x和y,当且仅当x.equals(y)为true时,y.equals(x)也为true。违反对称性的例子:

public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) 
        	return true;
        if ( ! (o instanceof Point)) 
        	return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        int result = x;
        result = 31 * result + y;
        return result;
    }
}

public class CounterPoint extends Point {
    private int z;
    public CounterPoint(int x, int y, int z) {
        super(x, y);
        z = z;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) 
        	return true;
        if (!(o instanceof CounterPoint)) 
        	return false;
        if (!super.equals(o)) 
        	return false;
        CounterPoint that = (CounterPoint) o;
        return z == that.z;
    }

	// 尝试修复可能违反对称性的改造equals方法
    @Override
    public boolean equals(Object o) {
        if (this == o) 
        	return true;
        if (!(o instanceof Point)) 
        	return false;
        if (!(o instanceof CounterPoint))
        	return o.equals(this);
        if (!super.equals(o)) 
        	return false;
        CounterPoint that = (CounterPoint) o;
        return z == that.z;
    }
   
    @Override
    public int hashCode() {
        int result = super.hashCode();
        result = 31 * result + z;
        return result;
    }
}

public static void main(String[] args) {
	CounterPoint cPoint1 = new CounterPoint(1, 0, 1);
    Point point = new Point(1, 0);
    // 校验对称性
    System.out.println(point.equals(cPoint1));  // true
    System.out.println(cPoint1.equals(point));  // false
	// 校验传递性
	CounterPoint cPoint2 = new CounterPoint(1, 0, 2);
	System.out.println(cPoint1.equals(point)); // true
    System.out.println(point.equals(cPoint2)); // true
    System.out.println(cPoint1.equals(cPoint2)); // false
}
3)传递性(transitive)

对于任何非null的引用值x、y和z,如果x.equals(y)为true、y.equals(z)也为true时,那么x.equals(z)为true。
违反传递性的例子:如上2)中示例代码传递性校验部分。其实这是面向对象中普遍容易遇到的问题,就是无法再扩展可实例化类的同事,既增加新的成员变量,同时又保留equals约定,像Java类库中java.sql.Timestamp和java.sql.Data就是典型的例子,前者对后者进行了扩展,并增加了nanoseconds成员变量,但是仔细研究Timestamp的equals方法会发现,其实是违反equals的对称性约定的,所以不要混用这两个类。

// java.sql.Data的equals方法
public boolean equals(Object obj) {
        return obj instanceof Date && getTime() == ((Date) obj).getTime();
    }

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

public boolean equals(Timestamp ts) {
        if (super.equals(ts)) {
            if  (nanos == ts.nanos) {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
}

public static void main(String[] args) {
	long time = System.currentTimeMillis();
    Timestamp timestamp = new Timestamp(time);
    Date date = new Date(time);

    System.out.println(date.equals(timestamp)); //true
    System.out.println(timestamp.equals(date)); //false
}   

那有什么解决办法么?
Effective Java提到的一种权宜之计:复合优先于继承

public class CounterPoint {
	private Point point;
    private int z;
    public CounterPoint(int x, int y, int z) {
        point = new Point(x, y);
        z = z;
    }
	
	// 满足构建诀窍的要求
	@Override
	public boolean equals(Object o) {
		if (this == o) {
			return true;
		}
		if(!(o intanceof CounterPoint)) {
			return false;
		}
		CounterPoint cp = (CounterPointer)o;
		return cp.point.equals(point) && cp.z == o.z;
	}
}

当然,将父类定义为抽象类(无法直接创建对象),也能规避上述的问题。

4)一致性(consistent)

对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,那么多次调用x.equals(y)返回的值是一致的。如果对象是不可变的,比如String,那么它们的equals方法就必须满足这样的限制条件:相等的对象永远相等,不相等的对象永远不相等。

5)非空性(Non-nullity)

即所有的对象都必须不等于null,若intanceof操作符第一个参数为null,那么会执行返回false,因此无需单独的检测null

4. 实现高质量equals方法有哪些诀窍

1)使用==操作符检测对象是否相等,即是否指向同一个内存地址
2)使用intanceof操作符检测“参数是否为正确的类型”
3)把参数转换成正确的类型,依赖于intanceof测试
4)对于该类中的每个“关键成员变量”,检查参数时都要覆盖到,全部测试通过才返回true,否则返回false。

对于既不是float也不是double类型的基本类型成员,可以使用 ==操作符进行比较;对于float成员,可以使用Float.compare方法;对于double成员,可以使用Double.compare;对于对象成员,可以递归地调用equals方法;对于集合成员,每个元素都要基于上述原则做比较,可以参考Arrays.equals方法。其中,对于float,double类型的特殊处理是有必要的,因为存在着Float.NaN、0.0f以及类似的double常量,比如0.0d与-0.0d是不相等的。
当然,为了能提供equals比较的性能,实际情况中我们应该考虑优先比较最有可能不一致的或是开销最低的成员,最理想的情况是两个条件同时满足的成员。

5)当你编写完成equals方法治好,应该问自己三个问题:它是否对称的、传递的、一致的?

5. 告诫

1)覆盖equals方法时总要覆盖hashCode方法,见第9条

如果 覆盖equals方法时不覆盖hashCode方法会出什么问题呢?
假设上面的Point类没有覆盖hashCode方法,那么其使用的应该是hashCode方法(比较的是内存地址)。

public static void main(String[] args) {
	Map map = new HashMap();
    Point point1 = new Point(1,2);
    map.put(point1, "point1");
    Point point2 = new Point(1,2);
    // 覆盖equals方法但不覆盖hashCode方法时
    System.out.println(point1 == point2); // false
    boolean contain = map.containsKey(point2); // false
	// 覆盖equals方法且覆盖并重写hashCode方法时
 	System.out.println(point1 == point2); // false
    boolean contain = map.containsKey(point2); // true
}   

上述代码可以说明,若只覆盖equals方法而不覆盖hashCode方法时,在使用集合时可能达不到预期,equals方法返回true时我们认为这两个对象是相等的(逻辑相等,即关键成员变量的值是相等的),但是hashCode并不是根据成员变量的值计算出来的、而是内存地址,所以当我们在使用HashMap、HashSet、TreeSet等涉及到hash地址计算来存取元素时,就会碰到无法达到预期的情况,也就是说该类无法结合基于散列的集合一起正常运作

2)不要企图让equals方法过于智能
3)不要将equals声明中的Object对象替换为其他的类型

6. 总结

关于equals方法的覆盖使用需要慎重,同时要按照通用约定来。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值