(强制)要求覆写equals必须覆写hashCode(原理分析)
hashCode和equals
hashCode和equals用来标识对象,两个方法协同工作可用来判断两个对象是否相等。众所周知,根据生成的哈希将数据散列开来,可以使存取元素更快。对象通过调用Object.hashCode()生成哈希值
;由于不可避免会存在哈希值冲突 的情况
,因此当hashCode相同时,还需要再调用equals进行一次值的比较;但是若hashCode不同,将直接判定Object不同,跳过equals,这加快了冲突处理效率
。Object类定义中对hashCode和equals要求如下:
(1)如果两个对象的equals的结果是相等的,则两个对象的hashCode的返回值结果也必要是相同的。
(2)任何时候覆写equals,都必须同时覆写hashCode。
在Map和Set集合中,用到这两个方法时,首先判断hashCode的值,如果hash相等,则再判断equals的结果,HashMap的get判断代码如下
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){
return (e = getNode(hash(key), key)) ==null ? null :e.value;
}
if条件表达式中的e.hash == hash是先决条件,只有相等才会执行&&后的部分。如果不相等,则&&后面的equals根本不会被执行。equals不相等时并不强制要求hashCode也不相等,但一个优秀的哈希算法应尽可能地让元素均匀分布,降低冲突概率,即在equals不相等时hashCode也不相等,这样&&或||短路
操作一旦生效,会极大提高程序的执行效率。如果自定义对象作为Map的键
,那么必须覆写hashCode和equals。此外,因为Set存储的是不可重复的对象。依据hashCode和equals进行判断,所以Set存储的自定义对象也必须覆写这两个方法。此时如果覆写了equals,而没有覆写hashCode,具体会有什么影响,让我们通过以下代码深入体会:
public class EqualsObject{
privite int id;
privite String name;
public EqualsObject(int id, String name){
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object obj){
// 如果为null,或者并非同类,则直接返回false(第一处)
if (obj == null || this.getClass() != obj.class()){
return false;
}
// 如果引用指向同一个对象,则返回true
if(this == obj){
return true;
}
// 需要强制类型转换来获取EqualsObject的方法
EqualsObject temp = (EqualsObject)obj;
// 本示例判断标准是两个属性值相等,逻辑随业务场景不同而不同
if (temp.getId() == this.id && name.equals(temp.getName())){
return true;
}
return false;
}
}
第一处说明:首先判断两个对象的类型是否相同,如果不匹配则直接返回false
。此处使用getClass的方式,就是严格限制了只有EqualsObject对象本身才可以执行equals操作。
这里并没有覆写hashCode,那么把这个对象放到Set集合中去:
Set<EqualsObject> hashSet = new HashSet<>();
EqualsObject a = new EqualsObject(1,"one");
EqualsObject b = new EqualsObject(1,"one");
EqualsObject c = new EqualsObject(1,"one");
hashSet.add(a);
hashSet.add(b);
hashSet.add(c);
System.out.println(hashSet.size());
输出结果是3,。虽然这些对象显而易见是相同的
,但在HashSet操作中,应该只剩下一个,为什么结果是3呢?因为如果不覆写hashCode(),即使equals()相等也毫无意义,Object.hashCode()的实现是默认为每一个对象生成不同的int值,它本身是Native
方法,一般与对象内存地址有关。下面查看C++的源码实现:
VM_ENTRY(jint, JVM_IHashCode(JNIEnv* env,jobject handle))
JVMWrapper("JVM_IHashCode");
return handle == NULL ? 0 : objectSynchronizer::FashHashCode
(THREAN,JNIHandles::resolve_non_null(handle));
VM_END
ObjectSynchronizer的核心代码如下,从代码分析角度也印证了hashCode就是根据对象的地址进行相关计算得到int类型数值的:
mark = monitor->header();
assert(mark->is_neutral(),"invariant");
hash = mark->hash();
intptr_t hash() const{
return mask_bits(value() >> hash_shift, hash_mask);
}
因为EqualsObject没有覆写hashCode,所以得到的是一个与对象地址相关的唯一值,回到刚才的HashSet集合上
,如果想存储不重复的元素,那么需要在EqualsObject类中覆写hashCode();
@Override
public int hashCode(){
return id + name.hashCode();
}
EqualsObject的name属性是String类型,String覆写了hashCode(),所以可以直接调用。equals()的实现方式与类的具体逻辑
有关,但又各部相同,因而应尽量分析源码来确定其判断结果,比如下列代码:
public class ListEquals(){
public static void main(String[] args){
LinkedList<Integer> linkedList = new LinkedList<Integer>();
linkedList.add(1);
ArrayList<Integer> arrayList = new ArrayList<Integer>();
arrayList.add(1);
if (arrayList.equals(linkedList)){
System.out.println("equals is true");
} else {
System.out.println("equals is false");
}
}
}
两个不同的集合类,输出的结果是equals is true。因为ArrayList的equals()只进行了是否为List子类的判断
,接着调用了equalsRange()方法:
boolean equalsRange(List<?> other,int form,int to){
final Object[] es = elementData;
//用var变量接受linkedList的遍历器(第一处)
var oit = other.iterator();
for(; form < to; form++){
//如果linkedList没有元素,则equals结果直接为false;
//如果linkList有元素,则再对应的下标进行值的比较(第二处)
if (!oit.hasNext() || !Objects.equals(es[form], oit.next())){
return false;
}
}
// 如果ArrayList已经遍历完,而linkList还有元素,则equals结果为false
return !oit.hasNext;
}
第一处说明:局部变量类型推断(Local Variable Type Inference)是JDK10引入的变量命名机制
,一改Java是强类型语言的传统形象,这是Java致力于未来体积跟小、面向生产效率的新语言特性,减少累赘的语法规则,当然这仅仅是一个语法糖
,Java仍然是一种静态语言。在初始化阶段,在处理var变量的时候,编译器会检测右侧代码的返回类型,并将其类型用于左侧,如下所示:
var a = "String";
// 输出:class java.lang.String
System.out.println(a.getClass());
var b = Integer.valueOf(7);
// 输出:class java.lang.Integer
System.out.println(b.getClass());
// 编译出错。虽然是var,但是依然存在类型限定
b = 3.0;
b在第一次赋值时,类型推断为Integer,所以在第二次赋值为double时编译出错。如果一个方法内频繁地使用var,则会大大降低可读性,这是一个权衡,建议当用var定义变量时,尽量不要超过两个
。
第二处说明:尽量避免通过实例对象引用来调用equals方法,否则容易抛出空指针异常。推荐使用JDK7引入的Objects的equals
方法,源码如下,可以有效地防止equals调用时产生NPE问题:
public static boolean equals(Object a,Object b){
return (a==b) || (a != null && a.equals(b));
}
(强制)要求覆写equals必须覆写hashCode(原理分析)的更多相关文章
- [改善Java代码]覆写equals方法必须覆写hashCode方法
覆写equals方法必须覆写hashCode方法,这条规则基本上每个Javaer都知道,这也是JDK API上反复说明的,不过为什么要这样做呢?这两个方法之间有什么关系呢?本建议就来解释该问题,我们先 ...
- [改善Java代码]覆写equals方法时不要识别不出自己
建议45: 覆写equals方法时不要识别不出自己 我们在写一个JavaBean时,经常会覆写equals方法,其目的是根据业务规则判断两个对象是否相等,比如我们写一个Person类,然后根据姓名判断 ...
- java覆写equals方法
何时需要重写equals() 当一个类有自己特有的“逻辑相等”概念(不同于对象身份的概念). object规范规定,如果要重写equals(),也要重写hashcode() 如何覆写equals() ...
- 为什么覆写equals()方法的时候总是要覆写hashcode()?
要回答这个问题,我们应该先认识一下obj中的equals和hascode方法 1.equals()方法在obj中定义如下: public boolean equals(Object obj) { re ...
- 为什么覆写equals必须要覆写hashCode?
============================================= 原文链接: 为什么覆写equals必须要覆写hashCode? 转载请注明出处! ============= ...
- 覆写equals方法为什么需要覆写hashCode方法
覆写equals方法必须覆写hashCode方法,是JDK API上反复说明的,不过为什么要这样做呢?这两个方法之间有什么关系呢? void test() { // Person类的实例作为Map的k ...
- 8.2.3 覆写 Equals
经过对四种不同类型判等方法的讨论,我们不难发现不管是 Equals 静态方法.Equals 虚方法 抑或==操作符的执行结果,都可能受到覆写 Equals 方法的影响.因此研究对象判等就必须将注意 力 ...
- NOR Flash擦写和原理分析
NOR Flash擦写和原理分析 1. NOR FLASH 的简单介绍 NOR FLASH 是很常见的一种存储芯片,数据掉电不会丢失.NOR FLASH支持Execute On Chip,即程序可以直 ...
- Markdown: 用写代码的思维写文档
作者:吴香伟 发表于 2014/08/07 版权声明:可以任意转载,转载时务必以超链接形式标明文章原始出处和作者信息以及版权声明 本文不讲解Markdown的语法规则,只关注它带来的好处以及我使用的方 ...
随机推荐
- .Net Core微服务——自动收缩、健康检查:Consul(三)
继续上一篇的话题,顺便放上一篇的传送门:点这里. 健康检查 经过之前的操作,我的consul已经支持自动扩展,并且调用也很靠谱.但是这里有个问题,一旦服务列表里的某个服务挂了,consul并不知道,还 ...
- Lesson2 Thirteen equals one
Lesson2 Thirteen equals one equal ['i:kwəl] v. 等于 He equaled the world record. Nobody equals him i ...
- PAT乙级:1063 计算谱半径 (20分)
PAT乙级:1063 计算谱半径 (20分) 题干 在数学中,矩阵的"谱半径"是指其特征值的模集合的上确界.换言之,对于给定的 n 个复数空间的特征值 { a1+b1i,⋯,a** ...
- SQL慢查询排查思路
前言 平时在工作中每天都会做巡检,将前一天所有超过500ms的慢SQL排查出来 查找原因,是否能进行优化.慢慢中,在形成了一套思路方法论. 我个人认为对于排查慢SQL还是有一定的帮助 (一).是否是S ...
- SDN与OpenFlow架构--初识
一,为什么需要SDN 1,传统网络的缺点: a,传统网络及其设备的只可配置,不可编程,只能按照已定义好的协议处理或转发数据,不能适应需求新变化,不能自主开发新功能. 如购买一个电饭煲,可以煮饭,煲汤. ...
- Mysql 基础用法
#创建表 CREATE TABLE table_name (column_name int) CREATE TABLE IF NOT EXISTS `runoob_tbl`( `runoob_id` ...
- Intouch/ifix语音报警系统制作(4-自动发送邮件提醒)
在近期项目完成后,有遇到情况:类似于语音报警后,中控室人员未及时报告给我们造成了事件的危害升级,以及造成很不好的影响.针对这个情况特此添加语音报警后,自动发送邮件提醒,完善现有的报警机制. 1.函数编 ...
- 如何使用SQL Server实现SignalR的横向扩展
一般来说,Web应用的扩展有两种:scale up(纵向扩展)和scale out(横向扩展). 1.纵向扩展 使用配置高(大内存,多处理器)的服务器或者虚拟机. 2.横向扩展 使用多个服务器(Web ...
- Vue2中父子组件通信的几种常用方法
源码地址 点击查看演示源码 Vue2父子传参:props 首先在父组件中引入子组件,然后以属性的方式将数据传递给子组件 父组件: <template> <div class=&quo ...
- Git点赞82K!字节跳动保姆级Android学习指南,干货满满
这是一份全面详细的<Android学习指南>,如果你是新手,那么下面的内容可以帮助你找到学习的线路:如果你是老手,这篇文章列出的内容也可以帮助你查漏补缺.如果各位有什么其他的建议,欢迎留言 ...