天天看点

《Effective Java Third》第三章总结:对于所有对象都通用的方法第三章 对于所有对象都通用的方法

https://sjsdfg.github.io/effective-java-3rd-chinese

https://github.com/clxering/Effective-Java-3rd-edition-Chinese-English-bilingual/

第三章 对于所有对象都通用的方法

10 重写equals方法时遵守通用约定

因为没有哪个类是孤立存在的。一个类的实例常常被传递给另一个类的实例。许多类,包括所有的集合类,都依赖于传递给它们遵守 equals 约定的对象,所以要遵守规定

满足以下任一下条件,则不覆盖 equals 方法:

  • 每个类的实例都是固有唯一的。

    如对于像 Thread 这样代表活动实体而不是值的类来说,这是正确的。

    Object 提供的 equals 实现对这些类来说完全是正确的行为。

  • 类不需要提供一个「逻辑相等(logical equality)」的测试功能。

    例如

    java.util.regex.Pattern

    可以重写 equals 方法检查两个是否代表完全相同的正则表达式 Pattern 实例,但是设计者并不认为客户需要或希望使用此功能。

    在这种情况下,从 Object 继承的 equals 实现是最合适的。

  • 父类已经重写了 equals 方法,且父类行为完全适合于该子类。

    例如,大多数 Set 从 AbstractSet 继承了 equals 实现、List 从 AbstractList 继承了 equals 实现,Map 从 AbstractMap 的 Map 继承了 equals 实现。

  • 类是私有的或包级私有的,可以确定它的 equals 方法永远不会被调用。

    如果非常厌恶风险,可以重写 equals 方法,抛出异常,以确保不会被意外调用:

@Override 
public boolean equals(Object o) {
    throw new AssertionError(); // Method is never called
}
           

什么时候需要重写:

当一个类具有逻辑相等的概念时(不同于对象本身相同的概念),而超类还没有重写equals;

这通常是“值类(value class)”的情况。

值类指的是只表示值的类,例如Integer或String。程序员在利用equals方法来比较对象的引用时,希望知道它们在逻辑上是否相等,而不是像了解它们是否引用了相同的对象;

为了满足程序猿的需求,不仅必须重写equals方法,而且这样做也使得这个类的实例可以被用作映射表(map)的键(key),或者集合(set)的元素,使映射或者集合表现出预期的行为。

不需要重写的值类:

一种是使用实例控制(instance control)(第 1 )的类,以确保每个值至多存在一个对象;

枚举类型(第 34 )属于这个类别。;

对于这些类,逻辑相等与对象标识是一样的,因此对象的 equals 方法函数与逻辑 equals 方法相同

当你重写 equals 方法时,必须遵守它的通用约定。Object 的规范如下: equals 方法实现了一个等价关系(equivalence relation)。它有以下这些属性:

  • 自反性: 对于任何非空引用 x,

    x.equals(x)

    必须返回 true。

如果你违反了它,然后把类的实例添加到一个集合中,那么

contains

方法可能会说集合中没有包含刚添加的实例

  • 对称性: 对于任何非空引用 x 和 y,如果且仅当

    y.equals(x)

    返回 true 时

    x.equals(y)

    必须返回 true。

错误的例子:实现了不区分大小写的字符串的类,但是却与String进行equals相比:

CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";

System.out.println(cis.equals(s)); // true
System.out.println(s.equals(cis)); // false
           

解决方案:删除 equals 方法中与 String 类相互操作的恶意尝试,即如果cis.equals传入的参数如果是String类型,则直接返回false

  • 传递性: 对于任何非空引用 x、y、z,如果

    x.equals(y)

    返回 true,

    y.equals(z)

    返回 true,则

    x.equals(z)

    必须返回 true。

可以通过组合来代替继承,即在不违反 equals 约定的情况下将值组件添加到抽象类的子类中:

public class ColorPoint {
    private final Point point;//组合进Point而不是继承
    private final Color color;//值组件

    public ColorPoint(int x, int y, Color color) {
        point = new Point(x, y);
        this.color = Objects.requireNonNull(color);
    }

    @Override 
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))//Point使用getClass而不是instanceof来判断,这样就有了对称性(都返回false)
            return false;
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }

    ...    // Remainder omitted
}
           
  • 一致性: 对于任何非空引用 x 和 y,如果在 equals 比较中使用的信息没有修改,则

    x.equals(y)

    的多次调用必须始终返回 true 或始终返回 false。

