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

时间:2016-06-10 09:12:26

标签: java collections hashmap linkedhashmap keyset

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())将链接到&lt; 2,false&gt;将链接到&lt; 10,true&gt;

(正如我所理解的那样,在真值之后输入假值)。

但是当我执行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

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

2 个答案:

答案 0 :(得分:6)

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() 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 ,其迭代顺序是随机的。)

答案 1 :(得分:0)

使用{doc LinkedHashMap

的Java doc回答您的问题

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

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

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