object_equality

第四章 对象相等性(Object Equality)

1 简介

在Java语言中,开发人员常常需要用到对象相等性这个概念。Java语言中,==操作符和equals()函数均可判断两个对象是否相等。但是操作符==和equals()函数的内在逻辑却不同。

==操作符和equals()函数的区别在于:其一,==是操作符,而equals()是Object类的成员方法。换句话说,所有的类都会有一个equals()成员方法。它或者继承自Object类,或者由开发人员定义。因为Java语言不支持操作符重载(Operator Overloading),所以,开发人员无法重新定义一个==操作符来比较两个对象。其二,==操作符可用来比较任何对象(包括基本数据类型(Primitive Data Type)和对象);而equals()函数只能比较对象。

2 操作符==

操作符==是一个二元操作符,它接受两个操作数,并返回比较这两个操作数的结果。==操作符有三种应用场景。

  1. 数值比较(Numeric Equality Comparison)。如果两个操作数都是数值类型的变量,或者一个是数值类型的变量,另一个是可以转换为数值类型(Convertible to Numeric Type)的变量,则适用于此场景。如果两个操作数的类型不同,则会发生类型提升转换(Numeric Promotion)。(例如,如果比较一个float变量和一个double变量,则float变量会提升为double类型之后,在进行两个double变量的等值测试。)在必要的转换完成后,==操作符按照操作数的数值进行等值测试。如果比较一个int变量和一个Integer类的对象,Java编译器会对Integer类的对象进行拆箱操作,将其转换为int类型,然后进行两个int变量的等值测试。
  2. 布尔比较(Boolean Equality Comparison)。如果两个操作数都是boolean类型的变量,或者一个是boolean类型的变量,另一个是Boolean类型的对象,则适用于此场景。如果有一个操作数是Boolean对象,则需要进行拆箱操作(Unboxing),将其转换为boolean类型。最后,==操作符按照其布尔取值进行等值测试。
  3. 引用比较(Reference Equality Comparison)。当两个操作数都是对象引用或者null的时候,适用此场景。==操作符比较两个操作数是否指向同一对象,或者同时都是null。如果上述条件满足,==操作符返回true;否则,返回false。

3 equals()成员方法

为了能够提供基于业务逻辑(Business Logic)的等值比较,Java语言在Object类对象中添加了一个成员方法equals()。因为Java支持函数覆盖(Overriding)特性,所有的类(所有的类都是Object类的子类)都可以覆盖默认的equals()成员方法,从而开发人员能够根据业务逻辑自定义对象相等的逻辑。因此,在任何需要使用对象相等测试的代码中,均应使用equals()函数进行相等测试。例如,在容器的查询中,均使用的是equals()函数进行对象相等测试。

类似的,String作为一个特殊的类,开发人员也应使用equals()函数比较两个String对象是否包含了相同的字符。

4 hashCode()成员方法

在使用equals()函数比较两个对象时,按照业务逻辑比较这两个对象的内容可能是一个较为耗时的过程。这可能是因为比较过程逻辑复杂,也可能是这两个对象较大,需要比较的内容较多。为了加快这个比较的过程,Java在Object类中增添了hashCode()成员方法。

大致上讲,hashCode()成员方法根据对象的内容计算出一个散列值。在比较两个对象是否相等之前,可以先比较两个对象的散列值。如果两个对象的散列值不相等,那么,这两个对象一定是不相等的。如果两个对象的散列值相等,则需要进一步比较两个对象的内容。因为散列值计算速度快,因此,hashCode()成员方法常常被用于equals()函数中的第一个测试步骤,以提升对象比较效率。

另一个使用hashCode()成员方法的场景是HashMap或者有着相似功能的类。在HashMap中,hashCode()函数被用来计算对象的散列值,进而确定对象所在的Bucket和其index值。

5 覆盖equals()和hashCode()成员方法