意味着可变对象可以在不同时期可以与不同的对象相等,而不可变对象则不会

如果一个类被设计为不可变类,则其

equals

方法强制执行这样的限制:相等的对象永远相等,不相等的对象永远都不会相等。

  • 非空性(非官方说法):对于任何非空引用 x,

    x.equals(null)

    必须返回 false。

不需要明确的null检查,因为在执行类型转换之前,该方法必须使用 instanceof 运算符来检查其参数是否是正确的类型,而如果第一个操作数为 null,则 instanceof 运算符会返回 false,而不管第二个操作数中出现何种类型,因此也间接达到了检查null的目的

编写高质量 equals 方法的流程:

  1. 使用 == 运算符检查参数是否为该对象的引用。

    如果是,返回 true。这只是一种性能优化,但是如果这种比较可能很昂贵的话,那就值得去做。

  2. 使用

    instanceof

    运算符来检查参数是否具有正确的类型。

    如果不是,则返回 false。

    通常,正确的类型是 equals 方法所在的那个类。 有时候,改类实现了一些接口。 如果类实现了一个接口,该接口可以改进 equals 约定以允许实现接口的类进行比较,那么使用接口。 集合接口(如 Set,List,Map 和 Map.Entry)具有此特性。

  3. 参数转换为正确的类型。因为转换操作在 instanceof 中已经处理过,所以它肯定会成功。
  4. 对于类中的每个「重要」的属性,请检查该参数属性是否与该对象对应的属性相匹配。如果所有这些测试成功,返回 true,否则返回 false。如果步骤 2 中的类型是一个接口,那么必须通过接口方法访问参数的属性;如果类型是类,则可以直接访问属性,这取决于属性的访问权限。

对于类型为非 float 或 double 的基本类型,使用 == 运算符进行比较;

对于对象引用属性,递归地调用 equals 方法;

对于 float 基本类型的属性,使用静态

Float.compare(float, float)

方法;

对于 double 基本类型的属性,使用

Double.compare(double, double)

方法

(因为存在

Float.NaN

-0.0f

和类似的 double 类型的值,所以需要对 float 和 double 属性进行特殊的处理)

虽然可以使用静态方法 Float.equals 和 Double.equals 方法对 float 和 double 基本类型的属性进行比较,但是这会导致每次比较时发生自动装箱,引发非常差的性能;

对于数组属性,将这些准则应用于每个元素。 如果数组属性中的每个元素都很重要,请使用其中一个重载的

Arrays.equals

方法。

某些对象引用的属性可能合法地包含 null。 为避免出现

NullPointerException

异常,请使用静态方法

Objects.equals(Object, Object)

检查这些属性是否相等。

对于一些类,例如上的

CaseInsensitiveString

类,属性比较相对于简单的相等性测试要复杂得多。在这种情况下,你想要保存属性的一个规范形式(canonical form),这样 equals 方法就可以基于这个规范形式去做开销很小的精确比较,来取代开销很大的非标准比较。这种方式其实最适合不可变类(详见第 17 条);

一旦对象发生改变,一定要确保把对应的规范形式更新到最新。

equals 方法的性能可能受到属性比较顺序的影响。 为了获得最佳性能,你应该

首先比较最可能不同的属性,开销比较小的属性,或者最好是两者都满足(derived fields)

。;

不要比较不属于对象逻辑状态的属性,例如用于同步操作的 lock 属性;

不需要比较可以从“重要属性”计算出来的派生属性,但是这样做可以提高 equals 方法的性能;

如果派生属性相当于对整个对象的摘要描述,比较这个属性将节省在比较失败时再去比较实际数据的开销;

例如,假设有一个 Polygon 类,并缓存该区域。 如果两个多边形的面积不相等,则不必费心比较它们的边和顶点。

当完成编写完 equals 方法时,问自己三个问题:它是对称的吗?它是传递吗?它是一致的吗?

除此而外,编写单元测试加以排查,除非使用 AutoValue 框架(第 49 页)来生成 equals 方法,在这种情况下可以安全地省略测试;

如果持有的属性失败,找出原因,并相应地修改 equals 方法。当然,equals 方法也必须满足其他两个属性 (自反性和非空性),但这两个属性通常都会满足。

