LinkedHashMap的内部实现与HashMap实现有何不同?

40

我阅读到HashMap有以下实现方式:

main array 
   ↓
[Entry] → Entry → Entry      ← linked-list implementation
[Entry]
[Entry] → Entry
[Entry]
[null ]

所以,它有一个Entry对象的数组。

问题:

  1. 我想知道,在哈希值相同但对象不同的情况下,该数组的索引如何存储多个Entry对象。

  2. 这与LinkedHashMap实现有何区别?它是地图的双向链表实现,但是否保持类似上述的数组结构,并且如何存储指向下一个和上一个元素的指针?


只是猜测,但在每个键值对内部,值存储了指向其他条目的下一个和上一个指针,以维护插入顺序,这样可以保留顺序并仍然允许常数时间的删除、插入和检索。 - aaronman
好的。我也是这么想的。所以,数组中的每个“Entry”对象都有一个链接到下一个和上一个“Entry”对象? - Mercenary
我正在添加一个答案来更好地解释它。 - aaronman
5个回答

89

HashMap不维护插入顺序,因此它不维护任何双向链表。

LinkedHashMap最显著的特点是它维护键值对的插入顺序。LinkedHashMap使用双向链表来实现这一点。

LinkedHashMap的条目看起来像这样 -

  static class Entry<K, V> {
     K key;
     V value;
     Entry<K,V> next;
     Entry<K,V> before, after;        //For maintaining insertion order    
     public Entry(K key, V value, Entry<K,V> next){
         this.key = key;
         this.value = value;
         this.next = next;
     }
  }

通过使用before和after,我们可以跟踪LinkedHashMap中新添加的条目,这有助于我们维护插入顺序。

Before指的是前一条目,而after指的是LinkedHashMap中的下一个条目。

LinkedHashMap

有关详细说明和逐步解释,请参见http://www.javamadesoeasy.com/2015/02/linkedhashmap-custom-implementation.html

谢谢..!!


8
这张图表非常实用并且更加详细。 - xploreraj
嗨,我只想知道上图中next和after之间有什么区别。 - Gurinder
3
如果您仔细观察上面的图表,'next' 是指向同一桶链表中下一个条目的指针(更具体地说,是具有相同哈希码的条目),而'after' 则是指向按插入顺序的下一个条目的指针。 - Satya
2
图表和参考链接非常有帮助。 - Nisarg Patil
4
因此得证,一张图片胜过千言万语。谢谢分享。 - Shanu Gupta
谢谢,通过图片进行的解释很棒。 - NeeruKSingh

53
所以,它有一个Entry对象数组。

不完全是这样。 它有一个Entry对象链的数组。 一个HashMap.Entry对象有一个next字段,允许将Entry对象作为链表连接在一起。

我在想,如果hashCode相同但对象不同,那么该数组的索引如何存储多个Entry对象。

因为(正如您问题中显示的图片所示),Entry对象被链接在一起成为一个链。

这与LinkedHashMap实现有什么不同?它是地图的双向链表实现,但它是否像上面那样维护一个数组,并且如何存储指向下一个和上一个元素的指针?

LinkedHashMap 实现中,LinkedHashMap.Entry类扩展了HashMap.Entry 类,通过添加beforeafter 字段来实现。这些字段用于将LinkedHashMap.Entry对象组装成独立的双向链表,记录插入顺序。 因此,在LinkedHashMap类中,每个entry对象都在两个不同的链中:

  • 有许多经过单向链接的哈希链,可以通过主哈希数组访问。这用于(普通)哈希映射查找。

  • 有一个单独的双向链接列表,其中包含所有entry对象。它按照插入顺序保留,当您迭代哈希映射中的条目、键或值时使用。


感谢您的所有答案!阅读它们确实让我有点困惑。但是我觉得这个更清晰一些!因此,我接受这个作为答案! - Mercenary

14
请自行查看这里的源代码。 以后参考时,您只需谷歌搜索:

java LinkedHashMap source

HashMap使用一个LinkedList来处理碰撞,但是HashMapLinkedHashMap之间的差异在于LinkedHashMap具有可预测的迭代顺序,这是通过另一个双向链接列表实现的,该列表通常保持键的插入顺序。唯一的例外是当键被重新插入时,它会返回到列表中的原始位置。
需要注意的是,通过LinkedHashMap进行迭代比通过HashMap进行迭代更有效率,但是LinkedHashMap的内存效率较低。
如果从上面的解释还不清楚,请注意哈希过程是相同的,因此您可以获得正常哈希的好处,但是由于您正在使用双向链接列表来维护Entry对象的排序,因此您也可以获得如上所述的迭代优势,这是与哈希用于碰撞时使用的链表无关的。
编辑(针对评论): HashMap由数组支持,在其中一些槽中包含Entry对象的链以处理碰撞。 要遍历所有(key,value)对,您需要遍历数组中的所有槽,然后遍历LinkedLists。 因此,您的整体时间将与容量成比例。
当使用LinkedHashMap时,您只需遍历双向链接列表,因此总时间是与大小成比例的。

