Item7:覆盖equals时需要遵守通用约定

  在我们日常开发过程中,重写equals是比较常用的,但存在许多不合适的覆盖方式导致错误,最好的避免方法就是不去重写equals。但有时我们的业务又需要建立特定的等价关系,而父类中又没有这种特定的等价关系,我们就要重写equals,我们必须遵守它的通用约定(JAVASE6):

  •   自反性(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)必须返回true;
  • 一致性(consistent):对于非null的引用x,y,只要equals所用的信息没有被修改,多次调用equals的返回值都一致;
  • 对于非null的引用x,   x.equals(null)必须返回false。

下面我们逐条展开讲讲:

自反性:要求对象必须等于其自身,不过多赘述,这条都能达到;

对称性:要求两个引用相互对等,下面举个违反对称性的例子:

public final class CaseInsensitiveString {
private final String s; public CaseInsensitiveString(Srting s){
if(s == null){
throw new NullPointerException();
this.s = s;
}
} @Override
public boolean equals(Object o){
if(o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase( ((CaseInsensitiveString) o).s );
if(o instanceof String)
return s.equlsIgnoreCase((String) o);
return false;
} }
//实例化
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
cis.equals(s);
s.equals(cis);

这是一个不区分大小写的字符串类,结果是:true,false。

究其原因,在于String类中的equals方法并不区分大小写,这就违背了对称性,难以预测该对象的行为。

传递性:要求一个对象等于第二的对象,并且第二个对象等于第三个对象,则第一个对象一定等于第三个对象。下面有一个二维点的类作为引例:

public class Point {
private final int x;
private final int y;
public Point(int x,int y){
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if(!(o instanceof Point))
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
}

接下来写一个它的子类:带颜色的二维点类

public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y,Color color) {
super(x, y);
this.color = color;
} @Override
public boolean equals(Object o){
if(!(o instanceof Point))  //首先判断是否为二维点对象
return false; if(!(o instanceof ColorPoint))  //如果是普通二维点,直接调用普通二维点对象的equals方法
return o.equals(this); return super.equals(o) && ((ClolorPiont) o).color = color;  //最后是带颜色的二维点的判断
}
}

以上的equals方法的确提供了对称性,但牺牲了传递性。观察以下代码:

ColorPoint p1 = new ColorPoint(1,2,Color.RED);
Point p2 = new Point(1,2);
ColorPoint p3 = new ColorPoint(1,2,Color.BLUE);
p1.equals(p2);
p2.equals(p3);
p1.equals(p3);

显然只有p1.equals(p3)返回false。

这就引出了面向对象语言关于等价关系的一个矛盾:无法在扩展可实例化的类的同时,既增加新的组件,又并保留equals约定。目前还没有什么方法能够完美解决这个矛盾,但是权宜之计还是存在的:利用复合代替继承

public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y,Color color) {
point = new Point(x, y);
this.color = color;
} public boolean equals(Object o){
if(!(o instanceof ColorPoint)) //参数强制转换前推荐先判断是否为对应类型
return point;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
} }

直接在有色二维点类中注入二维点对象即可;

一致性:要求两个相等的对象在任意时刻都是相等的(留意不可变对象和可变对象的比较,相等的对象永远相等,不相等的对象永远不相等)

非空性:要求传进的对象为null时,永远返回false

  在equals开始进行instanceof的实例判断,当参数为null时,表达式永远都是false,不必单独进行非空判断;

总结编写高质量equals方法的诀窍:

  • 观察需求能否使用==比较来提高性能
  • 使用instanceof来检查参数是否为正确类型
  • 转换参数类型
  • 对于每个关键域检查参数与this是否匹配

Item8:覆盖equals时总要覆盖hashCode

  结合以下代码讨论覆盖equals的最佳实践;

public final class Url {

    private final String protocal;
private final String host;
private final String port; public Url (String protocal, String host, String port){
this.protocal = protocal;
this.host = host;
this.port = port;
} @Override
public boolean equals(Object o){
if(o == this)
return false;
if(!(o instanceof Url))
return false;
Url url = (Url) o ;
return url.protocal == this.protocal
&& url.host == this.host
&& url.port == this.port;
} }

单看实体类好像并不能看出什么,我们将其对象存进集合内试试:

Map<Url, String> m = new HashMap<Url, String>();
m.put(new Url("http", "www.baidu.com", ""), "百度");

