【读书笔记】《Effective Java》(2)--对于所有对象都通用的方法

又读了一章,之前一直觉得Java的体系很完善了,读了这一章,发现原来Java平台本身也有一些设计不周到的地方,而且有些地方因为已经成为公开API的一部分还不好改,相信继续读下去对Java的了解会更深一步的。

昨天下载了VS Code,尝试了一下,感觉比sublime还要用一些,尤其Markdown支持预览,不用设置,正好我觉得在oneNote上做笔记格式不好控制,就学了下Markdown的语法,用md写下了这一章的笔记…

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

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

  • 不需要覆盖equals的情况:

    1. 类的每个实例本质上都是唯一的。
    2. 不关心类是否提供了“逻辑相等”的测试功能。
    3. 超类已经覆盖了equals方法,且从超类继承过来的行为对于子类来说也合适。
    4. 类是私有的或者包级私有的,可以确定他的equals方法永远不会被调用
  • equals方法实现了等价关系(和离散数学里的一样),也就是:

    1. 自反性:

    自身相等;

  • 对称性:
    x和y,有x.equals(y),就有y.equals(x);
  • 传递性:
    有x、y、z,当x.equals(y)、y.equals(z),就有x.equals(z);
  • 一致性:
    多次调用equals方法,结果一样;
  • 非空性(这一条不是离散里的):
    x.equals(null)结果为false;
  • equals方法要注意的地方:

    1. 对于传递性:我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定

      对于这种情况,有一种做法是将equals方法中检测Object是否为类的实例的方法:

      if(o instaceof MyClass){…};

      换成另外一种:

      if(o==null|o.getClass()!=getClass()){…}

      但是这种方法并不好,根据 里氏替换原则 ,一个类型的任何重要属性也要适用于它的子类,而这种方法会导致equals中传入父类无法被识别,
      一个好的做法是使用复合替代继承,这个方法将在之后提到。

      • 一个Java没做好的地方:java.sql.Timestamp类对java.util.Date类进行了扩展,但这两个类的equals方法违反了对称性
    2. 对于一致性:不要将equals方法依赖于不可靠的资源,确保当类本身不变时,多次调用equals方法返回的结果一致。
      • 一个Java没做好的地方:java.net,URL的equals方法依赖于主机的IP地址,而同一个主机名多个IP也是常见的,这导致了一些问题。
    3. 对于非空性:对于equals方法传入的Object,需要检验是否为空,当时用instanceof时,这是不需要的,因为instanceof null 返回false,但是对于上述出现的使用getClass方法,这是需要的,因为null.getClass()将报错。
  • 综上,实现高质量equals方法的要点:

    1. 使用==操作符检测,检验是否是同一个对象的应用,这对于重型对象的比较来说有助于提高性能;
    2. 使用intanceof方法操作符检测“参数是否为正确的类型”,这个操作符对于实现接口的类、父类都返回true;
    3. 把参数转化成正确的类
    4. 对于该类的每个 “关键域”,检查参数中的域是否与该对象对应的域相匹配

      • 对于既不是float和double的基本类型,可以使用==直接比较
      • 对于float和double基本类型,因为存在比较精度的问题,使用Float.compare()和Double.compare()方法
      • 对于引用类型,递归调用equals方法
      • 对于允许为空的引用类型域,为了避免抛出空指针异常,可以使用如下格式:

      (field==null?o.field==null:field.equals(o.field))
      或者(field==o.field||(field!=null&&field.equals(o.field))),这种形式对于相同的对象引用更快一些

  • 编写完equals方法,想一想是否满足自反性、对称性、传递性、一致性,并且单元测试

  • 覆盖equals方法总是同时覆盖hashCode方法(下一条详述)
  • 不要企图让equals方法过于智能,仅仅负责简单检测的equals方法易于编写,但是考虑太多例外情况会使代码过于复杂
  • 不要将equals方法传入的参数更换成其他类,始终将其保持为Object,否则将不会覆盖超类的方法,这种情况属于重载了equals方法,一个方便的做法是使用@override注解检测

  • 9.覆盖equal方法时总是覆盖hashCode方法

    • 为什么:为了遵循hashCode的通用规定:

      1. 只要equals方法用到的信息没有改变,hashCode多次调用返回的值应当相同
      2. equals一致的对象需要返回相同的hashCode
      3. 不同的对象则不要求返回相同的hashCode,但返回不同的hashCode有助于提高散列表的性能
    • 如何编写一个好的hashCode:

      1. 首先,选中某一个非零的常数,保存为result(通常选择质奇数,尤其看好31,因为31有一个很好的特性,对31的乘法可以通过移位和减法完成[31*i==(i<<5)-i])
      2. 对于对象的每一个关键域,做如下操作:

        a. 为该域计算int类型的散列值c:

        • 如果是bool,计算f?1:0;
        • 如果是byte、char、short、int,则计算(int)f;
        • 如果是long,计算(int)(f^(f>>>32))
        • 如果是float,计算Float.floatToIntBits(f)
        • 如果是Double,计算Double.doubleToLongBits(f),然后再按long的规则转化
        • 如果是域是一个对象,并且对象的equals方法对于这个对象域递归调用equals,则hashCode方法同样递归调用这个对象域的hashCode,
          如果对象域的比较很复杂,可以设计一个范式(规定一个计算方式),按计算公式返回hashCode
        • 如果对象是一个数组。对数组中的每一个元素求hashCode,在Java1.5以后,Array有一个方法Array.hashCode,可以使用

        b. 将上述计算的int值按照如下公式相加合并:

        • result=31*result+c;
      3. 返回result;
      4. 测试
    • 注意点:

      1. 如果一个域的值可以由其他关键域求出,则这个域可以被排除在外
      2. equals中没有用到的域尽量不要出现在hashCode的计算中,着很有可能违反“equals方法一致,hashCode方法的值也要一致的约定”
    • 两个不好的行为:

      1. 保存hashCode的确切值用于其他函数中,或者计算中,因为hashCode的产生收到计算函数的影响,可能会出现更改计算函数内部实现的情况,这样的话hashCode的确切值就会改变,依赖于他们(确切hashCode值)的函数就会出问题
      2. 试图排除一个对象的关键部分来提高散列计算的性能,这会导致散列的性能(分散的能力)下降,导致散列线性的查找性能变成平方级别,这个行为曾在Java1.2的字符串中出现过

      为了提高计算散列码的性能,可以考虑将散列码缓存到对象内部,以及延迟计算的方法


    10.始终要覆盖toString

    • 原因:对象使用起来更加清晰,调试起来更方便

    • 注意点:

      1. 默认toString 方法返回的是@+散列码的无符号16进制表示
      2. toString应当包含一个对象的所有值得关注的信息,如果对象太大或者状态信息不好用字符串表示,这时候应该返回摘要信息
      3. 注意头String的格式,并将它写入文档

        因为toString是公开的,也就是说,可能有客户代码使用toString做持久化或者其他依赖于toString格式的用途,这个时候,一个清晰、永不更改的格式是尤为重要的,如果不能提供一个稳定不变的格式,也要写入文档提醒

      4. 最好配套一个静态工厂方法,用于将对象和对象的toString返回的字符串之间相互转化


    11.谨慎地覆盖clone (看完之后的想法是个人绝不建议使用clone,对它无视掉最好

    • 背景知识:

      1. clone方法是Object的保护方法,当类没有实现Cloneable接口时,调用抛出CloneNotSupportedException异常,实现接口的话,Object的子类把clone重写为public的方法,方可调用(书上的意思是:也需要通过反射机制才有可能成功调用clone方法,我想这是指内部调用细节或者特指Object的调用,其他类的clone是public的,从表面看不需要反射)
      2. 默认的约定是不在clone方法中调用构造器,但是这个约定不太好遵守,因为clone的行为就像是一个构造器一样,似乎完全可以调用构造器来帮助完成任务
      3. 对于继承的类,clone方法有类似于构造器调用链的机制,即子类clone方法调用super.clone,一直调用到Object的clone
    • clone方法需要遵守的通用约定:

      1. x.clone()!=x 结果为true
      2. x.clone().getClass()==x.getClass()为true
      3. x.clone().equals(x)结果为true
    • 雷区:

      1. 如果子类的父类没有按规定实现一个适合的clone方法,子类的clone方法必然会失败,比如父类的clone方法中调用构造器,这回返回一个错误的类
      2. 如果类的对象域涉及到深拷贝需要特别注意,因为clone返回的是一个==操作返回false,而equals方法返回true的对象,这意味着内存空间不能共享
      3. 在深拷贝过程中,如果有域是final的话就麻烦了,会有编译错误“cannot be assigned”,因为clone方法禁止给final的域赋值,这个时候可能需要考虑去掉final关键字
      4. 和构造器一样,clone原则上不应该调用任何非final的方法,因为非final方法可能被子类重写实现,这会导致clone的不确定性
      5. 线程安全的类,clone方法需要客户代码自己同步
    • 最后的建议:使用拷贝构造器或者拷贝工厂而不是clone方法会是个好选择。


    12. 考虑实现Comparable接口

    • 优点:实现后在配合泛型算法以及基于它们的集合过程中表现好

    • 注意点:

      1. 实现一个好的Comparable接口需要遵守和equals相同的约定,缺点也是相同的:我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留compareTo的约定
      2. 同时,最好等同性测试compareTo和equals方法的结果相同,不同的话并不会导致灾难性的影响,不过会有些怪异。要知道有序集合通过compareTo检测等同性,而一般集合使用equals
      3. compareTo是顺序比较,所以比较的时候要注意比较顺序,从一个类的最关键的域开始,逐个比较到不重要的域
      4. 如果通过减法实现比较,返回结果的符号,需要注意结果的范围是否会超过int的最大值,这种问题比较难以发现
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值