【问题标题】:String as key in hashmap [duplicate]字符串作为哈希图中的键[重复]
【发布时间】:2013-08-04 11:55:21
【问题描述】:

在过去的一个小时里,我已经阅读了很多帖子,但我对使用不可变对象作为 Hashmap 中的键的概念仍然不是很清楚。我有一个哈希图,其键为字符串。哈希图中的值是 MyStore,其中 MyStore 表示有关我拥有的商店的信息。字符串代表地址。在我的代码中,我的逻辑是,我首先在映射中查找该键,如果存在 - > 获取它的值,如果它不存在则将其放入 hashmap。我的经理刚刚告诉我关键是将来会改变,那就是我的商店地址将来会改变。他说在那种情况下,我首先检查密钥是否存在的逻辑将不起作用。我不明白他在这里的意思。我想非常清楚地理解以下几点-

  1. hashmap 的可变键和不可变键之间的区别。
  2. 如果使用可以更改的不可变密钥会发生什么? - 我知道这没有意义,但我想清楚地了解我的经理在这里所说的内容。
  3. 有些帖子谈到字符串如果用作哈希映射中的键缓存其哈希码 - 这是什么意思?
  4. 如果假设我在实现 hashcode 和 equals 的 hashmap 中使用可变对象作为键,那么它会起作用吗?我假设它会因为如果密钥发生变化,contains 方法将查看密钥是否存在。如果它不存在,它将放入该条目,以便您以后可以获取它。

如果之前已经讨论过,我并不是要创建重复的帖子。如果我错过了阅读可以回答我所有问题的帖子,请指出我。如果没有,请用外行的方式解释我的上述问题,以便将来对其他读者有用:)。随意编辑我的帖子的主题,所以将来如果有人有类似的问题,他们会直接登陆这里:)

【问题讨论】:

标签: java


【解决方案1】:

首先:HashMap 是如何工作的?

基本上它有一个数组,当你在映射中放置一个键值对时,它存储在数组中的一个位置。数组中的位置是根据传递给散列方法的键 hashCode() 的结果来选择的。这是为什么?好吧,如果您请求某个键的值,则可以简单地重新计算查找该键的数组中的索引及其关联值,以再次查找该数组中的索引。 (需要更多的逻辑来处理映射到同一索引的键,但我只是想让您了解基本机制)然后使用equals() 来验证计算索引处的键是否确实是请求的密钥。

  1. 从这里应该更清楚为什么不可变键比可变键更好。不可变的键将始终保持相同的hashCode() 值,并且哈希函数将再次找到正确的桶(= hashMap 数组中的索引)。

    这并不意味着可变键不能工作。如果键上的突变不会影响哈希码,或者只要使用了 hashMap,键就不会发生突变,则可变键将起作用。

  2. 不可变密钥如何更改?好吧,键本身可能无法改变,但是键值映射可以改变业务逻辑。如果您创建地图,使用地址作为键,您依赖于商店地址不会更改的事实。如果商店的地址发生变化,您将无法在使用其新地址作为键的地图中找到它。你的经理有一个有效的观点。

  3. 在 Map 中找到键的速度很大程度上取决于计算 hashCode 的速度。对于字符串,此计算循环遍历字符串中的所有字符。如果您使用长字符串作为键并且拥有大量 Map 访问权限,这可能会导致性能瓶颈。因此,Java 的 String 实现会缓存哈希值,因此只会计算一次。但是,如果您再次使用相同的 String 实例(新实例将没有缓存值),您将只能避免计算哈希码。您可以intern() 使用您使用的密钥,但只有在可以证明确实存在性能瓶颈时才考虑这一点,因为String 实习确实有其自身的开销。

  4. 如 1 中所述:如果可变键的哈希码不受突变影响,则它们可以工作。例如使用 Customer 作为键,其中 hashCode() 仅基于客户的名称,然后一个不允许更改名称但允许更改其他值的 Customer 实现是一个可靠的键。

【讨论】:

  • 优秀。谢谢鲍莫尔。
  • String 的 hashcode 已经被缓存了...
  • @assylias 编辑了我的答案
  • 我想如果你改变一个可变键,那么 HashMap 应该计算它是新的hashCode() 并重新定位它。我不知道 HashMap 在 Java 中的实现是什么,但我认为检查密钥是否已更改以及是否更改是合理的 -> 计算新的hashCode() -> 重新定位元素并释放之前的位置。
  • @AntonBelev Map 实现(尤其是通用的)如何知道键的哈希码何时发生变化? Map 可以在每次获取之前检查所有包含的键,但这只会破坏 Map 的目的:快速获取与键关联的值。
