当前位置: 首页 > 知识库问答 >
问题:

如果keySet()维护HashMap的顺序,为什么我们需要LinkedHashMap?

黄骏喆
2023-03-14
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维护一个链表。条目对象,例如

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

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

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

所以,我在这里假设,因为keySet()是一个集合,集合保持顺序,所以在迭代时我们得到true和false。但是,再说一遍,为什么我们不说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

现在,当我添加chnsge时,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()方法是如何工作的吗?

共有2个答案

蒯翰墨
2023-03-14

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

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

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

 void foo(Map m) {
     Map copy = new LinkedHashMap(m);
     ...
 }
萧晔
2023-03-14

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()函数非常糟糕,对于单字符字符串来说尤其糟糕。下面是String的hashCode()规范。本质上,它是一个求和和乘法,但对于单个字符串,它只是char的Unicode值。所以上面的单个字符串的哈希码是65,66。。。90.HashMap的内部表总是2的幂,在本例中是64个条目。使用的表项是键的hashCode()值,右移16位,并与自身异或,以表大小为模。(请参见此处的代码。)因此,这些单个字符串在HashMap表中的顺序存储桶中结束,位于数组位置1、2、。。。26

密钥迭代在存储桶中按顺序进行,因此密钥最终以相同的顺序出现。同样,这并不能保证,因为上面描述的各种实现的特点,它恰好以这种方式工作。

现在考虑HashCode同类,其中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将在同一个bucket中出现过多条目时,将一个bucket从线性链表转换为平衡树。如果同一个存储桶中有超过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等现有集合的迭代顺序没有随机化。)

 类似资料:
  • 问题内容: 如果我们使用ExecutorCompletionService,则可以将一系列任务作为s 提交,并将结果作为进行交互。 但也有在的,它接受一个任务,我们得到的名单,以检索结果。 据我所知,使用一个或多个都不会有任何好处(除了我们避免使用循环,否则我们将不得不对任务进行操作),并且基本上它们是相同的想法,只是稍有不同。 那么,为什么有两种不同的方式提交一系列任务呢?我在性能上正确吗?有没

  • 问题内容: Angular应用使用属性而不是事件。 为什么是这样? 问题答案: ng-click包含一个角度表达式。Angular表达式是在Angular 范围的上下文中求值的,该范围绑定到具有ng- click属性的元素或该元素的祖先。 Angular表达式语言不包含流控制语句,也不能声明变量或定义函数。这些限制意味着模板只能访问由控制器或指令提供的变量和运行功能。

  • 以我的拙见,关于“什么是单子”这个著名问题的答案,尤其是投票最多的答案,试图解释什么是单子,而没有明确解释为什么单子是真正必要的。它们能被解释为一个问题的解决方案吗?

  • 为什么我们需要字典? 计算机最适合使用数字,而人类最适合使用姓名。我们创建了DNS以便记住主机名而不是IP地址。字典以相同的方式使用,因此我们可以记住AVP名称而不是类型编号。当FreeRADIUS解析请求或生成响应时,会查阅字典。 但是,字典与DNS不同,因为RADIUS客户端不知道FreeRADIUS使用的这些“友好”名称。永远不会在RADIUS客户端和RADIUS服务器之间交换AVP名称。

  • 问题内容: 我想知道一个仅在Eclipse上使用Maven或Ant的具体示例。 当我在Eclipse中进行开发时,Eclipse会为我做所有事情,而我只需要单击run按钮。而且,Eclipse可以让您将代码导出到Windows的可运行jar或.exe中。 所以我真的不知道为什么我需要Maven或Ant。 而且,如果确实需要, 我应该选择Maven还是Ant? 问题答案: 因为您的同事可能更喜欢Ne

  • 问题内容: 我开始使用RxJS,但我不明白为什么在此示例中我们需要使用类似or 的函数;数组的数组在哪里? 如果有人可以直观地解释正在发生的事情,那将非常有帮助。 问题答案: 当您有一个Observable的结果是更多Observable时,可以使用flatMap。 如果您有一个由另一个可观察对象产生的可观察对象,则您不能直接过滤,缩小或映射它,因为您有一个可观察对象而不是数据。如果您生成一个可观