尽管Object是一个具体类,但是设计Object类主要是为了扩展。它的所有非final方法(equals、hashCode、toString、clone和finalize)都有明确的通用约定,因为它们就是被设计成要被覆盖的。
第八条:覆盖equals时请遵守通用约定
覆盖equals方法看似简单,但是很多覆盖方式会导致错误,并且后果非常严重。最容易避免这类问题的方法就是不覆盖equals方法,这种情况下,类的每个实例都只与它自身相等。如果满足以下任何一个条件,这就正是所期望的结果:
1、类的每个实例本质上都是唯一的。例如Thread类。
2、不关心类是否提供了“逻辑相等”的测试功能。
3、超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。
4、类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用。
但是如果类具有自己特有的“逻辑相等”概念(不等同于对象相等的概念),而且超类还咩有覆盖equals以实现期望的行为,这时我们就要覆盖equals方法。这通常属于“值类”的情形。
有一种“值类”不需要覆盖equals方法,即用实例受控确保“每个值至多只存在一个对象”(即单例类)的类、枚举类型就属于这种类。对于这种类,逻辑相等与对象相等是同一回事。
在覆盖equals方法的时候,必须要遵守它的通用约定,内容如下:
1、自反性(reflexive)。对象必须等于其自身。
2、对称性(symmetric)。任何两个对象对于“它们是否相等”的问题都必须保持一致。
3、传递性(transitive)。如果一个对象等于第二个对象,并且第二个对象又等于第三个对象,则第一个对象一定等于第三个对象。
4、一致性(consistent)。如果两个对象相等,它们就必须时钟保持相等,除非它们中有一个对象(或者两个都)被修改了。
5、非空性(Non-nullity)。对于任何非null的引用值x,x.equals(null)必须返回false。即所有的对象都必须不等于null。
结合所有这些要求,得出一下实现高质量equals方法的诀窍:
1、使用==操作符检查“参数是否为这个对象的引用”。如果是,则返回true。这算是一种性能优化。
2、使用instanceof操作符检查“参数是否为正确的类型”。如果不是则返回false。
3、把参数转换成正确的类型。因为转换之前进行过instanceof测试,所以确保会成功。
4、对于该类中的每个“关键(significant)域,检查参数中的域是否域该对象中对应的域相匹配。如果这些测试全部成功,则返回true,否则返回false。
5、当编写完成了equals方法之后,最后问自己三个问题:它是否是对称的、传递的、一致的?并编写单元测试来检验这些特性!
下面是最后一些告诫:
1、覆盖equals时总要覆盖hashCode(见下一条)
2、不要企图让equals方法过于智能。过度地寻求各种等价关系,容易自找麻烦。
3、不要将equals声明中的Object对象替换为其他的类型。(使用@Override注解可以防止这种错误)
第九条:覆盖equals时总要覆盖hashCode
一个很常见的错误根源在于没有覆盖hashCode方法。
覆盖hashCode方法的约定如下:
1、在应用程序的执行期间,只要对象equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个程序的多次执行过程中,每次执行所返回的整数可以不一致。
2、如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象hashCode方法都必须产生同样的整数结果。
3、如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象hashCode方法,则不一定要产生不同不同的整数结果。但是我们应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)的性能。
考虑下面的类:
public final 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,9999,"lineNumber"); 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 o) { if(o == this) return true; if(!(o instanceof PhoneNumber)) { return false; } PhoneNumber pn = (PhoneNumber)o; return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode; //未重写hashCode方法 ... } }
这时,若考虑:
Map<PhoneNumber,String> m = new HashMap<>(); m.put(new PhoneNumber(123,456,789),"Jenny");
如果期望调用:
m.get(new PhoneNumber(123,456,789));
返回的是“Jenny”的话,实际上无法做到,因为它返回的是null,因为这里有两个PhoneNumber实例,第一个被插入到Map的散列桶中,第二个用于获取该对象,但两个对象的散列码不同,因为hashCode默认返回的是对象的地址值,get方法会首先判断Map中是否有与目标对象的hashCode相同的对象,显然,这是两个对象,hashCode明显不同,于是返回的结果为false,也就找不到了。所以需要重写hashCode方法,好的重写方式是为不相等的对象产生不相等的散列码,为相等的对象产生相等的散列码,即如果两个对象equals为true,那么两个对象的hashCode必相等,如果两个对象equals为false,那么两个对象的hashCode不相等。
可以按照以下思路重写hashCode方法:
1、把某个非零的常数值,比如说17,保存在一个名为result的into类型的变量中。
2、对于对象中每个关键域f(指equals方法中涉及的每个域),完成以下步骤:
a.为该域计算int类型的散列码c:
1)如果该域是boolean类型,则计算(f?1:0).
2)如果该域是byte、char、short或者int类型,则计算(int)f。
3)如果该域是long类型,则计算(int)(f^(f>>>32))。
4)如果该域是float类型,则计算Float.floatToIntBits(f)。
5)如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤2. a. 3) 为得到的long类型值计算散列值。
6)如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个“范式”,然后农户针对这个范式调用hashCode。如果这个域的值为null,则返回0(或者其他某个常数,但通常是0)。
7)如果该域是一个数组,则要把每一个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据2. b的做法把这些散列值组合起来。
b.按照下面的工时,把步骤2. a中计算得到的散列码c合并到result中:
result = 31 * result + c;
3、返回result。
4、写完了hashCode方法之后,问问自己“相等的实例是否都具有相等散列码”。要编写单元测试来验证推断。
为上述PhoneNumber类覆盖hashCode方法:
@Override public int hashCode() { int result = 17; result = 31 * result + areaCode; result = 31 * result + prefix; result = 31 * result + lineNumber; return result; }还可将缓存起来:
private volatile int hashCode; ... @Override public int hashCode() { int result = hashCode; if(result == 0) { result = 17; result = 31 * result + areaCode; result = 31 * result + prefix; result = 31 * result + lineNumber; } return result; }
第十条:始终要覆盖toString
java.lang.Object提供了toString方法的一个实现,它返回是:类的名称,以及一个“@”符号,接着是散列码的无符号十六进制表示法,例如"PhoneNumber@163b91"。
在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息。
要为toString返回值中包含的所有信息,提供一种编程式的访问途径(如getter方法)。
第十一条:谨慎地覆盖clone
如需要克隆对象,需要实现Cloneable接口。
有关clone方法的详解,可以参考这篇文章:
第十二条:考虑实现Comparable接口
compareTo方法并没有在Object中声明。它是Comparable接口中唯一的方法。compareTo方法不单允许进行简单的等同性比较,而且允许顺序比较。除此之外,它与Object的equals方法具有相似的特征,它还是个泛型。类实现了Comparable接口,就表明它的实例具有内在的排序关系。为实现Comparable接口的对象数组进行排序非常简单:Arrays.sort(a);。对存储在集合中的Comparable对象进行搜索、计算极限值以及自动维护也同样简单。例如,下面的代码依赖于String实现了Comparable接口,它去除命令行参数列表中的重复参数,并按字母顺序打印出来:
package Effective_Java; import java.util.Collections; import java.util.Set; import java.util.TreeSet; /** * @author [email protected] * @date 18-5-27 下午12:11 */ public class Demo1 { public static void main(String[] args) { Set<String> set = new TreeSet<>(); Collections.addAll(set, args); System.out.println(set); } }
一旦类实现了Comparable接口,它就可以跟许多泛型算法以及依赖于该接口的集合实现进行协作。付出很小的努力就可以获得非常强大的功能。
compareTo方法的通用约定与equals方法的相似:将这个对象与指定的对象进行比较。当该对象小雨、等于或大于指定对象的时候,分别返回一个负整数、零或者正整数。如果由于指定的对象的类型而无法与该对象进行比较,则抛出ClassCastException异常。
package Effective_Java; /** * @author [email protected] * @date 18-5-27 下午12:11 */ public class Demo1 implements Comparable<Demo1> { public int a, b, c; public Demo1(int a, int b, int c) { this.a = a; this.b = b; this.c = c; } /** * Compares this object with the specified object for order. Returns a * negative integer, zero, or a positive integer as this object is less * than, equal to, or greater than the specified object. * * <p>The implementor must ensure <tt>sgn(x.compareTo(y)) == * -sgn(y.compareTo(x))</tt> for all <tt>x</tt> and <tt>y</tt>. (This * implies that <tt>x.compareTo(y)</tt> must throw an exception iff * <tt>y.compareTo(x)</tt> throws an exception.) * * <p>The implementor must also ensure that the relation is transitive: * <tt>(x.compareTo(y)>0 && y.compareTo(z)>0)</tt> implies * <tt>x.compareTo(z)>0</tt>. * * <p>Finally, the implementor must ensure that <tt>x.compareTo(y)==0</tt> * implies that <tt>sgn(x.compareTo(z)) == sgn(y.compareTo(z))</tt>, for * all <tt>z</tt>. * * <p>It is strongly recommended, but <i>not</i> strictly required that * <tt>(x.compareTo(y)==0) == (x.equals(y))</tt>. Generally speaking, any * class that implements the <tt>Comparable</tt> interface and violates * this condition should clearly indicate this fact. The recommended * language is "Note: this class has a natural ordering that is * inconsistent with equals." * * <p>In the foregoing description, the notation * <tt>sgn(</tt><i>expression</i><tt>)</tt> designates the mathematical * <i>signum</i> function, which is defined to return one of <tt>-1</tt>, * <tt>0</tt>, or <tt>1</tt> according to whether the value of * <i>expression</i> is negative, zero or positive. * * @param o the object to be compared. * @return a negative integer, zero, or a positive integer as this object * is less than, equal to, or greater than the specified object. * @throws NullPointerException if the specified object is null * @throws ClassCastException if the specified object's type prevents it * from being compared to this object. */ @Override public int compareTo(Demo1 o) { int result = a - o.a; if(result != 0) { return result; } result = b- o.b; if(result != 0) { return result; } return c - o.c; } }
总结:本章主要讲解了在面向对象(对象在哪!)编程时,需要对Object类中的一些方法进行覆盖时所要注意的一些点,以及在涉及到比较操作时,实现Comparable接口所需要注意的一些事情。共勉!