Java - HashMap 的 key 更改后能否正确获取 value?

简介: Java - HashMap 的 key 更改后能否正确获取 value?


在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 类,我们来验证下以上的观点(附带结论):

  1. 修改了对象属性是否会改变它的 hashcode => 是的
  2. 在 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));
    }
}
@Data@AllArgsConstructor@NoArgsConstructorclassPerson {
privateStringname;
privateIntegerage;
publicinthashCode() {
return123456;
    }
}
@Data@AllArgsConstructor@NoArgsConstructorclassStudent {
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 值。


所以结论就是当把对象放到 HashMap 后,不要去修改 key 的属性,除非你重写了该实体类的 hashCode 方法不受属性限制。

目录
相关文章
|
10月前
|
存储 安全 Java
Java 集合框架中的老炮与新秀:HashTable 和 HashMap 谁更胜一筹?
嗨,大家好,我是技术伙伴小米。今天通过讲故事的方式,详细介绍 Java 中 HashMap 和 HashTable 的区别。从版本、线程安全、null 值支持、性能及迭代器行为等方面对比,帮助你轻松应对面试中的经典问题。HashMap 更高效灵活,适合单线程或需手动处理线程安全的场景;HashTable 较古老,线程安全但性能不佳。现代项目推荐使用 ConcurrentHashMap。关注我的公众号“软件求生”,获取更多技术干货!
168 3
|
11月前
|
Java
Java之HashMap详解
本文介绍了Java中HashMap的源码实现(基于JDK 1.8)。HashMap是基于哈希表的Map接口实现,允许空值和空键,不同步且线程不安全。文章详细解析了HashMap的数据结构、主要方法(如初始化、put、get、resize等)的实现,以及树化和反树化的机制。此外,还对比了JDK 7和JDK 8中HashMap的主要差异,并提供了使用HashMap时的一些注意事项。
387 2
Java之HashMap详解
|
4月前
|
存储 安全 Java
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
220 3
|
8月前
|
存储 缓存 安全
Java HashMap详解及实现原理
Java HashMap是Java集合框架中常用的Map接口实现,基于哈希表结构,允许null键和值,提供高效的存取操作。它通过哈希函数将键映射到数组索引,并使用链表或红黑树解决哈希冲突。HashMap非线程安全,多线程环境下需注意并发问题,常用解决方案包括ConcurrentHashMap和Collections.synchronizedMap()。此外,合理设置初始化容量和加载因子、重写hashCode()和equals()方法有助于提高性能和避免哈希冲突。
461 17
Java HashMap详解及实现原理
|
11月前
|
JSON Java 关系型数据库
Java更新数据库报错:Data truncation: Cannot create a JSON value from a string with CHARACTER SET 'binary'.
在Java中,使用mybatis-plus更新实体类对象到mysql,其中一个字段对应数据库中json数据类型,更新时报错:Data truncation: Cannot create a JSON value from a string with CHARACTER SET 'binary'.
1170 4
Java更新数据库报错:Data truncation: Cannot create a JSON value from a string with CHARACTER SET 'binary'.
|
11月前
|
存储 Java API
Java交换map的key和value值
通过本文介绍的几种方法,可以在Java中实现Map键值对的交换。每种方法都有其优缺点,具体选择哪种方法应根据实际需求和场景决定。对于简单的键值对交换,可以使用简单遍历法或Java 8的Stream API;对于需要处理值不唯一的情况,可以使用集合存储或Guava的Multimap。希望本文对您理解和实现Java中的Map键值对交换有所帮助。
236 1
|
12月前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
609 5
|
12月前
|
存储 Java API
详细解析HashMap、TreeMap、LinkedHashMap等实现类,帮助您更好地理解和应用Java Map。
【10月更文挑战第19天】深入剖析Java Map:不仅是高效存储键值对的数据结构,更是展现设计艺术的典范。本文从基本概念、设计艺术和使用技巧三个方面,详细解析HashMap、TreeMap、LinkedHashMap等实现类,帮助您更好地理解和应用Java Map。
214 3
|
12月前
|
存储 缓存 安全
在Java的Map家族中,HashMap和TreeMap各具特色
【10月更文挑战第19天】在Java的Map家族中,HashMap和TreeMap各具特色。HashMap基于哈希表实现,提供O(1)时间复杂度的高效操作,适合性能要求高的场景;TreeMap基于红黑树,提供O(log n)时间复杂度的有序操作,适合需要排序和范围查询的场景。两者在不同需求下各有优势,选择时需根据具体应用场景权衡。
127 2
|
Java
让星星⭐月亮告诉你,HashMap中保证红黑树根节点一定是对应链表头节点moveRootToFront()方法源码解读
当红黑树的根节点不是其对应链表的头节点时,通过调整指针的方式将其移动至链表头部。具体步骤包括:从链表中移除根节点,更新根节点及其前后节点的指针,确保根节点成为新的头节点,并保持链表结构的完整性。此过程在Java的`HashMap$TreeNode.moveRootToFront()`方法中实现,确保了高效的数据访问与管理。
96 2