第八条:覆盖equals是请遵守通用约定
满足下列四个条件之一,就不需要覆盖equals方法:
类的每个实例本质上都已唯一的。不包括代表值的类,如:Integer,String等,Object提供的equals方法就够用了
不关心是否提供了“逻辑相等”的测试功能。对于Random类,用户只关心函数返回的随机数,不会关心产生的两个随机数是不是相等,所以对其进行equal方法覆盖将没有意义
超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。
类是私有的或是包级私有的,并且确定它的equals方法永远不会被调用。同时为了防止该equals被调用,可以如此覆盖:
@Override public boolean equals(Object obj) { throw new AssertionError(); }
注:此处原文是 类是私有的或包级私有的,可以确定它的equals方法永远不会被调用 。但翻译的不太合适,原文为
The class is private or package-private, and you are certain that its equals method will never be invoked.
如果类有自己的“逻辑相等”的概念,通常属于“值类”的情形,且超类还没有覆盖equals以实现期望的行为,此时就需要对euqals进行覆盖。但对于“每个值最多只存在一个对象”的类即单例模式实现的类则不需要覆盖。
在覆盖equals方法时,需要遵守其通用约定:
自反性 。对于任何非null的引用值x,x.equals(x)必须==true。
对称性 。对于任何非null的引用值x和y,当且仅当y.equals(x)==true时,x.equals(y)必须==true。
class CaseInsensitiveString { private final String s; public CaseInsensitiveString(String s) { if (s == null){ throw new NullPointerException(); } this.s = s; } //违反了对称性 //企图与与普通的String对象进行互操作,但是String类中的equals方法并不知道这个类 //一旦违反了对称性,当其他对象面对你的对象时,行为是无法知道的 @Override public boolean equals(Object obj) { if (obj instanceof CaseInsensitiveString){ return s.equalgnoreCase(((CaseInsensitiveString) obj).s); } if (obj instanceof String){ return s.equalsIgnoreCase((String) obj); } return false; } }
传递性 。对于任何非null的引用值x, y和z,如果x.equals(y)==true,并且y.equals(z)==true, 那么对于x.equals(z)也必须==true。
一致性 。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回同样的结果。
可变对象在不同的时候可以与不同的对象相等,而不可变对象则不会这样。无论类是否不可变,都不要使用equals方法依赖于不可靠的资源。
对于任何非null的引用值x,x.equals(null)必须==false。
实现高质量equals方法的诀窍,下列每一项都是基于前一项:
使用==操作符检查“参数是否为这个对象的引用”。性能优化
使用instanceof操作符检查“参数是否为正确的类型”。
把参数转换成正确的类型。
对于该类中的每个“关键”域,检查参数中的域是否对该对象中对应的域相匹配。
对于float,使用Float.compare;对于double,使用Double.compare
有些对象引用域包含null可能是合法的,使用下面的方式避免NullPointException
(field == null ? o.field == null : field.equals(o.field))
域的比较可能会影响到equals性能。应先比较最有可能不一致的域,或是开销最低的域
当你编写完成了equals方法后,问三个问题:是否是对称的、传递的、一致的。同时还需编写测试单元来检验。另外两个特性通常会自动满足。
最后的告诫:
- 覆盖equals时总要覆盖覆盖hashCode
- 不要企图让equals方法过于智能
- 不要将equals声明中的Object对象替换为其他的类型
第九条:覆盖equals时总要覆盖hashCode
在覆盖equals方法时,如果不覆盖hashCode方法,会导致该类无法与HashMap、HashSet和HashTable一起正常运作。相等的对象必须具有相等的散列码。
class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode, int prefix, int lineNumber) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix");
rangeCheck(lineNumber, 999, "line number");
this.areaCode = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
}
private static void rangeCheck(int arg, int max, String name) {
if (arg < 0 || arg > max){
throw new IllegalArgumentException(name + ":" + arg);
}
}
@Override
public boolean equals(Object obj) {
if (obj == this){ //使用 == 操作符检查 “参数是否为这个对象的引用”
return true;
}
if (!(obj instanceof PhoneNumber)){//使用instanceof检查“参数是否为正确的类型”
return false;
}
PhoneNumber pn = (PhoneNumber) obj;//转换为正确的类型
//为了获得最佳性能,先比较最有可能不一样的域
return pn.lineNumber == lineNumber
&& pn.prefix == prefix
&& pn.areaCode == areaCode;
}
}
在执行下列操作时
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5360), "jenny");
//执行下列操作时,虽然已经重写这个值类的equals方法
//但由于前后两个对象的hashCode值不同,所以不会执行我们预期的操作
m.get(new PhoneNumber(707, 867, 5360));
一个好的散列函数通常倾向于“为不想等的对象产生不想等的散列码”。下面是一种简单的解决办法:
把某个非零的常数值,比如说17,保存在一个名为result的int类型的变量中
对于对象中每个关键域 f (知equals方法中涉及的每个域),完成以下步骤:
为该域计算int类型的散列码c:
- 如果该域是boolean类型,则计算( f ? 1 : 0)
- 如果该域是byte、char、short或者int类型,计算(int)f
- 如果该域是long类型,则计算(int)(f ^ (f >>> 32))
- 如果该域是float类型,则计算Float.floatToIntBits(f)
- 如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照2计算
- 如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个“范式”,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0(或者其他某个常熟,但通常是0)
- 如果该域是一个数组,则要把每一个元素当作单独的域来处理。
按照下面额公式,把上面步骤中计算的散列码c合并到result中:
result = 31 * result + c;
返回result
编写单元测试来检验
在散列码的计算过程中。可以把冗余域(该域的值可以通过其他域值计算出来)排除在外。同时必须排除equals比较中没有用到的域。
对于PhoneNumber类,可以这样覆盖其hashCode方法
@override
public int hashCode() {
int result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return result;
}
如果一个类是不可变的,并且计算散列码的开销也比较大,可以考虑把散列码缓存在对象内部。如果这种类型的大多数对象会被用作散列键,就应该在创建实例的时候计算散列码,否则,可以选择直到hashCode被第一次调用的时候才初始化。
不要试图从散列码计算中排除掉一个对象的关键部分来提高性能。
第十条:始终要覆盖toString
建议所有的子类都覆盖这个方法。
在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息。同时决定是否在文档中指定返回值的格式,对于值类,建议这么做,同时再提供一个相匹配的静态工厂或者构造器,以便于程序员可以很容易的在对象和它的字符串表示法之间转换,例如:BigInteger、BigDecimal和绝大多数的基本类型包装类。
但不足之处在于,如果该类被广泛使用,一旦指定格式,即必须始终坚持这种格式,如果在将来的发行版本中改变,就会破坏代码和数据。如果不指定格式,就可以保留灵活性,便于在将来的发行版本中增加信息,或者改进格式。
无论是否指定格式,都应在文档中明确地表明你的意图。同时都为toString返回值中包含的所有信息,提供一种编程式的访问途径。
第十一条:谨慎地覆盖clone
Cloneable接口中没有clone方法,Object的clone方法是protected的。Cloneable决定了Object中受保护的clone方法的实现行为:如果一个类实现了Cloneable,Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportException。
在克隆对象时,如果每个域包含一个基本类型的值,或者包含一个指向不可变对象的引用,那么不需要再做进一步处理。如果对象中包含的域引用了可变的对象,此时就需要使用深拷贝。
实际上,clone方法就是另一个构造器,你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。clone架构与引用可变对象的final域的正常用法是不相兼容的,因为一个域被final修饰后,就无法再调用clone方法对其进行克隆(即赋值)。
克隆复杂对象最后一种办法是先调用super.clone,然后把结果中的所有域都设置成他们的空白状态,然后调用高层的方法来重新产生对象的状态。
覆盖版本的clone方法如果是公有的,就应该将Object中的clone抛出的CloneNotSupportException进行try-catch处理,因为这样会使得覆盖版本中的clone使用起来更加轻松。如果专门为了继承而设计的类,就应该模拟Object.clone的行为,这样使得子类具有实现或不实现Cloneable接口的自由。
如果用线程安全的类实现Cloneable接口,要使得clone方法也有很好的同步。
另一个实现对象拷贝的好办法是提供一个拷贝构造器,或者拷贝工厂,比起Cloneable/clone有以下优势:
- 不依赖域某一种很有风险、语言之外的对象创建机制
- 不要求遵守尚未指定好文档的规范
- 不会与final域的正常使用发生冲突
- 不会抛出不必要的手贱异常
- 不需要进行类型转换
- 可以带一个参数,参数类型是通过该类实现的接口。假设你有一个HashSet,并且希望把他拷贝成一个TreeSet,使用转换构造器:new TreeSet(s)
第十二条:考虑实现Comparable接口
类实现该接口,就表明它的实例具有内在的排序关系,可以跟许多泛型算法以及依赖于该接口的集合实现进行协作,java平台类库中的所有值类都实现了该借口。
将这个对象与指定对象进行比较。当该对象小于、等于或大于指定对象的时候,分别返回一个负整数、零或正整数。若由于指定对象类型无法比较,则抛出ClassCastException。
说明(sgn为符号函数):
- 必须确保所有的x和y都满足sgn(x.compareTo(y)) == -sgn(y.compareTo(x))(也暗示着,当且仅当y.compareTo(x)抛出异常时,x.compareTo(y)才必须抛出异常)
- 必须确保此关系可传递
- 必须确保x.compareTo(y) == 0 暗示着所有的z都满足sgn(x.compareTo(z)) == sgn(y.compareTo(z))
强烈建议(x.compareTo(y) == 0) == (x.equals(y)),但并非绝对必要。一般来说,任何实现了Comparable接口的类,若违反了这个条件,都应明确予以说明。推荐使用的说法:“注意:该类具有内在排序功能,但与equals不一致”
违反compareTo约定的类也会破坏其他依赖于比较关系的类,例如TreeSet和TreeMap,以及Collections和Array。
告诫:无法在用新的值组建扩展可实例化的类时的同时保持compareTo约定,除非愿意放弃面向对象的抽象优势。 如果想为一个实现了Comparable接口的类增加值组建,要编写一个不相关的类,其中包含第一个类的一个实例。
如果遵守上述“说明”中的最后一条,那么由compareTo所施加的顺序关系就被认为“与equals一致”,如果违反这条规则,就是“与equals不一致”,如果不一致,仍然能正常工作,但如果一个有序集合包含了该类元素,该集合可能就无法遵守相应集合接口(Collection, Set, Map)的通用约定。因为,这些接口的通用约定是按照equals来定义的,但是有序集合使用了由compareTo来定义。例如,new BigDecimal(“1.0”)和new BigDecimal(“1.00”)在HashSet中会两个都存在,但是在TreeSet中只存在一个。
Comparable接口是参数化的,而且comparable方法是静态的类型,不必进行类型检查,也不必对它的参数进行类型转换。如果参数不合适,甚至无法编译。
如果一个类用多个关键域,那么必须从最关键的域开始,逐步进行到所有的重要域。如果某个域的比较产生了非零的结果,则整个比较结束。
public int compareTo(PhoneNumber pn){
if (areaCode < pn.areaCode)
return -1;
if (areaCode > pn.areaCode)
return 1;
// area code are equals, compare prefixes
if (prefix < pn.prefix)
return -1;
if (prefix > pn.prefix)
return 1;
//area code and prefixes are equals, compare line number
if (lineNumber < pn.lineNumber)
return -1;
if (lineNumber > pn.lineNumber)
return 1;
//all fields are equals
return 0;
}
如果compareTo的约定没有指定返回值的大小,而只是指定了返回值的符号,可以对上述代码进行简化
public int compareTo(PhoneNumber pn){
int areaCodeDiff = areaCode - pn.areaCode;
if (areaCodeDiff != 0)
return areaCodeDiff;
int prefixDiff = prefix - pn.prefix;
if (prefixDiff != 0)
return prefixDiff;
return lineNumber - pn.lineNumber;
}
但这种简化方法,除非确定相关的域不会为负值,或者更一般的情况:最小和最大的可能域值之差小于或者等于INTEGER.MAX_VALUE。