当需要实现自定义的对象比较时,开发人员需要实现自定义的equals()函数,覆盖默认的Object.equals()函数。为了提高自定义equals()函数的效率,开发人员还可以实现自定义的hashCode()函数。在下面的例子中,类Example覆盖了equals()和hashCode()成员方法。使用@Override 标注(Annotation)是一个好的习惯,Java编译器能帮助检查函数原型是否与父类的成员函数保持一致。

import java.util.Objects;
public class Example {
    @Override
    public boolean equals(Example obj) {
        return Objects.equals(this, obj);
    }
    @Override
    pulibc int hashCode() {
        return Objects.hash(0);
    }
}

在实现自定义的equals()成员方法时,应注意以下几点,这也是equals()成员方法需要遵守的契约(Contracts)。

  1. 自反性(Reflexivity),即任意一个对象永远等于自己。给定一个对象x,x不为null时,x.equals(x)永远返回true。
  2. 对称性(Symmetricity),即当给定两个对象x和y,x和y不为null时,如果x.equals(y)为true,则y.equals(x)也为true。
  3. 传递性(Transitivity),即当给定三个对象x, y和z,x, y和z不为null时,如果x.equals(y)为true,y.equals(z)为true,那么,x.equals(z)也为true。
  4. 一致性(Consistency),即给定两个对象x和y,x和y不为null时,如果在某一次调用中x.equals(y)返回true,那么,当x和y的值未发生变化时,在任何场景下调用x.equals(y)都返回true。
  5. 当对象x不为null时,x.equals(null)返回false。

在实现自定义hashCode()成员方法时,hashCode()成员方法需要遵守如下契约(Contracts)。

  1. 一致性(Consistency)。在一个应用程序的运行过程中,对于同一对象,当对象的值未发生变化时,hashCode()函数必须始终返回相同的整型值。但是,当这个应用程序运行多次时,对于包含相同值的对象,hashCode()函数不必每次返回相同的值。返回的值是否相同依赖于具体实现。实际上,简单的说,如果运行某一应用程序两次,对于包含相同值的对象,hashCode()函数可以返回不同的值。但是,如果该应用程序只运行一次,在此次运行过程中,hashCode()函数如果被调用多次的话,hashCode()函数需要始终返回相同的值。
  2. 给定两个对象x和y,如果x.equals(y)返回true,那么,x.hashCode()和y.hashCode()必须返回相同的值。
  3. 给定两个对象x和y,如果x.equals(y)返回false,那么,x.hashCode()和y.hashCode()不必返回不同的值。但是,如果hashCode()函数能返回不同的值的话,能提高x.equals(y)函数调用运行的效率。

从上述的契约来看,写好一个自定义hashCode()函数并不是一件容易的事情。在这里,小水滴给出一个经典的hashCode()函数的写法。这种实现方法能应对绝大多数的场景。总的来说,StandardHashcodeExample.hashCode()函数使用了两个素数7和31来帮助计算一个较好的散列值,降低散列值冲突(Collisions)的概率。如果成员变量是对象的话,可以直接使用该对象的hashCode()成员函数。如果成员变量是基本数据类型的话,可以直接使用它们的整数值,或者将其装箱(Boxing)为相应的对象,然后使用该对象的hashCode()函数计算散列值。最后,将每个成员的散列值综合在一起。

总之,目前没有一个哈希函数是完美的。一些IDE也能为开发人员自动生成hashCode()函数。开发人员也可以考虑直接使用下面的例子。在实际运用中,还可考虑使用两个较大的素数。

public class StandardHashcodeExample {
    private int id;
    private String name;

    @Override
    pulibc int hashCode() {
        int hash = 7;
        hash = 31 * hash + id;
        hash = 31 * hash + (name == null ? 0 : name.hashCode());
        return hash;
    }
}

6 结语

本章介绍了Java语言中对象相等性的逻辑,以及==操作符和equals()成员函数的用法和实现细节。总的来说,基本数据类型的数据的比较需要使用==操作符,而对象之间的比较则应使用equals()成员方法。在容器的实现,或者搜索算法的实现中,都使用equals()成员方法测试元素的相等性。因此,小水滴也建议,开发人员应多使用对象和equals()成员方法,尽量减少在函数接口中使用基本数据类型。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.