成功案例:

public final class PhoneNumber {

    private final short areaCode, prefix, lineNum;

    public PhoneNumber(int areaCode, int prefix, int lineNum) {
        this.areaCode = rangeCheck(areaCode, 999, "area code");
        this.prefix = rangeCheck(prefix, 999, "prefix");
        this.lineNum = rangeCheck(lineNum, 9999, "line num");
    }

    private static short rangeCheck(int val, int max, String arg) {
        if (val < 0 || val > max)
            throw new IllegalArgumentException(arg + ": " + val);

        return (short) val;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof PhoneNumber))
            return false;

        PhoneNumber pn = (PhoneNumber) o;

        return pn.lineNum == lineNum && pn.prefix == prefix
                && pn.areaCode == areaCode;
    }
}
           

总之,除非必须:在很多情况下,不要重写 equals 方法,从 Object 继承的实现完全是你想要的。 如果你确实重写了 equals 方法,那么一定要比较这个类的所有重要属性,并且以保护前面 equals 约定里五个规定的方式去比较。

11 重写equals方法时也要重写hashcode方法

在每个类中,在重写 equals 方法的时侯,一定要重写 hashcode 方法。;

如果不这样做,则违反了 hashCode 的通用约定,这会阻止它在 HashMap 和 HashSet 这样的集合中正常工作。

根据 Object 规范,有如下的约定:

  1. 当在一个应用程序执行过程中,如果在 equals 方法比较中没有修改任何信息,在一个对象上重复调用 hashCode 方法时,它必须始终返回相同的值;

    从一个应用程序到另一个应用程序的每一次执行返回的值可以是不一致的。

  2. 如果两个对象根据 equals(Object) 方法比较是相等的,那么在两个对象上调用 hashCode 就必须产生的结果是相同的整数。
  3. 如果两个对象根据 equals(Object) 方法比较并不相等,则不要求在每个对象上调用 hashCode 都必须产生不同的结果。 但是,程序员应该意识到,为不相等的对象生成不同的结果可能会提高散列表(hash tables)的性能。

当无法重写 hashCode 时,所违反第二个关键条款是:相等的对象必须具有相等的哈希码( hash codes)。

根据类的 equals 方法,两个不同的实例可能在逻辑上是相同的,但是对于 Object 类的 hashCode 方法,它们只是两个没有什么共同之处的对象。因此, Object 类的 hashCode 方法返回两个看似随机的数字,而不是按约定要求的两个相等的数字。

不要试图从哈希码计算中排除重要的属性来提高性能

不要为 hashCode 返回的值提供详细的规范,这样客户端就不能合理地依赖它。这(也)给了你更改它的灵活性,否则会阻碍在未来版本中改进hash函数的能力

12 始终重写toString方法

使显示更符合我们的业务逻辑,也更容易调试

实现 toString 方法时,必须做出的一个重要决定是:在文档中指定返回值的格式。

指定格式的好处是它可以作为标准的,明确的,可读的对象表示

缺点是,假设你的类被广泛使用,一旦指定了格式,就会终身使用。

13 谨慎地重写clone方法

Cloneable 接口的目的是作为 mixin 接口(见20),用于让类来宣称它们允许克隆;

不幸的是,它没有达到这个目的;

它的主要缺点是缺少 clone 方法,并且 Object 类的 clone 方法是受保护的;

如果不求助于反射,就不能仅仅因为对象实现了 Cloneable 就能调用 clone 方法;

即使反射调用也可能失败,因为不能保证对象具有可访问的 clone 方法;

尽管存在这样那样的缺陷,但该设施的使用范围相当广泛,因此理解它是值得的;

请注意,不可变类永远不应该提供 clone 方法,因为这只会浪费复制。

什么时候应该这样做呢?其替代方法又是什么呢?

假设你希望在一个类中实现 Cloneable 接口,而它的父类提供了一个行为良好的 clone 方法;

那么首先调用 super.clone,得到的对象将是原始的完全功能的复制品,在你的类中声明的任何属性将具有与原始属性相同的值;

如果每个属性包含原始值或对不可变对象的引用,则返回的对象可能正是你所需要的,在这种情况下,不需要进一步的处理。

