如果要将自定义类的实例 作为HashMap的 键,必须重写hashCode和equals方法
简单版本,看不懂看后面复杂版本解释
复杂版本解释
当我们用 HashMap存入自定义的类时,如果不重写这个自定义类的equals和hashCode方法,得到的结果会和我们预期的不一样。我们来看 WithoutHashCode.java这个例子。
在其中的第 2到第 18行,我们定义了一个 Key类;在其中的第 3行定义了唯一的一个属性 id。当前我们先注释掉第 9行的 equals方法和第 16行的 hashCode方法。
在 main函数里的第 22和 23行,我们定义了两个 Key对象,它们的 id都是 1,就好比它们是两把相同的都能打开同一扇门的钥匙。
在第 24行里,我们通过泛型创建了一个HashMap对象。它的键部分可以存放 Key类型的对象,值部分可以存储String类型的对象。
在第 25行里,我们通过 put方法把 k1和一串字符放入到 hm里;而在第 26行,我们想用 k2去从HashMap里得到值;这就好比我们想用 k1这把钥匙来锁门,用 k2来开门。这是符合逻辑的,但从当前结果看, 26行的返回结果不是我们想象中的那个字符串,而是 null。
原因有两个:一是没有重写hashCode方法,二是没有重写equals方法。
当我们往HashMap里放 k1时,首先会调用 Key这个类的 hashCode方法计算它的 hash值,随后把 k1放入hash值所指引的内存位置。
关键是我们没有在 Key里定义 hashCode方法。这里调用的仍是 Object类的 hashCode方法(所有的类都是Object的子类),而 Object类的 hashCode方法返回的 hash值其实是 k1对象的 内存地址(假设是1000)。
如果我们随后是调用 hm.get(k1),那么我们会再次调用 hashCode方法(还是返回 k1的地址 1000),随后根据得到的 hash值,能很快地找到 k1。
但我们这里的代码是 hm.get(k2),当我们调用 Object类的 hashCode方法(因为 Key里没定义)计算 k2的 hash值时,其实得到的是 k2的内存地址(假设是 2000)。由于 k1和 k2是两个不同的对象,所以它们的内存地址一定不会相同,也就是说它们的 hash值一定不同,这就是我们无法用 k2的 hash值去拿 k1的原因。
当我们把第 16和 17行的 hashCode方法的注释去掉后,会发现它是返回 id属性的 hashCode值,这里 k1和 k2的 id都是1,所以它们的 hash值是相等的。
我们再来更正一下存 k1和取 k2的动作。存 k1时,是根据它 id的 hash值,假设这里是 100,把 k1对象放入到对应的位置。而取 k2时,是先计算它的 hash值(由于 k2的 id也是 1,这个值也是 100),随后到这个位置去找。
但结果会出乎我们意料:明明 100号位置已经有 k1,但第 26行的输出结果依然是 null。其原因就是没有重写 Key对象的 equals方法。
下面可能有点绕
HashMap是用链地址法来处理冲突,也就是说,在 100号位置上,有可能存在着多个用链表形式存储的对象。它们通过 hashCode方法返回的 hash值都是100。
当我们通过 k2的 hashCode到 100号位置查找时,确实会得到 k1。但 k1有可能仅仅是和 k2具有相同的 hash值,但未必和 k2内容相等( k1和 k2两把钥匙未必能开同一扇门),这个时候,就需要调用 Key对象的 equals方法来判断两者是否相等了。
由于我们在 Key对象里没有定义 equals方法,系统就不得不调用 Object类的 equals方法。由于 Object的固有方法是根据两个对象的内存地址来判断,所以 k1和 k2一定不会相等,这就是为什么依然在 26行通过 hm.get(k2)依然得到 null的原因。
为了解决这个问题,我们需要打开第 9到 14行 equals方法的注释。在这个equals方法里,只要两个对象都是 Key类型,且它们的 id相等,它们内容就相等。