Android 中通用的 equals 与 hashCode 实现
Java 项目的数据模型类常常需要实现 equals 与 hashCode 方法,以保证包含这些类实例的各种标准集合操作正常。而 Android 系统因为 Java 版本问题,实现起来也有其他注意的要点。
前提
equals 方法用于判断两个同一类的实例是否逻辑上相等,如果不提供自定义实现,那么一般默认的实现是判断两个实例是否为同一个对象(即 == 操作符)。 hashCode 方法用于进行 hash 操作,为不同的实例输出不同的哈希代码。而只要实现了自定义的 equals 方法,就要自己定义 hashCode 方法。所以大部分时候只要考虑 equals 的实现要求。
实际上,并不是每一个类都要实现 equals 方法,一般以下的情况不需要实现 equals 方法:
- 类的每个实例本质上都是唯一的,如:Thread
- 不关心任意两个实例是否“逻辑相等”
- 父类 equals 适用于子类
- 类是私有的或包级私有的,equals 方法不会被调用
- 如果“值类”能确保每个值对应一个对象,比如枚举类 这种时候基类 Object 的 equals 与 hashCode 方法就足够了。
其他情况下,尤其是要存入标准集合中的“值类”则需要实现 equals 与 hashCode 方法。
约定
equals 约定
实现自定义 equals 时要遵守的约定(对应同一个类的实例 x 和 y):
- 自反性:非空 x 的 x.equals(x) 为 true
- 对称性:非空 x 和 y,当且仅当 x.equals(y) 为 true 时,y.equals(x) 为 true
- 传递性:非空 x、y 和 z,如果 x.equals(y) 和 y.equals(z) 为 true,则 x.equals(z) 为 true
- 一致性:非空 x 和 y,如果它们比较所用的信息没有更改,则 equals 结果也不能改变
- 非空性:非空 x 必有 x.equals(null) 为 false
对于某些约定又有特别要注意的情况是:
- 传递性:父类如果可实例化,且子类增加了新的值域,则无法保留 equals 约定
- 如果一定要保证约定,可以考虑使用聚合代替继承关系,并在聚合类中提供公有的视图方法返回对应原有类实例。
- Java 类库中的
java.sql.Timestamp
扩展了java.util.Date
,就违反了 equals 约定,注意不要混用它们 - 抽象父类则不必有此顾虑
- 一致性:equals 不应该依赖不可靠的资源,比如:Java 类库的 java.net.URL 的 equals 依赖对 URL 中主机 IP 地址的比较,不确保结果不变,违反了约定
- 非空性:如果实例 x 为 null,则不应该抛出 NullPointerException,而是返回 false
hashCode 约定
hashCode 要遵守的约定:
- 应用程序执行期间,equals 所用的信息没有修改,则多次调用 hashCode 返回信息必须相同。同一应用程序多次执行的返回结果可以不同。
- 两个对象 equals 为 true,则 hashCode 也必须相等
- 两个对象 equals 为 false,则 hashCode 不一定要不同,但不同的结果更好,更利于提高散列表的性能
最佳实践原则
equals
准则与流程
设计高质量 equals 的要点有:
- 使用 == 操作符检查是否为相同引用,是则返回 true(实际是对重型对象的优化)
- 使用
instanceof
检查是否为正确类型,否则返回 false - 把参数转换为正确的类型
- 对于类中比较需要的关键域,依次检查是否相等
- 不是
float
和double
的基本类型使用 == float
和double
使用Float.compare
和Double.compare
- 如果数组所有元素都很关键,则使用
Arrays.equals
- 如果为其他一般对象,则有递归调用 equals 方法,同时注意:
- 如果可能为空,则对于参数 o 有:
field == null ? o.field == null : field.equals(o.field)
- 如果域常常是相同的对象引用,则更高效的方式为:
field == o.field || (field != null && field.equals(o.field))
- 如果对象极为复杂,可考虑使用与它的值对应的替代值进行比较
- 如果可能为空,则对于参数 o 有:
- 先比较可能不等的域,或者比较开销较低的域,最好两者兼备
- 不要比较无关比较逻辑的域
- 不要比较可以由其他域推导出的域
- 不是
- 编写完成后,进行测试
另外,还有注意有些操作是要极力避免的:
- 不要让 equals 处理过于负责的相等逻辑,复杂的相等判断属于业务代码
- 不要把参数改成其他类型(可以标明 @Override 注解,IDE 会智能提醒)
借助标准库
Java 标准库提供了 Objects.equals(Object, Object)
方法,可以方便的判断两个对象是否逻辑相等,而免去大量的条件判断,最好在判断对象的域的时候使用它。但是,要注意这个方法会调用参数对象的 equals 方法,所以不要对没有定义 equals 方法的域使用,总的来说尽量只对基本类型使用它。
但是对于 Android 项目要特别注意,Objects.equals(Object, Object)
方法是在 api 19(KitKat)中引入的,如果 minSdkVersion
的数值小于 19 的话不能很好的使用,必须加上注解 @RequiresApi(api = Build.VERSION_CODES.KITKAT)
或者 @TargetApi(api = Build.VERSION_CODES.KITKAT)
标明方法的适用范围。
但是,使用注解标明的办法使得方法在 api 19 以下的系统中仍然无法正常运行。参考 Objects.equals(Object, Object)
的源代码,可以得到 Android 系统也可正常使用的方法:
|
|
hashCode
可行性方案
一般来说,哈希算法没有过于强硬的实现规定,但仍有一套自行设计 hashCode 方法的简单方案:
- 声明结果变量 result,令其为非零常数,如:17
- 对每个 equals 使用的关键域 f,完成以下步骤:
a. 计算其散列码 code:
- 如果是
boolean
类型,则code = f ? 1 : 0
- 如果是
byte
、char
、short
或int
类型,则code = (int) f
- 如果是
long
类型,则code = f ^ (f >>> 32)
- 如果是
float
类型,则code = Float.floatToIntBits(f)
- 如果是
double
类型,则先计算long temp = Double.doubleToLongBits(f)
再将temp
当作域内容执行long
类型计算,即code = temp ^ (temp >>> 32)
- 如果是对象引用,则递归调用 hashCode;如果对象极为复杂,则考虑对它的替代只使用 hashCode,但替代值必须和它的值对应
- 如果数组每个元素都很重要,则
code = Arrays.hashCode(f)
b. 把散列码 code 合并到结果中:result = 31 * result + code
- 如果是
- 返回 result
- 测试结果是否符合约定
借助标准库
与 equals 相同,Objects
类中也有工具方法 Objects.hash(Object...)
来辅助计算多个对象的哈希数值综合结果。同时,也类似 Objects.equals(Object, Object)
的使用条件,要保证所有参数都有可用的 hashCode 实现。
另外,在 Android 中同样注意要保证 api 19 以下的版本也可正常运行方法。注意到 Objects.hash(Object...)
实际调用了 Arrays.hashCode(Objcet...)
方法,而此方法并在 api 19 以下仍可正常使用,所以 Android 系统通常应该调用 Arrays.hashCode(Objcet...)
方法。
实际例子
这里使用一个简单的例子来说明 equals 与 hashCode 的基本使用:
|
|
这里
@CheckResult
注解保证方法的结果必须被使用,防止其他使用这个工具方法的用户误解它的使用方法。
参考文章
《Effective Java(中文第二版)》 第 3 章:
- 第 8 条:覆盖 equals 时请遵守通用约定
- 第 9 条:覆盖 equals 时总要覆盖 hashCode