super.clone 的调用可以包含在一个 try-catch 块中。 这是因为 Object 声明了它的 clone 方法来抛出

CloneNotSupportedException

异常,这是一个检查时异常;

由于自定义的类实现了 Cloneable 接口,所以我们知道调用 super.clone 会成功;

这里引用的需要表明

CloneNotSupportedException

应该是未被检查的

@Override public PhoneNumber clone() {
    try {
        return (PhoneNumber) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();  // Can't happen
    }
}
           

如果对象包含引用可变对象的属性,则前面的简单 clone 代码实现可能是灾难性的:

引用类型的属性的引用将引用与原始实例相同的属性;

最简单的方法就是把引用类型的属性一起clone,如对于数组实现的栈中的数组:

public Stack clone() {
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}
           

事实上,数组是 clone 机制的唯一有力的用途。

还要注意,如果 elements 字段是 final 的,那么早期的解决方案将不起作用,因为克隆将被禁止为该字段分配新值;

这是一个基本的问题: 像序列化一样,Cloneable 架构与引用可变对象的最终字段的正常使用是不兼容的,除非可变对象可以在对象及其克隆之间安全地共享;

为了使类可克隆,可能需要从某些字段中删除最终修饰符。

与构造方法一样,clone 方法绝对不可以在构建过程中,调用一个可以重写的方法(19);

如果 clone 调用一个在子类中被重写的方法,这个方法将在子类有机会修复其在克隆中的状态之前执行,很可能导致克隆和原始的破坏

在为继承设计一个类时( 19 ),通常有两种选择,但无论选择哪一种,都不应该实现

Clonable

接口。

可以选择通过实现正确运行的受保护的 clone 方法来模仿 Object 的行为,该方法声明为抛出

CloneNotSupportedException

异常。

这给了子类实现

Cloneable

接口的自由,就像直接继承 Object 一样。

或者,可以选择不实现工作的 clone 方法,并通过提供以下简并 clone 实现来阻止子类实现它:

@Override
protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
}
           

如果编写一个实现了 Cloneable 的线程安全的类,它的 clone 方法必须和其他方法一样( 78 )需要正确的同步。 Object 类的 clone 方法是不同步的,所以即使它的实现是令人满意的,也可能需要编写一个返回 super.clone() 的同步 clone 方法。

更好的选择:提供一个用来复制的构造方法或复制工厂;

它们不依赖风险很大的语言外的对象创建机制;

不要求遵守那些不太明确的惯例;

不会与 final 属性的正确使用相冲突;

不会抛出不必要的检查异常; 而且不需要类型转换。

总结:通常,复制功能最好由构造方法或工厂提供。 这个规则的一个明显的例外是数组,它最好用 clone 方法复制。

14 考虑实现Comparable接口

如果编写的值类具有明显的自然顺序,如字母顺序、数字顺序或时间顺序,则应实现 Comparable 接口,重写compareTo 方法,其一般约定类似于 equals 方法:

将一个对象与指定的对象进行顺序比较;

当该对象小于、等于或大于指定对象时,对应返回一个负整数、零或正整数;

如果指定对象的类型阻止它与该对象进行比较,则抛出 ClassCastException。

编写 compareTo 方法类似于编写 equals 方法,但是有一些关键的区别:

因为 Comparable 接口是参数化的,compareTo 方法是静态类型的,所以不需要进行类型检查或强制转换它的参数;

如果参数类型错误,则该调用将不能编译;

如果参数为 null,则调用应该抛出 NullPointerException,并且在方法尝试访问其成员时抛出该异常。

尽量使用

Comparator

的构建方法或静态

compare

方法来代替“-”号:

static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return o1.hashCode() - o2.hashCode();//错误,可能会导致整数最大长度溢出和IEEE 754浮点运算失真的危险
    }
};
           
//使用静态compare方法
static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return Integer.compare(o1.hashCode(), o2.hashCode());
    }
};
//或Comparator的构建方法
static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());
           

总结:无论何时实现具有合理排序的值类,都应该让该类实现

Comparable

接口,以便在基于比较的集合中轻松对其实例进行排序,搜索和使用。

比较

compareTo

方法的实现中的字段值时,请避免使用「<」和「>」运算符,相反,使用包装类中的静态

compare

方法或

Comparator

接口中的构建方法。

继续阅读