Scala Map使用可变对象作为键是否可行及异常现象原理解析
问题复现代码
val list2=List(2,3) val list3=ListBuffer(4,5) // 初始状态下list3的哈希值 list3.hashCode ==> -770945474 scala> val map2=Map((list3,list2)) val map2: scala.collection.immutable.Map[scala.collection.mutable.ListBuffer[Int],List[Int]] = Map(ListBuffer(4, 5) -> List(2, 3)) // 执行修改操作 list3+=6 // 修改后list3变为 ListBuffer(4, 5, 6),哈希值变为341479237 scala> map2 val res74: scala.collection.immutable.Map[scala.collection.mutable.ListBuffer[Int],List[Int]] = Map(ListBuffer(4, 5, 6) -> List(2, 3))
为什么immutable的map2内容会跟随list3变化
首先要明确Scala中immutable集合的不可变范围:
- immutable Map的不可变指的是Map本身的结构不可变:你无法对这个Map执行增删键值对、修改已有键对应的值的操作,每次对immutable Map的修改操作都会返回一个新的Map实例。
- 但immutable Map存储的是键和值的对象引用,不会对存入的可变对象做深拷贝。你存入的键是
list3的引用,指向堆上的同一个ListBuffer实例,所以当你修改list3指向的实例内部内容时,Map里的键引用指向的对象自然也会同步变化,这和Map本身的不可变特性不冲突。
额外补充一个容易混淆的点:你修改list3的哈希值后,直接打印Map能看到更新后的内容,是因为打印操作会遍历Map所有存储的条目,不需要通过哈希值查找。如果你此时执行map2.get(list3),大概率会返回None:因为Map创建时是按list3初始的哈希值把条目存在对应的哈希桶里,修改后的哈希值对应了其他哈希桶,按新哈希值查找自然找不到原来存储的条目,这就是可变对象做键最典型的异常表现。
可变对象作为Scala Map的键是否合规
- 语法层面是完全允许的,Scala编译器不会报错。
- 但工程实践中强烈不推荐,属于典型的反模式:哈希类集合(包括HashMap、HashSet等)的底层实现依赖键的
hashCode和equals方法的稳定性,这是所有哈希集合的通用约定。如果键是可变对象,一旦修改后hashCode或equals逻辑发生变化,就会导致之前存入的条目无法被正常查找、哈希表结构混乱,严重时甚至会引发内存泄漏。
如果必须用类似集合的内容做键,可以先把可变集合转为不可变集合再存入,比如val map2 = Map(list3.toList -> list2),后续修改list3就不会影响Map的键了。
内容的提问来源于stack exchange,提问作者muthukrishnan
相关产品推荐
相关产品推荐

