(强制)要求覆写equals必须覆写hashCode(原理分析)

简介: hashCode和equalshashCode和equals用来标识对象,两个方法协同工作可用来判断两个对象是否相等。众所周知,根据生成的哈希将数据散列开来,可以使存取元素更快。对象通过调用Object.hashCode()生成哈希值;由于不可避免会存在哈希值冲突 的情况,因此当hashCode相同时,还需要再调用equals进行一次值的比较;但是若hashCode不同,将直接判定Object不同,跳过equals,这加快了冲突处理效率。Object类定义中对hashCode和equals要求如下:

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));
}
目录
相关文章
|
缓存 小程序 Android开发
mPaaS问题之更改包名之后就进不了小程序如何解决
mPaaS小程序是阿里巴巴移动平台服务(mPaaS)推出的一种轻量级应用解决方案,旨在帮助开发者快速构建跨平台的小程序应用;本合集将聚焦mPaaS小程序的开发流程、技术架构和最佳实践,以及如何解决开发中遇到的问题,从而助力开发者高效打造和维护小程序应用。
251 1
|
人工智能 算法 前端开发
打破传统叙事逻辑,构建基于原子化任务的人机交互
在复杂中后台设计中,为解决配置变更影响多场景问题,提出结合正向和逆向信息架构,采用原子化任务,动态组合任务,降低用户和开发成本,优化体验并改变已有的产品迭代和人机交互模式。未来可能发展为AI自动根据业务规则和用户行为生成最佳方案。
|
XML 设计模式 JSON
QT 项目视图(QListView&QTreeView&QTableView)和项目部件(QListWidget&QTreeWidget&QTableWidget)详解-1
QT 项目视图(QListView&QTreeView&QTableView)和项目部件(QListWidget&QTreeWidget&QTableWidget)详解
|
存储 监控 负载均衡
阿里云上云方案:Web与移动App云上部署解决方案及服务指南
对于绝大部分的上云用户来说,部署Web与移动App是最常见的,很多新手用户不知道上云时该如何选择阿里云产品与配置,为此,阿里云专门针对这部分用户的需求推出了Web与移动App云上部署解决方案,下面是方案详情介绍。
1339 0
阿里云上云方案:Web与移动App云上部署解决方案及服务指南
|
机器学习/深度学习 设计模式 Web App开发
上海交大开源MALib多智能体并行训练框架,支持大规模基于种群的多智能体强化学习训练
基于种群的多智能体深度强化学习(PB-MARL)方法在星际争霸、王者荣耀等游戏AI上已经得到成功验证,MALib 则是首个专门面向 PB-MARL 的开源大规模并行训练框架。MALib 支持丰富的种群训练方式(例如,self-play, PSRO, league training),并且实现和优化了常见多智能体深度强化学习算法,为研究人员降低并行化工作量的同时,大幅提升了训练效率。此外,MALib 基于 Ray 的底层分布式框架,实现了全新的中心化任务分发模型,相较于常见的多智能体强化学习训练框架(RLlib,PyMARL,OpenSpiel),相同硬件条件下吞吐量和训练速度有着数倍的提升。现
822 0
上海交大开源MALib多智能体并行训练框架,支持大规模基于种群的多智能体强化学习训练
|
2天前
|
弹性计算 运维 搜索推荐
三翼鸟携手阿里云ECS g9i:智慧家庭场景的效能革命与未来生活新范式
三翼鸟是海尔智家旗下全球首个智慧家庭场景品牌,致力于提供覆盖衣、食、住、娱的一站式全场景解决方案。截至2025年,服务近1亿家庭,连接设备超5000万台。面对高并发、低延迟与稳定性挑战,全面升级为阿里云ECS g9i实例,实现连接能力提升40%、故障率下降90%、响应速度提升至120ms以内,成本降低20%,推动智慧家庭体验全面跃迁。
|
2天前
|
数据采集 人工智能 自然语言处理
3分钟采集134篇AI文章!深度解析如何通过云无影AgentBay实现25倍并发 + LlamaIndex智能推荐
结合阿里云无影 AgentBay 云端并发采集与 LlamaIndex 智能分析,3分钟高效抓取134篇 AI Agent 文章,实现 AI 推荐、智能问答与知识沉淀,打造从数据获取到价值提炼的完整闭环。
346 91