集合里的元素怎么“不见了”?

点击上方 "程序员小乐"关注, 星标或置顶一起成长

每天凌晨00点00分, 第一时间与你相约

 

每日英文

Sometimes there is no next time, no second chance, no time out. Sometimes it is now or never.

有时候,没有下一次,没有机会重来,没有暂停继续。有时候,错过了现在,就永远永远的没机会了。

 

每日掏心

其实,痛是很难分享出去的,你复述一次你就伤一次,别人没共鸣,你就更受伤。隐忍不说,只是做事,时间会让痛过去。

 

 

 

 

来自:卡瓦邦噶!| 作者:赖信涛 | 责编:乐乐

链接:kawabangga.com/posts/3941

程序员小乐(ID:study_tech)第 802 次推文   图片来自百度

 

往日回顾:小米回应暴力裁员:已提前三个月通知不续签合同,并且给了N+1补偿

 

     

   正文   

 

昨天花时间在 debug 一个非常诡异的问题。Java 代码里面的一个 HashSet 集合里面命令包含这个元素,equals、hashCode 都一样,甚至对象的 id 都是一样的,但是 contains 方法返回的结果总是 false !最后花了很多时间,百思不得其解,一度怀疑我生活在 Matrix 里面。最后发现问题的一刻也恍然大悟,发现这是一个我早就知道的问题。这必定成为我职业生涯的一个污点,所以我打算记录一下这个问题。

 

先卖个关子吧,我来描述一下问题的背景,看你能否想到答案。

 

问题是这样的,我们用 JGraphT 来解决一个图的问题。这个图是我们从应用的调用关系链中生成的,生成之后会导出到 json,放到一个地方。然后所有的计算节点都可以通过这个 json 来 load 图,就不用每个节点都去清洗一遍了。一个节点清洗过后,所有的节点都从这里加载。问题主要出现图的导出和导入,图中每个节点都有一个 id,一开始我用应用的名字作为 id,导出到 json,但是导入的时候发现 Importer 会重新生成 ID,图的关系是对的,但是节点的 ID 从字符串变成了重新生成的 id 了,那么应用名字的信息就丢失了。我又给节点加上 name 属性,期望这个属性 import 之后还是好的。结果发现 import 只是 import 图的关系,并没有 import 进来其他属性(这个库看起来很 nice 啊,不知道为啥文档这么差,import 的细节都没有文档)。于是我参考 Test 里面的做法,用一个 Map 存下来节点的其他属性。然后在 import 完成之后,将这些属性 set 进去。

 

OK,总结一下,简单来说就是,我先从 json 导入进图,导入的时候也存下来每个节点的属性(其实就是 name),导入之后遍历图的节点,将每个属性设置进去。

 

问题就出现了。我用图来找最短路径的时候报错:节点不存在!

 

定位到库里面,判断节点不存在的 contains函数是这么写的:

 

 

我 debug 了这个 Set 和 v 的关系,发现 Set 中的一个元素,跟 v 是一模一样的!对象 id 都是一样的。

 

equals 返回值是一样的:

 

 

hashCode 返回值也一样:

 

 

但是这个 contains 函数就是返回 false。

 

为了让这个问题更明显一些,我把这个问题简化成下面这段 Java 代码,可以直接运行:

 

			import java.util.HashSet;
import java.util.Objects;
 
public class Vertex {
    private String id;
    private String name;
 
    public Vertex(String id, String name) {
        this.id = id;
        this.name = name;
    }
 
    public static void main(String[] args) {
        Vertex app1 = new Vertex("1", null);
        Vertex app2 = new Vertex("2", null);
        Vertex app3 = new Vertex("3", null);
        // 模拟我们从 json 载入这个图的过程
        // 这个时候 name 是不在图里面的
        HashSet<Vertex> sets = new HashSet<>();
        sets.add(app1);
        sets.add(app2);
        sets.add(app3);
        // 载入之后,我们会将属性设置好,欢迎应用名字的信息
        app1.name = "app1";
        app2.name = "app2";
        app3.name = "app3";
 
        // 返回 false
        System.out.println(sets.contains(app1));
        System.out.println(sets.stream().filter(x -> x.hashCode() == app1.hashCode()).findFirst());
        System.out.println(sets.stream().filter(x -> x.equals(app1)).findFirst());
 
    }
 
