【问题标题】:Why do we need LinkedHashMap if keySet() maintains order for a HashMap?如果 keySet() 维护 HashMap 的顺序,为什么我们需要 LinkedHashMap?
【发布时间】:2016-10-11 04:36:07
【问题描述】:
public class HashMapKeySet {

public static void main(String[] args) {
    Map<HashCodeSame,Boolean> map=new HashMap();

    map.put(new HashCodeSame(10),true);
    map.put(new HashCodeSame(2),false);

    for(HashCodeSame i:map.keySet())
        System.out.println("Key: "+i+"\t Key Value: "+i.getA()+"\t Value: "+map.get(i)+"\t Hashcode: "+i
                .hashCode());

    System.out.println("\nEntry Set******");
    for(Map.Entry<HashCodeSame, Boolean> i:map.entrySet())
        System.out.println("Key: "+i.getKey().getA()+"\t Value: "+i.getValue()+"\t Hashcode: "+i.hashCode());

    System.out.println("\nValues******");
    for(Boolean i:map.values())
        System.out.println("Key: "+i+"\t Value: "+map.get(i)+"\t Hashcode: "+i.hashCode());

}

static class HashCodeSame{

    private int a;

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }

    HashCodeSame(int a){
        this.a=a;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        HashCodeSame that = (HashCodeSame) o;

        return a == that.a;

    }

    @Override
    public int hashCode() {
        return 1;
    }
}

}

如果您可以在上面的示例中看到,我已经明确地让 hashcode() 在所有情况下都返回 1,以检查当 hashmap 中 key.hashcode() 发生冲突时会发生什么。会发生什么,为这些 Map.Entry 对象维护一个链表,例如

1(key.hashcode()) 将链接到 将链接到

(据我所知,在真值之后输入假值)。

但是当我做keySet()时,先返回true,然后返回false,而不是先返回false。

所以,我在这里假设的是,因为 keySet() 是一个集合并且集合保持顺序,所以我们在迭代时得到真假。但是,话又说回来,我们为什么不说 hashmap 保持顺序,因为检索的唯一方法是按顺序。或者我们为什么要使用 LinkedHashMap?

 Key: DS.HashMapKeySet$HashCodeSame@1    Key Value: 10   Value: true     Hashcode: 1
Key: DS.HashMapKeySet$HashCodeSame@1     Key Value: 2    Value: false    Hashcode: 1

Entry Set******
Key: 10  Value: true     Hashcode: 1230
Key: 2   Value: false    Hashcode: 1236

Values******
Key: true    Value: null     Hashcode: 1231
Key: false   Value: null     Hashcode: 1237

现在,当我添加 chsnge 时,hashcode 方法会返回一个赞

@Override
    public int hashCode() {
        return a;
    }

我得到相反的顺序。再加上

    map.put(new HashCodeSame(10),true);
    map.put(new HashCodeSame(2),false);
    map.put(new HashCodeSame(7),false);
    map.put(new HashCodeSame(3),true);
    map.put(new HashCodeSame(9),true);

收到的输出是,

    Key: DS.HashMapKeySet$HashCodeSame@2     Key Value: 2    Value: false    Hashcode: 2
Key: DS.HashMapKeySet$HashCodeSame@3     Key Value: 3    Value: false    Hashcode: 3
Key: DS.HashMapKeySet$HashCodeSame@7     Key Value: 7    Value: false    Hashcode: 7
Key: DS.HashMapKeySet$HashCodeSame@9     Key Value: 9    Value: true     Hashcode: 9
Key: DS.HashMapKeySet$HashCodeSame@a     Key Value: 10   Value: true     Hashcode: 10

Entry Set******
Key: 2   Value: false    Hashcode: 1239
Key: 3   Value: false    Hashcode: 1238
Key: 7   Value: false    Hashcode: 1234
Key: 9   Value: true     Hashcode: 1222
Key: 10  Value: true     Hashcode: 1221

Values******
Key: false   Value: null     Hashcode: 1237
Key: false   Value: null     Hashcode: 1237
Key: false   Value: null     Hashcode: 1237
Key: true    Value: null     Hashcode: 1231
Key: true    Value: null     Hashcode: 1231

