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

尽管Object的是一个具体的类,但是他的设计主要是为了扩展,它所有的非final方法:
- equals
- hashCode
- toString
- clone
- finalize

都有明确的通用约定(general cantract)

第8条 覆盖equals方法时请遵守通用约定

在不覆盖equals方法时,类的所有实例只与它自身相等

以下情况不用覆盖equeal方法。
- 类的每个实例本质上都是唯一的,例如:Thread
- 不关心类是否提供了“逻辑相等(logical equality)”的测试功能,例如:Random实现了equals方法用于判断两个Random产生的随机数是否相同,但是设计者并不认为客户端需要或者期望这样的功能,这样的情况下继承Object的equals方法以及足够满足需求
- 超类覆盖了equals方法,从超类继承过来的行为对于子类是适合的
- 类是私有的或者包级私有的,可以保证它的equal方法永远不会被调用
- 值类不需要覆盖equals方法,即每个值之所存在一个对象。

如果类具有自己特有的逻辑相等概念,而且超类还没有覆盖equals已实现所期望的行为,这个时候我们应该实现equals方法。

下面是实现equals的通用规范
- 自反性——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)也必须返回ture
- 一致性——consistent:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)必须一致的返回相同的结果
- 对于任何非null的值引用x,x.equals(null)必须返回false

如何高质量的实现equals方法:
1. 优先使用==操作符检查参数是否为这个对象的引用,如果是直接返回true,只是一种性能优化
2. 使用instanceof操作检查参数类型
3. 把参数转换成正确的类型
4. 对于该类中的每个关键域,检查参数中的域是否与该对象中对于的域相等
5. 编写完成queals方法后,应该问自己,它是是否对称的,传递的,一致的?
6. 覆盖equals方法时总要覆盖hashCode方法
7. 不要使equals方法过于智能
8. 不要将equals方法声明的Object参数替换成其他类型(实际上这已经不是覆盖了)

如下面示例:

     public static class PhoneNumber {

        private final int areaCode;
        private final int prefix;
        private final int lineNumber;

        public PhoneNumber(int areaCode, int prefix, int lineNumber) {
            this.areaCode = areaCode;
            this.prefix = prefix;
            this.lineNumber = lineNumber;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == this) {
                return true;
            }
            if (!(obj instanceof PhoneNumber)) {
                return false;
            }
            PhoneNumber phoneNumber = (PhoneNumber) obj;
            return phoneNumber.areaCode == areaCode && phoneNumber.prefix == prefix && phoneNumber.lineNumber == lineNumber;
        }
    }

具体可参考《EffectiveJava》

第9条:覆盖equals时总要覆盖hashCode

覆盖equals时总要覆盖hashCode,如果不这么做的话,就违反了Object.hashCode的通用规定,从而导致该类无法结合所有基于散列的集合一起正常工作,这样的集合包括HashMap,HashSet和HashTable。

hashCode有以下约定:
- 在应用程序的执行期间,只要对象的equals方法的比较操作所用的信息没有被修改,那么对这个对象调用多次hashCode方法,都必须始终返回相同的整数(在同一个应用程序多次执行的过程中,可以不一致)。
- 如果两个对象根据equals方法比较是相等的,那么调用两个对象任意一个的hashCode方法都必须产生同样的整数结果
- 如果两个对象根据equals方法比较是不相等的,那么调用两个对象任意一个的hashCode方法则不一定要产生不同的整数结果

但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)的性能

对于第八条给出的例子PhoneNumber:

        HashMap<PhoneNumber, String> hashMap = new HashMap<>();
        hashMap.put(new PhoneNumber(204, 132, 255), "ztiany");

        String s = hashMap.get(new PhoneNumber(204, 132, 255));
        System.out.print(s);
打印结果:null

由于PhoneNumber没有覆盖hashCode方法,导致根据相同的电话号码回去已存入的联系人却返回null。

所以只需要覆盖hashCode方法即可:

        @Override
        public int hashCode() {
            return 42;
        }

这时我们就可以获取到字符串”ztiany”了。

但是这样覆盖hashCode方法是非常恶劣的,它使每个对象都具有了相同的散列码,因此每个对象都被映射到相同的散列桶中,使散列表退化成链表结构。

一个好的散列表函数通常倾向于“为不同的对象产生不相等的散列码”。理想情况下应该把集合中不相等的实均匀的分不到所有可能的散列值上,要达到这种理想的情形很困难,但是接近这种理想的情形则并不困难,下面是一种简单的解决办法:

  1. 把某一个非0常整数,比如17,保存作为一个result的int类型的变量中
  2. 对于对象中每个关键域f(equals中涉及的每个域),完成以下步骤:

    • 如果是boolean类型:计算:f?1:0
    • 如果是byte,char,short或者int类型,计算:(int)f
    • 如果是long类型,则计算:(int)(f^(f>>>32));
    • 如果是flat类型,则计算:Float.floatToIntBits(f);
    • 如果是double类型,计算:Doul,doubleToLongBits(f);
    • 如果是对象引用,并且该类的equals方法通过递归的调用equals方法来比较这个域,则同样为这个域递归的调用hashCode
    • 如果是数组,Arrays.HashCode

    最后按照下面公式合并上述规则计算的结果c合并到result中:
    resut = 31*result+c;

  3. 返回result
  4. 编写单元测试,验证

比如多上面PhoneNumber可以实现如下hashCode方法:

   @Override
        public int hashCode() {
            int result = 17;
            result  = 31 *result + areaCode;
            result  = 31 *result + prefix;
            result  = 31 *result + lineNumber;
            System.out.println(result);
            return result;
        }

第10条:始终要覆盖toString

覆盖toString方法,并且准守一定的规则,可以让类用起来更加舒适,在打印对象的时候,打印的结果更加直观和有意义。

你可能感兴趣的:(对象,HashCode)