当我们用m.get(new Url("http", "www.baidu.com", ""))获取的时候,并没有得到期望的"百度",而是得到了null值,意味着两者的hashCode不同,但利用我们编写的equals方法却得到两者"相等"的结论,这不符合JavaSE6中对于hashCode的约定:两个相等的实例必须拥有相同的hash码.

  这又是为什么呢?那就不得不稍微提到HashMap的实现,在大学数据结构的课本里概括的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

  哈希函数的构造方法很多,常用的有直接定址法、数字分析法、平方取中法、折叠法、除留余数法、随机数法等..目的就是为了使众多数据"均匀地"分布在不同的区域(散列桶)中,从而降低哈希冲突发生的机率,提高数据的查找效率,Java中每个引用类型对象的hashCode就是这样由不同构造函数生成的,当然一个hashCode可以对应多个对象,这就得出了一个结论:两个hashcode相等的对象不一定是相等的对象。

  再者,避免散列冲突并没有什么好的方法,但解决散列冲突的策略有很多:开放地址法,再哈希法,链地址法,和建立公共溢出区等,这里就不再一一细说,而HashMap底层避免散列冲突的方法是链地址法,将所有关键字为同义词的记录存儲在同一个线性表中,也就是把哈希函数生成的哈希值相等的这些数据存在一个散列桶(hash bucket)里,HashMap做了优化,它缓存了每个元素的hashCode,在判断对象的哈希值不同之后,并不会再判断对象相不相等了,为什么的呢?举个现实中不太恰当的例子:户口中的地址是以省市区的顺序进行判断,如果户籍所在省都不同,那么就没必要看哪个市的哪个区了,哪怕市和区都相同(大概没有这种情况),户籍地址也不可能一样;

  回头再看JavaSE6的约定,有其逆否命题:拥有不同哈希码的对象一定不是相等的实例,就很符合逻辑了.

讨论完规范的由来,我们看看怎样更好的遵从规范:

  1.将非零的常数(常用两位的素数)保存在int类型的变量中

  2.对于对象的每一个关键域f(equals涉及到的域),完成以下步骤:

      • 如果是boolean,则计算(f?1:0)
      • 如果是byte,char,short或int,则计算(int)f
      • 如果是long,计算(int)(f ^ (f>>>32))
      • 如果是float,计算Float.floatToIntBits(f)
      • 如果是double,计算D = Double.doubleToLongBits(f),再(int)(D ^ (D >>> 32))
      • 如果是引用类型,该类的equals方法若有递归,hashCode也用递归的方式,如果有复杂的比较,为域计算一个范式,针对范式调用hashCode,如果域的值为null,则返回0
      • 如果是一个数组,要把每个元素单独当成一个域来处理,递归地执行以上步骤,计算散列码,再将多个散列码组合

  3.组合散列码的公式(c为第二步生成的散列码):

    result = 31 * result + c;

  4.返回result

  5.测试"相等实例是否具有相等的散列码",若不满足,找出原因并修正;

下面是修改后的Url类(独立编写测试通过):

public final class Url {

    private final String protocal;
private final String host;
private final String port; public Url (String protocal, String host, String port){
this.protocal = protocal;
this.host = host;
this.port = port;
} @Override
public boolean equals(Object o){
if(o == this)
return false;
if(!(o instanceof Url))
return false;
Url url = (Url) o ;
return url.protocal == this.protocal
&& url.host == this.host
&& url.port == this.port;
} @Override
public int hashCode() {
//初始化
int result = 17;
//String类型
result = this.protocal.hashCode()+result;
result = this.host.hashCode()+result;
result = this.port.hashCode()+result;
//返回
return result;
} }
public class Example00 {
public static void main(String[] args){
Map<Url, String> m = new HashMap<Url, String>();
m.put(new Url("http", "www.baidu.com", ""), "百度"); System.out.println(m.get(new Url("http", "www.baidu.com", ""))); System.out.println(new Url("http", "www.baidu.com", "").hashCode());
System.out.println(new Url("http", "www.baidu.com", "").hashCode());
}
}

结果:

提升Java代码质量(三)的更多相关文章

  1. 提升Java代码质量(二)

    Item5:消除过期对象的引用 JVM为我们实现了GC(垃圾回收)的功能,让我们从手工管理内存中解放了出来,这固然很好,但并不意味着我们就再也不需要去考虑内存管理的事情了;我们用简单的栈实现的例子来解 ...

  2. 提升Java代码质量(一)

    博主双12入手了一本"Effective Java第二版",本系列文章将初步梳理书中内容,我也查了些资料,我会针对知识点做一点展开,方便以后复习回顾; Item1.考虑用静态工厂代 ...

  3. 拔高你的Java代码质量吧:推荐使用枚举定义常量(转)

    提高你的Java代码质量吧:推荐使用枚举定义常量 一.分析 常量的声明是每一个项目中不可或缺的,在Java1.5之前,我们只有两种方式的声明:类常量和接口常量.不过,在1.5版之后有了改进,即新增了一 ...

  4. 提高Java代码质量的Eclipse插件之Checkstyle的使用详解

    提高Java代码质量的Eclipse插件之Checkstyle的使用详解 CheckStyle是SourceForge下的一个项目,提供了一个帮助JAVA开发人员遵守某些编码规范的工具.它能够自动化代 ...

  5. Eclipse远程调试Java代码的三种方法

    Eclipse远程调试Java代码的三种方法, 第1种方法是用来调试已经启动的Java程序,Eclipse可以随时连接到远程Java程序进行调试, 第2种方法可以调试Java程序启动过程,但是Ecli ...

  6. java 性能优化:35 个小细节,让你提升 java 代码的运行效率

    前言 代码 优化 ,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没 ...

  7. JAVA性能优化:35个小细节让你提升java代码的运行效率

    代码优化,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是, ...

  8. 提高Java代码质量的Eclipse插件之Checkstyle的使用具体解释

    CheckStyle是SourceForge下的一个项目,提供了一个帮助JAVA开发者遵守某些编码规范的工具.它可以自己主动化代码规范检查过程.从而使得开发者从这项重要可是枯燥的任务中解脱出来. Ch ...

  9. Java基础学习总结(72)——提升 java 代码的运行效率

    前言 代码 优化 ,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没 ...

随机推荐

  1. WSDL入门

    WSDL: 网络服务描述语言, 是Web Service的描述语言,它全名:Web Services Description Language,是一门基于 XML 的语言,用于描述 Web Servi ...

  2. 5、预测和鉴定miRNA的靶基因

    转载:http://www.oebiotech.com/Article/mirnabjyyc.html http://www.ebiotrade.com/newsf/2014-9/2014925941 ...

  3. c/c++进制转换练习

    1 下列数最大的是( ).括号内为数字,括号外为进制.(360集团) (10010101)2 (227)8------>10010111 (96)16------>10010110 (14 ...

  4. C# 开发网页的打印版

    在项目中,有一个需求时是需要打印产品页面.但是打印出来的版本和网页上的版本不太一致,有些图片不需要,网页上以tab选项卡显示的内容,都需要在打印页面中看到..等等 CSS针对这种需求,引入了一个@me ...

  5. JavaScript中函数作为值

    function myfunc() { // .. } 这是个函数,这样理解, myfunc只是外层作用域的一个变量,指向刚刚声明的function. 也就是说,function本身就是一个值, 就像 ...

  6. 【msyql_获取时间的前后几天函数date_sub】

    select now()-- 2017-05-16 16:48:02select curdate()  -- 2017-05-16 select curdate() + 1 -- 20170517 s ...

  7. C# Object reference not set to an instance of an object.

    一.问题 Object reference not set to an instance of an object. (你调用的对象是空的) 二.解决问题 在使用 c# 的查询时,先筛选后在关联其他表 ...

  8. 新一代web框架Koa源码学习

    此文已由作者张佃鹏授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. Koa 就是一种简单好用的 Web 框架.它的特点是优雅.简洁.表达力强.自由度高.本身代码只有1000多行 ...

  9. 看一篇,学一篇,今日份的pandas,你该这么学!No.2

    开篇先嘚啵 昨天写到哪了? 睡醒就忘了... ... 不过聪明伶俐的博主,仅用1秒钟就想起来了 我们昨天学了一个pandas的类型series 并且会创建了,厉不厉害 对于一个新的数据结构来说 额,不 ...

  10. Ansible Playbooks基本使用

    你将学到什么 如何使用playbook 如何编写playbook 如何使用roles PlayBook使用 基础环境 ### 64 位 Ubuntu 16.04 LTS,创建CentOS LXC容器w ...