现在这又让我想知道,为什么顺序是有序的。?谁能详细解释一下 hashmap 的 keySet()、entrySet() 方法是如何工作的?

【问题讨论】:

  • 那是因为添加相同哈希码的项目最终都在同一个桶中,并且插入顺序被保留,如果您有分布式哈希码则不是这种情况。对所有对象使用相同的哈希码是个坏主意。
  • “如果 keySet() 维护 HashMap 的顺序,为什么我们需要 LinkedHashMap?”哈希映射键的顺序是未定义的;如果您看到它们以您期望的顺序出现,那是巧合,并不保证总是如此。
  • 你能让我理解keySet()的内部实现吗?正如在这个stackoverflow.com/questions/1882762/… 链接中给出的那样,keySet 总是与输入的顺序相同,尽管迭代它会比迭代linkedHashMap 更昂贵。
  • 构造一个迭代顺序 != 插入顺序的例子是微不足道的,例如ideone.com/SOe3Qh您无需了解内部实现。您只需要知道HashMap 的迭代顺序无法保证。
  • 如果您可以构造一个恰好保留了排序的示例,这并不重要:HashMap 没有排序保证,所以这仅仅是巧合:您不能依赖排序存在保存。

标签: java collections hashmap linkedhashmap keyset


【解决方案1】:

HashMap没有定义的迭代顺序,LinkedHashMap有指定的迭代顺序。

HashMap 的困难在于它很容易构建简单的示例,其中迭代顺序非常可预测且相当稳定,即使这不能保证。

例如,假设您这样做了:

    Map<String, Boolean> map = new HashMap<>();
    String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    for (int i = 0; i < str.length(); i++) {
        map.put(str.substring(i, i+1), true);
    }
    System.out.println(map.keySet());

结果是

[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z]

嘿!那些是有序的!嗯,原因是 String 的 hashCode() 函数非常糟糕,而且对于单字符串来说特别糟糕。这是字符串的hashCode() specification。从本质上讲,它是一个加法运算,但对于单个字符串,它只是 char 的 Unicode 值。所以上面的单个字符串的哈希码是 65, 66, ... 90。HashMap 的内部表总是 2 的幂,在这种情况下它有 64 个条目长。使用的表条目是键的 hashCode() 值右移 16 位并与自身异或,以表大小为模。 (See the code here。)因此,这些单字符串最终出现在HashMap 表中的顺序存储桶中,位于数组位置 1、2、... 26 中。

密钥迭代通过存储桶按顺序进行,因此密钥最终会按照它们放入的顺序出现。同样,这不能保证,它只是碰巧以这种方式工作,因为各种特性如上所述的实现部分。

现在考虑HashCodeSame,其中hashCode() 函数每次都返回1。将这些对象中的一些添加到HashMap 将导致它们最终都在同一个存储桶中,并且由于迭代按顺序遍历链表,它们将按顺序出现:

    Map<HashCodeSame, Boolean> map = new HashMap<>();
    for (int i = 0; i < 8; i++) {
        map.put(new HashCodeSame(i), true);
    }
    System.out.println(map.keySet());

(我添加了一个 toString() 方法,它可以做显而易见的事情。)结果是:

[HCS(0), HCS(1), HCS(2), HCS(3), HCS(4), HCS(5), HCS(6), HCS(7)]

同样,由于实现的巧合,密钥按顺序出现,但原因与上述不同。

但是等等!在 JDK 8 中,HashMap 会将桶从线性链表转换为平衡树,如果同一桶中出现太多条目。如果超过 8 个条目最终位于同一个存储桶中,则会发生这种情况。让我们试试吧:

    Map<HashCodeSame, Boolean> map = new HashMap<>();
    for (int i = 0; i < 20; i++) {
        map.put(new HashCodeSame(i), true);
    }
    System.out.println(map.keySet());

结果是:

[HCS(5), HCS(0), HCS(1), HCS(2), HCS(3), HCS(4), HCS(6),
HCS(18), HCS(7), HCS(11), HCS(16), HCS(17), HCS(15), HCS(13),
HCS(14), HCS(8), HCS(12), HCS(9), HCS(10), HCS(19)]