    @Override
    public boolean equals(Object o) {
        if (this == o) { return true; }
        if (o == null || getClass() != o.getClass()) { return false; }
        Vertex vertex = (Vertex) o;
        return Objects.equals(id, vertex.id) &&
                Objects.equals(name, vertex.name);
    }
 
    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}
 
运行结果如下:
 
 
$ javac Vertex.java
$ java Vertex
false
Optional[Vertex@2dd3e0]
Optional[Vertex@2dd3e0]

明明 equals 和 hashCode 都一样,为什么 contains 就是 false 呢?

 

答案就在查找 Hash 表的方式。我之前写过一篇文章《Hash碰撞和解决策略》,介绍如果发生 hash 碰撞,那么 hash 表一般会通过某种方式存放 hash 相同的元素。这就要求,在 hash 表中查找元素的时候,必须满足以下两个条件,才算是找到了元素:

 

  1. 按照 hash 值能找到这个元素所在的 hash 位置,但是这个位置存放着很多 hash 值相同的元素,所以还要满足2;

  2. 必须满足相等(equals)。

 

Hash碰撞和解决策略: kawabangga.com/posts/2493

 

HashSet 其实就是没有 value 的 HashMap,本质上也是个 hash 表,所以 contains 要返回 true,也必须满足上面两个条件。元素在存进去的时候,name 是空的,按照 name 是 null 得到了一个 hash 值,放到了 HashMap 的一个地方,记作位置 A。然后我后来修改 name 的值,再 hash 的时候,就会得到另一个 hash 值,记作位置 B.。然后 contains 去位置 B 一看,这个位置是个null,就认为这个元素不在集合中了。

 

 

 

 

为什么 hashCode 和 equals 返回都是相等的呢?因为我们先按照 name = null 保存了进去,保存的时候 hash 值已经确定了。后来修改了 name,hash 值已经不会修改(不会在 HashSet 里面移动的)。虽然对象即使是同一个对象,但是 hash 值已经和放进去的时候变了。拿现在的对象(Set里面的那个对象,和现在的要确定是否被 contains 的对象,都是“现在的对象”,name 已经被修改了的)来对比 hash 值肯定是相等的,但是已经和放进去的时候的那个 hash 值不同了。去看 HashSet 中,现在的这个 hash 值的位置,肯定是个 null,所以判断为元素不存在。

 

简单总结一下,就是放入 Hash 中的元素,一定要是不可修改的(这个和 Python 为什么 list 不能作为字典的 key?的原理是一样的)。如果修改了,那这个元素就从集合中找不回来了。

 

最后,从这个故事中我们能学到什么呢?

 

感觉学不到什么,现在回想起来就跟自己的智商受到了降维打击一样。

 

哦,对了。如果你看懂了这个问题,那么就会理解,之所以找不到这个元素是因为这个元素放进去的时候的 hashCode 和现在的这个元素的 hashCode 已经不一样了。我不禁回忆起另外一个问题:

 

有三个人去住旅馆,住三间房,每一间房$10元,于是他们一共付给老板$30,第二天,老板觉得三间房只需要$25元就够了,于是叫小弟退回$5给三位客人。

 

谁知小弟贪心,只退回每人$1,自己偷偷拿了$2,这样一来便等于那三位客人每人各花了九元,于是三个人一共花了$27,再加上小弟独吞了$2,总共是$29。可是当初他们三个人一共付出$30那么还有$1呢?

 

欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,学习能力的提升上有新的认识,欢迎转发分享给更多人。

欢迎各位读者加入订阅号程序员小乐技术群,在后台回复“加群”或者“学习”即可。

猜你还想看

 

阿里、腾讯、百度、华为、京东最新面试题汇集

必须要掌握的 InterruptedException 异常处理

Github 3.4k星,200余行代码,让你实时从视频中隐身

一次SQL查询优化原理分析(900W+数据,从17s到300ms)

关注订阅号「程序员小乐」,收看更多精彩内容

嘿,你在看吗

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值