第三章 对于所有对象都通用的方法
本章将讲述何时以及如何覆盖这些非final的Object方法。本章也对Comparable.compareTo方法进行讨论,因为它具有类似的特征。
第10条 覆盖equals时请遵守通用约定
-
不宜覆盖equals方法的情形:
-
类的每个实例本质上都是唯一的,对于代表活动实体而不是值的类来说,确实如此
-
类没有必要提供“逻辑相等”的测试功能
-
超类已经覆盖了equals,超类的行为对于这个类也是合适的
-
类是私有的,或者是包级私有的,可以确定它的equals方法永远不会被调用
@Override
public boolean equals(Object o) {
//method is never called
throw new AssertionError();
}
-
如果类具有自己特有的“逻辑相等”(logitical equality)概念,而且超类还没有覆盖equals。这样做也使得这个类的实例可以被用作映射表(map)的键,或者集合(set)的元素,使映射或者集合表现出预期的行为。
-
有一种“值”不需要覆盖equals方法,即用实例受控确保“每个值至多只存在一个对象”的类。枚举类型就属于这种类。对于这样的类而言,逻辑相同与对象等同是一回事。
-
equals约定:等价关系
-
自反性(reflexive):对于任何非null的引用值x, x.equals(x)必须返回true。
-
对称性(symmetric):对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals必须返回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。
-
非空性:对于任何非null的引用值x,x.equals(null)必须返回false。
-
我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象的抽象所带来的优势。
-
一种权益之计:复合优先于继承
/**
* 复合优先于继承
*/
public class ColorPoint {
private final Point point;
private final String color;
public ColorPoint(int x, int y, String color) {
point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
public Point asPoint() {
return point;
}
@Override
public boolean equals(Object object) {
if (!(object instanceof ColorPoint)) {
return false;
}
ColorPoint cp = (ColorPoint)object;
return cp.point.equals(point) && cp.color.equals(color);
}
}
-
高质量equals方法的诀窍:
-
使用 "==" 操作符检查“参数是否为这个对象的引用”,如果是,则返回true。
-
使用 "instanceof" 操作符检查“参数是否为正确的类型”:一般情况下是指equals方法所在的那个类,某些情况下是指该类所实现的某个接口,如Set, List, Map,Map.Entry。
-
把参数转换为正确的类型
-
对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配。
-
对于既不是float也不是double类型的基本类型域,可以使用==操作符进行比较;对于对象引用域,可以递归地调用equals方法;对于float域,可以使用静态Float.compare(float, float)方法;对于double域,则使用Double.compare(double, double)。对于数组域,则要把以上这些指导原则应用到每一个元素上。如果数组域中的每个元素都很重要,就可以使用其中一个Arrays.equals方法;有些对象引用域包含null可能是合法的,则使用Objects.equals(Object, Object)来检查这类域的等同性;
-
为了获取最佳性能,应该最先比较最有可能不一致的域,或者是开销最低的域
-
在编写完equals方法之后,应该问自己三个问题:它是否是对称的,传递的、一致的?
-
覆盖equals时总要覆盖hashCode
-
不要企图让equals方法过于智能。例如:File类不应试图把指向同一个文件的符号链接当作相等的对象来看待。
-
不要将equals声明中的Object对象替换为其他的类型。
-
代替手工编写和测试这些方法的最佳途径,是使用Google开源的AutoValue框架,它会自动替你生成这些方法,通过类中的单个注解(@AutoValue)就能触发(生成的.java文件在class路径下)。IDE也有工具可以生成equals和hashCode方法,但得到的源代码比使用Auto-Value的更加冗长,可读性也更差,它无法追踪类中的变化,因此需要进行测试。
-
总而言之,不要轻易覆盖equals方法,除非迫不得已。
第11条 覆盖equals时总要覆盖hashCode
-
在每个覆盖了equals方法的类中,都必须覆盖hashCode方法
-
约定:
-
只要对象的equals方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用,hashCode方法都必须始终返回同一个值
-
equals(Object)是相等的,那么hashCode必须相同;即 相等的对象必须具有相等的散列码
-
如果equals不等,不要求hashCode不同;但是给不相等的对象产生不同的整数结果,有可能提高散列表的性能
-
如果一个类是不可变的,并且计算散列码的开销也比较大,就应该考虑把散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码。或“延迟初始化”散列码,即一直到hashCode被第一次调用的时候才初始化。
-
不要试图从散列码计算中排除掉一个对象的关键域来提高性能。
-
不要对hashCode方法的返回值做出具体的规定,因此客户端无法理所当然地依赖它,这样可以为修改提供灵活性。
第12条 始终要覆盖toString
-
提供好的toString实现可以使类用起来更加舒适,使用了这个类的系统也更易于调试。
-
toString方法应该返回对象中包含的所有值得关注的信息,或者对象太大时应返回摘要信息。
-
无论是否决定指定格式,都应该在文档中明确地表明你的意图
-
总而言之,toString方法应该以美观的格式返回一个关于对象的简洁、有用的描述。
第13条 谨慎地覆盖clone
-
事实上,实现Cloneable接口的类是为了提供一个功能适当的公有clone方法。
-
不可变的类用于都不应该提供clone方法
-
实际上,clone方法就是另一个构造器;必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件
-
Cloneable架构与引用可变对象的final域的正常用法是不相兼容的。
-
克隆复杂对象的一种方法:先调用super.clone方法,然后把结果对象中的所有域都设置成它们的初始状态,然后调用高层的方法来重新产生对象的状态。
-
clone方法不应该在构造的过程中调用可以覆盖的方法(子类覆盖方法有可能修改对象中的状态)
-
公有的clone方法应该省略throws声明
-
如果你编写线程安全的类准备实现Cloneable接口,要记住它的clone方法必须得到严格的同步,就像其他任何方法一样,即 synchronized clone()。
-
简而言之,所有实现Cloneable接口的类都应该覆盖clone方法,并且是公有的方法,它的返回类型为类本身。该方法应该先调用super.clone, 然后修正任何需要修正的域。
-
对象拷贝的更好方法是提供一个拷贝构造器或拷贝工厂
-
数组例外,最好利用clone方法复制数组
public Yum(Yum yum) {...}
public static Yum newInstance(Yum yum) {...}
第14条 考虑实现Comparable接口
-
违反compareTo约定的类会破坏其他依赖于比较关系的类,例如有序集合类TreeSet和TreeMap,以及工具类Collections和Arrays,它们内部包含有搜索和排序算法。
-
同equals一样,无法在用新的值组件扩展可实例化的类时,同时保持compareTo约定,除非愿意放弃面向对象的抽象优势
-
强烈建议(x.compareTo(y) == 0) == (x.equals(y)),但这并非绝对必要。
-
例如,BigDecimal,它的compareTo与equals不一致,new BigDecimal("1.0")与new BigDecimal("1.00")通过equals比较是不等的,而通过compareTo比较是相等的。若使用HashSet,会保存两个元素。而使用TreeSet则只会包含一个元素。
-
java7版本中,已经在Java的所有装箱基本类型的类中增加了静态的compare方法。
-
从最关键的域开始,逐步比较所有的重要域。如果某个域的比较产生了非零的结果,则整个比较操作结束,并返回该结果。
-
总而言之,每当实现一个对排序敏感的类时,都应该让这个类实现Comparable接口,以便其实例可以轻松地被分类、搜索,以及用在基于比较的集合中。在compareTo方法中比较域值时,都要避免使用<和>操作符,而应该在装箱基本类型的类中使用静态的compare方法,或者在Comparator接口中使用比较器构造方法。