effective Java第三章笔记

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

此文章是作者看完《effective Java》第三章后的笔记与总结,第三章强烈推荐去看书籍,因为里面的代码更能让你理解透彻。
此章最有感触的是equals的传递性,以前没有考虑过父子关系的传递,也没有将里氏替换原则带入进行思考,文本中给出使用组合代替继承的方案,也让我感受到了组合的优点

第8条:覆盖equals需要遵循的约定

​ 对于equals方法我相信作为一个Java开发,大家都熟记于心,自反性,对称性,传递性,一致性,自反性:对于非null的引用值a,a.equals(a) = true。对称性:a.equals(b)于b.equals(a),结果一样。传递性:a.equals(b) = true,b.equals(c)=true,则a.equals(c)也要为true。一致性:对于非null的引用a,b,如果equals比较的内容值没有改变,则a.equals(b)的值多次比较应该保持一致。则是大家都知道接下来再说一说书本里面的建议。

超类已经覆盖了equals,从超类继承过来的的行为对于其子类也是适合的

这个建议很重要,在设计模式的5大原理S.O.L.I.D 中,的L 里氏替换原则这个的建议类似,即进行继承关系抽象时,凡是可以用父类或者基类的地方,都可以用子类替换

在此借用书中的例子来说明这个的实际用处。

有一个类 point,其中有属性x,y。其equals方法就是比较着2个属性是否相等来进行判断。

有一天新增了一个工具类,用来判断point这个点是否存在于一个单位圆中,这个圆的具体实现如下

private static final Set<Point> unitCircle;

static{
	unitCircle = new HashSet<Point>();
    unitCircle.add(new Point(0,1));
    unitCircle.add(new Point(1,0));
    unitCircle.add(new Point(0,-1));
    unitCircle.add(new Point(-1,0));
}
public static boolean onunitCircle(Point point){
    return unitCircle.contians(p);
}

判断一个Point是否在这个单位圆中调用这个方法即可。

此时有一个类继承了Point,ColorPoint他多了一个color用来表示颜色。首先你要想的是他的传递性(其余性质基本容易满足),看看一下代码是否满足

public boolean equals(Object object){
	if(!(object instance colorPoint)){
        return false;
    }
    return super.equals(object) && ((colorPoint)object).color == this.color;
}

这是一个很容易踩的坑,很遗憾没有满足对称性,ColorPoint 的对象equals point 返回false。point equals ColorPoint 返回ture。这显然对称性就不会满足。

书中提出可以使用getClass代替instance,这样可以避免这种情况。getClass 2个类一致时踩返回ture,instance当他是他的父类也会返回ture。书中也说,虽然能够实现对称性,即ColorPoint 的对象equals point 返回false,point equals ColorPoint 也返回false。但是这样会导致他会损失一些父类的工具类。比如 前面提到的unitCircle方法。

相对较好的解决方案,拓展类的时候使用 组合来代替继承将ColorPoint改为如下

public class ColorPoint{
	private Point point;
    ptivate String color;
    
    public boolean equals(Object object){
        if(!(o instance ColorPoint)){
            return false;
        }
        ColorPoint cp = (ColorPoint)object;
        return cp.point.equals(this.point) && cp.color == this.color;
    }
}

这样的话传递性,自反性....都已满足,且在使用point的工具类时可以直接将ColorPoint.point传入进行比较!

第9条:覆盖equals方法时总要覆盖Hashcode

如果你覆盖了equals但没有覆盖hashcode,会导致很多散列表无法正常工作。下面是Object的规范

1.对象的equals方法中比较的属性的值没有改变时,其返回的hashcode也不能改变。

2.如果equals为ture,则其hashCode必须相等

3.hashcode相等,equals不一定相同,因为hash存在hash碰撞

一下是书中提到编写hashcode的方法

  1. 把某个非零常数值,比如说17,保存在一个叫result的int类型的变量中。
  2. 对于对象中每一个关键域f(指equals方法中考虑的每一个域),完成以下步骤:
    a. 为该域计算int类型的散列码c:
    i. 如果该域是boolean类型,则计算(f ? 0 : 1) 。
    ii. 如果该域是byte、char、short或者int类型,则计算(int) f。
    iii. 如果该域是long类型,则计算(int) (f^(f>>>32))。
    iv. 如果该域是float类型,则计算Float.floatToIntBits(f)。
    v. 如果该域是double类型,则计算Double.doubleToLongBits(f)得到一个long类型的值,然后 按照步骤2.a .iii,对该long型值计算散列值。
    vi.如果该域是一个对象引用,并且该类的equals方法通过递归调用equals的方式来比较这个域,则同样对这个域递归调用hashCode。如果要求一个更为复杂的比较,则为这个域计算一个“规范表示(canonical representation)”,然后针对这个范式表示调用hashCode。如果这个域的值为null,则返回0(或者其他某个常数,但习惯上使用0)。
    vii. 如果该域是一个数组,则把每一个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.b中的做法把这些散列值组合起来。
    b.按照下面的公式,把步骤a中计算得到的散列码c组合到result中:result =result*37+c
  3. 返回result。

第10条:始终覆盖toString

相信大家都会这么干,不然你print的时候看到一个hash,会有点懵逼。给一个建议,当类的属性很多时,toString只显示关键,有区分度的信息,通常于equals相关。

第11条:谨慎的覆盖clone

这点的话就是注意深拷贝与浅拷贝

第12条:考虑实现Comparable接口

comparable接口与排序相关,在很多容器中都是使用这个进行比较和排序。对于此方法的返回值是一个sgn(表达式),他只中国正负零来进行判断。

书中有一个很重要的提议其原理与equals覆盖的类,要拓展组件时一样即:如果你想为一个实现了Comparable接口的类增加值组件,请不要拓展这个类,而是编写一个不相关的类,其中包含第一个类的一个实例,随后提供一个方法返回这个实例。这样的话可以自由的第二个类上实现comparable方法。同时允许在某些时候使用第一个的实例进行比较。
当比较的为数字且没有负数的时候,溢出可能性不大时可以直接return a-b;