3
请问可以说明一下这个“downvote”是什么意思吗?我在这里说的任何话都是正确的。 - Steve P.
2
我也收到了,我打赌那个刚回答的家伙就是发件人。 - aaronman
3
我没有对这个回答进行负评。值得一提的是,我决定写自己的回答,因为我认为你的回答没有直接回答问题,并且没有清晰地解释实现细节。 - Stephen C
2
@aaronman,这不是关于声望,而是要提供可能的最佳答案。他的答案看起来很好,比我的更清晰。无论如何,它并没有以任何方式解释随意的负投票,但是再次强调,这不是他的错... - Steve P.
3
@aaronman- 嗯,你可以这样做。但是这不是“常规操作”。如果你“干预”他们的答案,人们可能会感到冒犯。(我很少这样做……只有当我看到一个被接受的答案非常错误时才这么做。) - Stephen C
显示剩余7条评论

1

由于其他答案都没有解释如何实现这样的功能,所以我来试试。

一种方法是在值(键值对中的值)中添加一些额外信息,对用户不可见,其中包含对插入哈希映射的前一个和后一个元素的引用。好处是您仍然可以在常量时间内删除元素,从哈希映射中删除是常量时间的,从链接列表中删除也是如此,因为您有对该条目的引用。您仍然可以在常量时间内插入,因为哈希映射插入是常量的,链接列表通常不是,但在这种情况下,您可以在链接列表中具有常量时间访问到某个位置,因此可以在常量时间内插入。最后,检索是常量时间,因为只需要处理结构的哈希映射部分即可。


请记住,这样的数据结构并非没有代价。由于所有额外的引用,哈希映射的大小将显著增加。每个主要方法都会稍微变慢(如果它们被重复调用可能会有影响)。虽然不确定是否是真正的术语,但数据结构的间接性增加了,尽管这可能不是很大的问题,因为引用保证指向哈希映射内部的内容。


由于这种结构的唯一优点是保持顺序,因此在使用时要小心。另外,在阅读答案时请记住,我不知道它是如何实现的,但如果给我这个任务,我会这样做。


Oracle文档 上有一句话确认了我的一些猜测。

这个实现与HashMap不同之处在于,它维护了一个双向链表来遍历所有的条目。

同一网站上另一段相关的引用。

这个类提供了所有可选的Map操作,并允许使用null元素。与HashMap一样,它为基本操作(添加、包含和删除)提供了常数时间性能,假设哈希函数正确地将元素分散到各个桶中。性能可能略低于HashMap,因为要维护链接列表的额外开销,但有一个例外:遍历LinkedHashMap的集合视图需要与映射大小成比例的时间,而不是其容量。遍历HashMap可能更昂贵,需要与其容量成比例的时间。


从LinkedList中删除元素不是在常数时间内完成的。只有当您拥有对要删除的元素的引用时,才可以在常数时间内完成,否则,您必须遍历列表以查找它。此外,通过LinkedHashMap进行迭代比通过HashMap进行迭代更有效,但LinkedHashMap的内存效率较低。 - Steve P.
@SteveP。我看到问题了,我只是没有把插入操作表述清楚。 - aaronman
是的,我也在想同样的问题!由于它扩展了HashMap,所以常数时间操作将保持不变,对吧?但是我没有理解其中说到的部分:LinkedHashMap需要与地图大小成比例的时间,而不管其容量如何 - Mercenary

-1

hashCode 会被哈希函数映射到任何一个桶中。如果在 hashCode 中发生冲突,那么 HashMap 将通过链接来解决这个冲突,即将值添加到链表中。以下是执行此操作的代码:

for (Entry<K,V> e = table[i]; e != null; e = e.next) {
392             Object k;
393             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
394         `enter code here`        V oldValue = e.value;
395                 e.value = value;
396                 e.recordAccess(this);
397                 return oldValue;
398             }
399         }

你可以清楚地看到它遍历链表,如果找到了键,则用新值替换旧值,否则将其追加到链表中。
但是,LinkedHashMap和HashMap之间的区别在于LinkedHashMap保持插入顺序。来自docs
这个链表定义了迭代顺序,通常是键被插入到映射中的顺序(插入顺序)。请注意,如果将键重新插入到映射中,插入顺序不会受到影响。(如果在调用m.put(k, v)之前m.containsKey(k)返回true,则键k被重新插入到映射m中)。

1
这不是他所问问题的答案。 - aaronman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接