【解决方案2】:
  1. 如果您修改用作键的可变对象,可能会出现问题。 map.containsKey(modifiedKey) 可能会返回 false,即使 key 存在,您也必须遍历键才能找到它。 所以尽量使用 immutable 或者当它是 key 时不要修改 mutable。

  2. 不可变对象永远不会改变。有些方法看起来像是在更改对象,但实际上是创建了一个新副本。示例:

    字符串 a = "A";

    字符串 b = a.substring(0); // 子字符串创建了一个“A”的副本,其中 a 根本没有被修改。

    a = a + b; // a+b 创建一个新的字符串“AA”,不修改之前的字符串。

  3. 这可能对caching-hashes-in-java-collections 有帮助,这也很棒why-are-immutable-objects-in-hashmaps-so-effective

  4. String 已经实现了equalshashcode,除非您绝对确定需要它,否则无需发明另一个类来代替它。

    如第 1 点所述。您可以这样做,但您必须小心,不要修改您的可变对象。不过,这不是一个很好的做法。

【讨论】:

  • 非常感谢您的链接 - stackoverflow.com/questions/10342859/…。它回答了我的所有问题。
  • 当然可以。但老实说,我对第 2 点还不是很清楚。另外,请修改您对我的第 4 点的回答。我指的是不可变对象。
【解决方案3】:
  1. 不可变键无法更改。因此,在插入时计算的哈希码不会改变。因此,当您尝试 get 地图中的元素时,要获取的对象的哈希码是根据已知的哈希码计算的。如果您的密钥从外部更改(它是可变的),新密钥的哈希码将与您插入的不同。

  2. 让我们看一个例子。对于(24

    public class RandomPair {
        int p;
        int q;
    
        public RandomPair(int p, int q) {
            this.p = p;
            this.q = q;
        }
        @Override
        public int hashCode() {
            return 31 * p + q;
        }
    
        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof RandomPair)) {
                return false;
            }
            if (obj == this) {
               return true;
            }
    
            RandomPair other = (RandomPair) obj;
            if (p != other.p)
                return false;
            if (q != other.q)
                return false;
            return true;
        }
    
        public static void main(String[] args) {
            RandomPair pair = new RandomPair(10, 10);
            Map<RandomPair, Integer> map = new HashMap<RandomPair, Integer>();
    
            map.put(pair, 1);
            System.out.println(map.get(pair)); //returns 1
    
            //someone somewhere just changed the value of pair
            pair.p = 20;
            //the object was the same, someone somewhere just changed value of pair and now you can't 
            //find it in the map
            System.out.println(map.get(pair));
    
            //had you made p and q final, this sort of modification wouldn't be possible
           //Strings are immutable and thus prevent this modification
        }
    }
    
  3. 由于字符串是不可变的,因此计算后的哈希码值可以再次重复使用。这 哈希码是惰性计算的。即第一次调用hashcode,然后hashcode的值被缓存。

【讨论】:

  • 感谢 bsd。您能否也添加几行关于我的经理正在谈论的内容?我们从客户那里接收我们的数据作为 JSON 字符串。所以他们肯定会改变地址。我想了解这会如​​何影响我的逻辑?
  • 不要将地址作为计算hashcodeequals 的一部分。常量字段可能是其唯一的 10 位标识号(可能是 SSN)应该是 hashcode 的一部分。名字可能会改变,地址可能会改变。如果您可以找出 Person 对象中的变化,您可以从地图中删除该项目并使用新的新值重新插入。
【解决方案4】:

一般来说,hashmap 中的键应该是不可变的。

this

注意:如果将可变对象用作映射,则必须非常小心 键。如果对象的值未指定映射的行为 以影响等于比较的方式更改,而 object 是地图中的一个键。

您的密钥的哈希在插入期间计算一次,哈希映射存储它,并且一旦您的密钥被修改,它将不会自动更新。这就是为什么假设密钥是不可变的。

您的选择是: 1. 不要使用可变对象作为键。尝试找到另一个密钥,或使用以前密钥对象的不可变部分 2. 不要在可变对象用作键时更改它们

【讨论】:

  • 您能回答我上面提出的每个问题吗?这真的很有帮助。
【解决方案5】:
  1. 可变键或对象意味着您可以修改对象 [通过修改我的意思是您可以更改对象表示的值]。如果用 equals 和 hashcode 编写的逻辑使用这些可修改的值,这将影响其在 HashMap 中的存储。

  2. 理想情况下,不可变性意味着对象一旦初始化就不能再更改。但是如果我们具体谈谈HashMap,那么在equals和hashcode方法中使用的所有变量,如果可以修改,那么该对象不应该用作键,否则它可以用作键[但仍然不推荐].

  3. 它不仅仅是关于String,任何关于都会缓存它的哈希码。几乎所有对象都会一次又一次地生成哈希码[我说几乎在某些情况下它可以改变是有原因的]。 Hashcode 缓存在 Object 头中。

  4. 如果你想使用可变对象作为键,那么你应该选择IdentityHashMap。只需阅读它们,它们在这种情况下会很有用。

【讨论】:

    猜你喜欢
    • 2018-05-26
    • 1970-01-01
    • 1970-01-01
    • 2012-10-03
    • 1970-01-01
    • 1970-01-01
    • 2012-07-05
    • 2021-04-25
    • 2015-03-16
    相关资源
    最近更新 更多