在HashMap 中存放的一系列键值对,其中键为某个我们自定义的类型。放入 HashMap 后,我们在外部把某一个 key 的属性进行更改,然后我们再用这个 key 从 HashMap 里取出元素,这时候 HashMap 会返回什么?
我们办公室几个人答案都不一致,有的说返回null,有的说能正常返回value。但不论答案是什么都没有确凿的理由。我觉得这个问题挺有意思的,就写了代码测试。结果是返回null。需要说明的是我们自定义的类重写了 hashCode 方法。我想这个结果还是有点意外的,因为我们知道 HashMap 存放的是引用类型,我们在外面把 key 更新了,那也就是说 HashMap 里面的 key 也更新了,也就是这个 key 的 hashCode 返回值也会发生变化。这个时候 key 的 hashCode 和 HashMap 对于元素的 hashCode 肯定一样,equals也肯定返回true,因为本来就是同一个对象,那为什么不能返回正确的值呢?
测试案例
这里有 2 个案例,一个是 Person 类,还有一个是 Student 类,我们来验证下以上的观点(附带结论):
- 修改了对象属性是否会改变它的 hashcode => 是的
- 在 HashMap 里存取的时候是否会受到修改属性影响取值 => 取值为 null
packagetech.luxsun.interview.luxinterviewstarter.collection; importlombok.AllArgsConstructor; importlombok.Data; importlombok.NoArgsConstructor; importjava.util.HashMap; /*** @author Lux Sun* @date 2021/4/22*/publicclassMapDemo0 { publicstaticvoidmain(String[] args) { HashMap<Object, Object>map=newHashMap<>(); // Person CasePersonp=newPerson("Bob", 12); map.put(p, "person"); System.out.println(p.hashCode()); System.out.println(map.get(p)); p.setAge(13); System.out.println(p.hashCode()); System.out.println(map.get(p)); // Student CaseStudentstu=newStudent("Bob", 12); map.put(stu, "student"); System.out.println(stu.hashCode()); System.out.println(map.get(stu)); stu.setAge(13); System.out.println(stu.hashCode()); System.out.println(map.get(stu)); } } classPerson { privateStringname; privateIntegerage; publicinthashCode() { return123456; } } classStudent { privateStringname; privateIntegerage; }
输出结果
123456person123456person71154student71213null
源码
- hashCode 源码
publicinthashCode() { intPRIME=true; intresult=1; Object$age=this.getAge(); intresult=result*59+ ($age==null?43 : $age.hashCode()); Object$name=this.getName(); result=result*59+ ($name==null?43 : $name.hashCode()); returnresult; }
- map.get 源码
/*** Returns the value to which the specified key is mapped,* or {@code null} if this map contains no mapping for the key.** <p>More formally, if this map contains a mapping from a key* {@code k} to a value {@code v} such that {@code (key==null ? k==null :* key.equals(k))}, then this method returns {@code v}; otherwise* it returns {@code null}. (There can be at most one such mapping.)** <p>A return value of {@code null} does not <i>necessarily</i>* indicate that the map contains no mapping for the key; it's also* possible that the map explicitly maps the key to {@code null}.* The {@link #containsKey containsKey} operation may be used to* distinguish these two cases.** @see #put(Object, Object)*/publicVget(Objectkey) { Node<K,V>e; return (e=getNode(hash(key), key)) ==null?null : e.value; } /*** Computes key.hashCode() and spreads (XORs) higher bits of hash* to lower. Because the table uses power-of-two masking, sets of* hashes that vary only in bits above the current mask will* always collide. (Among known examples are sets of Float keys* holding consecutive whole numbers in small tables.) So we* apply a transform that spreads the impact of higher bits* downward. There is a tradeoff between speed, utility, and* quality of bit-spreading. Because many common sets of hashes* are already reasonably distributed (so don't benefit from* spreading), and because we use trees to handle large sets of* collisions in bins, we just XOR some shifted bits in the* cheapest possible way to reduce systematic lossage, as well as* to incorporate impact of the highest bits that would otherwise* never be used in index calculations because of table bounds.*/staticfinalinthash(Objectkey) { inth; return (key==null) ?0 : (h=key.hashCode()) ^ (h>>>16); } /*** Implements Map.get and related methods** @param hash hash for key* @param key the key* @return the node, or null if none*/finalNode<K,V>getNode(inthash, Objectkey) { Node<K,V>[] tab; Node<K,V>first, e; intn; Kk; if ((tab=table) !=null&& (n=tab.length) >0&& (first=tab[(n-1) &hash]) !=null) { if (first.hash==hash&&// always check first node ((k=first.key) ==key|| (key!=null&&key.equals(k)))) returnfirst; if ((e=first.next) !=null) { if (firstinstanceofTreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash==hash&& ((k=e.key) ==key|| (key!=null&&key.equals(k)))) returne; } while ((e=e.next) !=null); } } returnnull; }
总结
可以看到先取得了一个table,这个table实际上是个数组。然后在table里面找对应 key 的value。找的标准就是hash等于传入参数的hash, 并且满足另外两个条件之一:k = e.key,也就是说他们是同一个对象,或者传入的 key 的equal目标的 key 。我们的问题出在那个hash(key.hashCode()),可以看到 HashMap 在存储元素时是把 key 的 hashCode 再做了一次hash。得到的hash将最终作为元素存储位置的依据。对应到我们的情况:第一次存储时,hash函数采用key.hashCode作为参数得到了一个值,然后根据这个值把元素存到了某个位置。
当我们再去取元素的时候,key.hashCode的值已经出现了变化,所以这里的hash函数结果也发生了变化,所以当它尝试去获得这个 key 的存储位置时就不能得到正确的值,导致最终找不到目标元素。要想能正确返回,很简单,把Person类的 hashCode 方法改一下,让它的 hashCode 不依赖我们要修改的属性,但实际开发中肯定不能这么干,我们总是希望当两个对象的属性不完全相同时能返回不同的 hashCode 值。