底线是HashMap 确实维护定义的迭代顺序。如果您想要一个特定的迭代顺序,您必须使用LinkedHashMap 或一个排序的映射,例如TreeMap。不幸的是,HashMap 有一个相当稳定和可预测的迭代顺序,事实上,它的可预测性足以让人们认为它的顺序是明确定义的,而实际上并非如此。


为了帮助解决这个问题,在 JDK 9 中,新的基于哈希的集合实现将随机化它们从运行到运行的迭代顺序。例如:

    Set<String> set = Set.of("A", "B", "C", "D", "E",
                             "F", "G", "H", "I", "J");
    System.out.println(set);

在JVM的不同调用中运行时会打印出以下内容:

[I, H, J, A, C, B, E, D, G, F]
[C, B, A, G, F, E, D, J, I, H]
[A, B, C, H, I, J, D, E, F, G]

(迭代顺序在 JVM 的单次运行中是稳定的。此外,现有集合(例如 HashMap不会将其迭代顺序随机化。)

【讨论】:

  • 我不同意String.hashCode 很糟糕,即使在缩小单个字符Strings 的视图时也是如此。所有单个字符 Strings 都有一个不同的哈希码,所以不清楚你对它有什么期望。执行该值的任意转换,希望特定的哈希映射实现受益?由于它是特定哈希映射实现的设计决策,使用 2 的幂作为大小,执行适当的转换也是哈希映射实现的任务,特别是考虑到它已经改变的频率……
  • @Holger String.hashCode 提供了不同的哈希码,所以在这方面没问题,但它不能很好地在 32 位哈希码空间中分配值。这就是它糟糕的地方。良好的分布对于诸如封闭哈希之类的事情或如果您想拆分表以进行并行处理很重要。 HashMap 尝试过以各种方式进行位混合,但对于短字符串来说是无效的,因为它们的哈希码有很多零。
  • @Stuart Marks:我不确定规范是否要求在 32 位哈希码空间中进行良好的分布(除了提供不同的值)。考虑到Strings 几乎无限的值空间,我并不惊讶短字符串的完美分布不是优先事项。作为旁注,Java 7 中尝试的替代散列(murmur32)在我测试的所有实际案例中产生了 更多 冲突......
  • 再次阅读您的解释后,我产生了疑问。值65 to 90 与它们的高位(全为零)异或将再次产生值65 to 90,应用模64(AND 63),产生值1 to 26。所以它们碰巧以与它们的自然顺序相同的顺序最终出现在连续的桶中,这仍然是散列算法的产物,没有保证顺序,但从性能的角度来看是完美的。即使没有异或,它们也会在同一个桶中。那么这些Strings 的String.hashCode 有什么问题呢?
  • @Holger 在HashMap 冲突方面,String.hashCode 很好。在其他标准上,它不是那么好。如果所有值都聚集在一起,如果 HashMap 用作并行流的源,这将导致不平衡拆分。或者,如果使用封闭式散列方案(例如 JDK 9 的不可变集和映射),则结块会导致线性探测性能不佳。
【解决方案2】:

LinkedHashMap 的 Java 文档中回答您的问题

Map 接口的哈希表和链表实现,具有可预测的迭代顺序。此实现与 HashMap 的不同之处在于它维护一个双向链表,该列表贯穿其所有条目。这个链表定义了迭代顺序,通常是键插入映射的顺序(插入顺序)。请注意,如果将键重新插入到地图中,则插入顺序不会受到影响。 (如果在 m.containsKey(k) 将在调用之前立即返回 true 时调用 m.put(k, v),则将键 k 重新插入到映射 m 中。)

此实现使其客户免于 HashMap(和 Hashtable)提供的未指定的、通常混乱的排序,而不会增加与 TreeMap 相关的成本。它可用于生成与原始地图具有相同顺序的地图副本,无论原始地图的实现如何:

 void foo(Map m) {
     Map copy = new LinkedHashMap(m);
     ...
 }

【讨论】:

  • 我的问题是,在使用 JDK 1.8 时,无论我删除或添加元素多少次,我都可以看到保留的顺序。这是怎么回事?
猜你喜欢
  • 1970-01-01
  • 2012-02-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多