前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Effective Java(二)

Effective Java(二)

作者头像
Remember_Ray
发布2020-08-03 23:15:12
4380
发布2020-08-03 23:15:12
举报
文章被收录于专栏:Ray学习笔记Ray学习笔记

对于所有对象都通用的方法

尽管 Object 是一个具体类,但设计它主要是为了扩展。它所有的非 final 方法(equals、hashCode、toString、clone和finalize )都有明确的通用约定( general contract), 因为它们设计成是要被覆盖( override )的。

覆盖 equals 时请遵守通用约定

覆盖 equals 方法看起来似乎很简单,但是有许多覆盖方式会导致错误,并且后果非常严重。最容易避免这类问题的办法就是不覆盖 equals 方法,在这种情况下,类的每个实例都只与它自身相等。如果满足了以下任何一个条件,这就正是所期望的结果:

  • 类的每个实例本质上都是唯一的。对于代表活动实体而不是值(value)的类来说确实如此,例如 Thread。Object 提供的 equals 实现对于这些类来说正是正确的行为。
  • 类没有必要提供“逻辑相等”(logical equality)的测试功能。例如,java.util.regex.Pattern 可以覆盖 equals,以检查两个 Pattern 实例是否代表同一个正则表达式,但是设计者并不认为客户需要或者期待这样的功能。在这类情况之下,从Object继承得到的 equals 实现已经足够了。
  • 超类已经覆盖了 equals,超类的行为对于这个类也是适合的。例如,大多数的 Set 实现都从 AbstractSet 继承 equals 实现,List 实现从 AbstractList 继承 equals 实现, Map 实现从 AbstractMap 继承 equals 实现。
  • 类是私有的,或者是包级私有的,可以确定它的 equals 方法永远不会被调用。

等价关系(equivalence relation)

不严格地说,等价关系是一个操作符,将一组元素划分到其元素与另一个元素等价的分组中。这些分组被称作等价类(equivalence class)。从用户的角度来看,对于有用的 equals 方法,每个等价类中的所有元素都必须是可交换的。

在覆盖 equals 方法的时候,必须要遵守它的通用约定。下面是约定的内容,来自 Object 的规范。 equals 方法实现了等价关系(equivalence relation)其属性如下:

  • 自反性(reflexive):对于任何非 null 的引用值 x, x.equals(x) 必须返回true。
  • 对称性(symmetric):对于任何非 null 的引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时, x.equals(y) 必须返回true。
  • 传递性(transitive):对于任何非 null 的引用值 x、y和z,如果 x.equals(y) 返回true,并且 y.equals(z) 也返回true,那么 x.equals(z) 也必须返回true。
  • 一致性(consistent):对于任何非 null 的引用值 x和y,只要 equals 的比较操作在对象中所用的信息没有被修改,多次调用 x.equals(y) 就会一致地返回 true,或者一致地返回 false。

现在我们按照顺序逐一查看以下5个要求:

  1. 自反性(Reflexivity) 第一个要求仅仅说明对象必须等于其自身。很难想象会无意识地违反这一条。假如违背了这一条,然后把该类的实例添加到集合中,该集合的 contains 方法将果断地告诉你,该集合不包含你刚刚添加的实例。
  2. 对称性(Symmetry) 第二个要求是说,任何两个对象对于“它们是否相等”的问题都必须保持一致。与第一个要求不同,若无意中违反这一条,这种情形倒是不难想象。
  3. 传递性(Transitivity) equals约定的第三个要求是,如果一个对象等于第二个对象,而第二个对象又等于第三个对象,则第一个对象一定等于第三个对象。同样地,无意识地违反这条规则的情形也不难想象。
  4. 一致性(Consistency) equals约定的第四个要求是,如果两个对象相等,它们就必须始终保持相等,除非它们中有一个对象(或者两个都)被修改了。换句话说,可变对象在不同的时候可以与不同的对象相等,而不可变对象则不会这样。
  5. 非空性(Non-nullity) 最后一个要求没有正式名称,我姑且称它为“非空性”,意思是指所有的对象都不能等于null。尽管很难想象在什么情况下 o.equals(null) 调用会意外地返回true,但是意外抛出 NullPointerException 异常的情形却不难想象。通用约定不允许抛出 NullPointerException 异常。许多类的 equals 方法都通过一个显式的 null 测试来防止这种情况。许多类的 equals 方法都通过一个显式的null测试来防止这种情况:
代码语言:javascript
复制
@Override
public boolean equals(Object obj) {
    if (obj == null) {
        return false;
    }

    ...
}

