提升Java代码质量(三)
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代码质量(三)的更多相关文章
- 提升Java代码质量(二)
Item5:消除过期对象的引用 JVM为我们实现了GC(垃圾回收)的功能,让我们从手工管理内存中解放了出来,这固然很好,但并不意味着我们就再也不需要去考虑内存管理的事情了;我们用简单的栈实现的例子来解 ...
- 提升Java代码质量(一)
博主双12入手了一本"Effective Java第二版",本系列文章将初步梳理书中内容,我也查了些资料,我会针对知识点做一点展开,方便以后复习回顾; Item1.考虑用静态工厂代 ...
- 拔高你的Java代码质量吧:推荐使用枚举定义常量(转)
提高你的Java代码质量吧:推荐使用枚举定义常量 一.分析 常量的声明是每一个项目中不可或缺的,在Java1.5之前,我们只有两种方式的声明:类常量和接口常量.不过,在1.5版之后有了改进,即新增了一 ...
- 提高Java代码质量的Eclipse插件之Checkstyle的使用详解
提高Java代码质量的Eclipse插件之Checkstyle的使用详解 CheckStyle是SourceForge下的一个项目,提供了一个帮助JAVA开发人员遵守某些编码规范的工具.它能够自动化代 ...
- Eclipse远程调试Java代码的三种方法
Eclipse远程调试Java代码的三种方法, 第1种方法是用来调试已经启动的Java程序,Eclipse可以随时连接到远程Java程序进行调试, 第2种方法可以调试Java程序启动过程,但是Ecli ...
- java 性能优化:35 个小细节,让你提升 java 代码的运行效率
前言 代码 优化 ,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没 ...
- JAVA性能优化:35个小细节让你提升java代码的运行效率
代码优化,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是, ...
- 提高Java代码质量的Eclipse插件之Checkstyle的使用具体解释
CheckStyle是SourceForge下的一个项目,提供了一个帮助JAVA开发者遵守某些编码规范的工具.它可以自己主动化代码规范检查过程.从而使得开发者从这项重要可是枯燥的任务中解脱出来. Ch ...
- Java基础学习总结(72)——提升 java 代码的运行效率
前言 代码 优化 ,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没 ...
随机推荐
- Angular10 组件之间的通讯
1 父组件和子组件之间的通讯 2 利用中间组件实现两个组件之间的通讯 3 利用服务实现两个组件之间的通讯 2017年8月26日20:09:13 待更新... 1 组件之间的关系图 1.1 父子关系 1 ...
- 项目一:第五天 1、区域数据(pinyin4j-简码,城市编码) 2、Web层代码重构(model对象,分页代码提取) 3、区域分页查询 3、分区添加功能 4、定区管理管理-添加,分页
Service: /** * @Description: 1.保存定区 2.让分区关联定区 * 对象三种状态 1.持久态(被session管理对象-一级缓存中有对象) 2.托管态(有OID标识,数据 ...
- CODING 代码托管架构升级之路
本文为 CODING 创始团队成员王振威在『CODING 技术小馆:上海站』的演讲实录. CODING 技术小馆,是由国内专业的一站式软件服务平台 CODING 主办的一系列技术沙龙.将邀请数位业内知 ...
- JavaScript中函数作为值
function myfunc() { // .. } 这是个函数,这样理解, myfunc只是外层作用域的一个变量,指向刚刚声明的function. 也就是说,function本身就是一个值, 就像 ...
- Java静态检测工具/Java代码规范和质量检查简单介绍(转)
静态检查: 静态测试包括代码检查.静态结构分析.代码质量度量等.它可以由人工进行,充分发挥人的逻辑思维优势,也可以借助软件工具自动进行.代码检查代码检查包括代码走查.桌面检查.代码审查等,主要检查代码 ...
- Ubuntu14跑DSO
按照https://github.com/JakobEngel/dso上的说明,make -j4的时候出现一下错误: /home/zhao/dso/src/FullSystem/CoarseIniti ...
- 会过vip怎么赚钱?大学生,宝妈创业圈子
会过vip怎么赚钱?是骗局吗1.如果说会过Vip不赚钱,那么一定是我们的没有时间去真心的热爱她:如果你对她如果你对它抱着一颗平和的心,认真的去分享,认真的去听一些前辈的经验,赚钱真的不是难事:2.如果 ...
- VS(Visual Studio)中快速找出含中文的字符串
环境:visual studio 2017 1.ctrl + shift + f 打卡全局查找 2.输入(".*[\u4E00-\u9FA5]+)|([\u4E00-\u9FA5]+.*&q ...
- [Xcode 实际操作]五、使用表格-(1)使用UITableView制作简单表格
目录:[Swift]Xcode实际操作 本文将演示表格视图的使用方法. 在项目导航区,打开视图控制器的代码文件[ViewController.swift] import UIKit //首先添加两个协 ...
- iOS端实现节日换肤
本文是我在网上看到一篇不错的文章,因为之前没接触过,所以特意转过来,和大家一起分享下..以下正文: 一.问题的提出 不知道大家有没有发现, 元旦期间, 很多APP界面里的图标都换成了具有节日气氛的样式 ...