这项测试是没必要的。为了测试其参数的同等性,equals方法必须先把参数转换成适当的类型,以便可以调用它的访问方法,或者访问它的域。在进行转换之前,equals方法必须使用 instanceof 操作符,检查其参数的类型是否正确:

代码语言:javascript
复制
@Override
public boolean equals(Object obj) {
    if (!(obj instanceof EqualsDemo)) {
        return false;
    }
    
    EqualsDemo ed = (EqualsDemo) obj;

    ...
}

如果漏掉了这一步的类型检查,并且传递给 equals 方法的参数又是错误的类型,那么 equals 方法将会抛出 ClassCastException 异常,这就违反了 equals 约定。但是,如果 instanceof 的第一个操作数为 null, 那么,不管第二个操作数是哪种类型,instanceof 操作符都指定应该返回 false。因此,如果把 null 传给 equals 方法,类型检查就会返回 false,所以不需要显式的 null 检查。

高质量 equals 方法的诀窍

  1. 使用 == 操作符检查“参数是否为这个对象的引用”。如果是,则返回 true。这只不过是一种性能优化,如果比较操作有可能很昂贵,就值得这么做。
  2. 使用 instanceof 操作符检查“参数是否为正确的类型”。如果不是,则返回 false。一般来说,所谓“正确的类型”是指 equals 方法所在的那个类。某些情况下,是指该类所实现的某个接口。如果类实现的接口改进了 equals 约定,允许在实现了该接口的类之间进行比较,那么就使用接口。集合接口如 Set、List、Map和Map.Entry 具有这样的特性。
  3. 把参数转换成正确的类型。因为转换之前进行过 instanceof 测试,所以确保会成功。
  4. 对于该类中的每个“关键(significant)”域,检查参数中的域是否与该对象中对应的域相匹配。

域的比较顺序可能会影响 equals 方法的性能。为了获得最佳的性能,应该最先比较最有可能不一致的域,或者是开销最低的域,最理想的情况是两个条件同时满足的域。

在编写完 equals 方法之后,应该问自己三个问题:它是否对称的、传递的、一致的?并且不要只是自问,还要编写单元测试来检验这些特性,除非用 AutoValue 生成 equals 方法,在这种情况下就可以放心地省略测试。

下面是最后的一些告诫:

  1. 覆盖 equals 时总要覆盖 hashCode
  2. 不要企图让 equals 方法过于智能
  3. 不要将 equals 声明中的 Object 对象替换为其他的类型

总结

总而言之,不要轻易覆盖 equals 方法,除非迫不得已。因为在许多情况下,从 Object 处继承的实现正是你想要的。如果覆盖 equals,一定要比较这个类的所有关键域,并且查看它们是否遵守 equals 合约的所有五个条款。

覆盖 equals 时总要覆盖 hashCode

在每个覆盖了 equals 方法的类中,都必须覆盖 hashCode 方法。如果不这样做的话,就会违反 hashCode 的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这类集合包括 HashMap 和 HashSet。下面是约定的内容,摘自 Object 规范:

  • 在应用程序的执行期间,只要对象的 equals 方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用,hashCode 方法都必须始终返回同一个值。在一个应用程序与另一个程序的执行过程中,执行 hashCode 方法所返回的值可以不一致。
  • 如果两个对象根据 equals(Object) 方法比较是相等的,那么调用这两个对象中的 hashCode 方法都必须产生同样的整数结果。
  • 如果两个对象根据 equals(Object) 方法比较是不相等的,那么调用这两个对象中的 hashCode 方法,则不一定要求 hashCode 方法必须产生不同的结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)的性能。

因没有覆盖 hashCode 而违反的关键约定是第二条:相等的对象必须具有相等的散列码(hash code)。

一个好的散列函数通常倾向于“为不相等的对象产生不相等的散列码”。这正是 hashCode 约定中的第三条的含义。理想情况下,散列函数应该把集合中不相等的实例均匀地分布到所有可能的 int 值上。

下面给出一种简单的解决办法:

1、声明一个 int 变量并命名为 result,将它初始化为对象中第一个关键域的散列码 c,如步骤2.1中计算所示(关键域是指影响 equals 比较的域)。 2、对象中剩下的每一个关键域 f 都完成以下步骤: 2.1 为该域计算 int 类型的散列码 c: 2.1.1 如果该域是基本类型,则计算 Type.hashCode(f),这里的 Type 是装箱基本类型的类,与 f 的类型相对于。 2.1.2 如果该域是一个对象引用,并且该类的 equals 方法通过递归地调用 equals 的方式来比较这个域,则同样为这个域递归地调用 hashCode。如果需要更复杂的比较,则为这个域计算一个“范式(canonical representation)”,然后针对这个范式调用 hashCode。如果这个域的值为 null,则返回0(或者其他某个常数,但通常是0)。 2.2 按照下面的公式,把步骤2.1 中计算得到的散列码 c 合并到 result中:

代码语言:javascript
复制
result = 31 * result + c;

3、返回 result。

写好了 hashCode 方法之后,问问自己“相等的实例是否都具有相等的散列码”。

之所以选择31,是因为它是一个奇素数。 31有个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能:

代码语言:javascript
复制
31 * i == (i << 5) - i

延迟初始化(lazily initialize)散列码

“延迟初始化”散列码,即一直到 hashCode 被第一次调用的时候才初始化。注意 hashCode 域的初始值(0)一般不能成为创建的实例的散列码:

代码语言:javascript
复制
private int hashCode; // 初始化0

@Test
public int hashCodeDemo() {

    int result = hashCode;

    if (result == 0) {
        // 省略 result 计算
        //result = 31 * result + Short.hashCode(str);
        hashCode = result;
    }
    return result;
}

不要试图从散列码计算中排除掉一个对象的关键域来提高性能。

总结

总而言之,每当覆盖 equals 方法时都必须覆盖 hashCode, 否则程序将无法正确运行。hashCode 方法必须遵守 Object 规定的通用约定,并且必须完成一定的工作,将不相等的散列码分配给不相等的实例。

始终要覆盖 toString

toString 约定进一步指出,“建议所有的子类都覆盖这个方法”。这是一个很好的建议,真的!

提供好的 toString 实现可以使类用起来更加舒适,使用了这个类的系统也更易于调试。

在实际应用中,toString 方法应该返回对象中包含额所有值得关注的信息

总结

总而言之,要在你编写的每一个可实例化的类中覆盖 Object 的 toString 实现,除非已经在超类中这么做了。这样会使类使用起来更加舒适,也更易于调试。toString 方法应该以美观的格式返回一个关于对象的简洁、有用的描述。

谨慎地覆盖 clone

Cloneable 接口的目的是作为对象的一个 mixin接口(mixin interface),表明这样的对象允许克隆(clone)。遗憾的是,它并没有成功地达到这个目的。它的主要缺陷在于缺少一个 clone 方法,而 Object 的 clone 方法是受保护的。如果不借助于反射(reflection),就不能仅仅因为一个对象实现了 Cloneable,就调用 clone 方法。即使是反射调用也可能会失败,因为不能保证该对象一定具有可访问的 clone 方法。

既然Cloneab1e接口并没有包含任何方法,那么它到底有什么作用呢?它决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException异常。这是接口的一种极端非典型的用法,也不值得仿效。通常情况下,实现接口是为了表明类可以为它的客户做些什么。然而,对于Cloneab1e接口,它改变了超类中受保护的方法的行为。

虽然规范中没有明确指出,事实上,实现Cloneable接口的类是为了提供一个功能适当的公有的clone方法。为了达到这个目的,类及其所有超类都必须遵守一个相当复杂的、不可实施的,并且基本上没有文档说明的协议。由此得到一种语言之外的(extralinguistic)机制:它无须调用构造器就可以创建对象。

clone方法的通用约定是非常弱的,下面是来自 Object 规范中的约定内容: 创建和返回该对象的一个拷贝。这个“拷贝”的精准含义取决于该对象的类。一般的含义是,对于任何对象 x,表达式

代码语言:javascript
复制
x.clone() != x

将会返回结果 true,并且表达式

代码语言:javascript
复制
x.clone().getClass() == x.getClass()

将会返回结果 true,但这些都不是绝对的要求。虽然通常情况下,表达式

代码语言:javascript
复制
x.clone().equals(x)

将会返回结果 true,但是,这也不是一个绝对的要求。 按照约定,这个方法返回的对象应该通过调用 super.clone 获得。如果类及其超类(Object除外)遵守这一约定,那么:

代码语言:javascript
复制
x.clone().getClass() == x.getClass()

按照约定,返回的对象应该不依赖于被克隆的对象。为了成功地实现这种独立性,可能需要在 super.clone 返回对象之前,修改对象的一个或更多个域。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-02-09|,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 对于所有对象都通用的方法
    • 覆盖 equals 时请遵守通用约定
      • 等价关系(equivalence relation)
      • 高质量 equals 方法的诀窍
      • 总结
    • 覆盖 equals 时总要覆盖 hashCode
      • 下面给出一种简单的解决办法:
      • 延迟初始化(lazily initialize)散列码
      • 总结
    • 始终要覆盖 toString
      • 总结
    • 谨慎地覆盖 clone
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档