Java中HashMap和Hashtable之间的区别是什么?

对于非线程应用程序,哪个更有效?


当前回答

HashMap和Hashtable也有显著的算法差异。以前没有人提到过这一点,所以我才提出来。HashMap将构造一个大小为两倍的哈希表,动态地增加它,以便在任何bucket中最多有八个元素(冲突),并且对于一般的元素类型,可以很好地搅拌这些元素。然而,如果你知道你在做什么,Hashtable实现提供了对哈希的更好和更精细的控制,也就是说,你可以使用最接近你的值域大小的素数来固定表大小,这将导致比HashMap更好的性能,即在某些情况下更少的冲突。

除了在这个问题中广泛讨论的明显差异之外,我认为Hashtable是一款“手动驾驶”汽车,您可以更好地控制哈希,而HashMap是“自动驾驶”的对应车型,通常表现良好。

其他回答

面试中经常会问这个问题,以检查应聘者是否了解集合类的正确用法,并了解可用的替代解决方案。

HashMap类大致等同于Hashtable,只是它是非同步的并且允许为null。(HashMap允许空值作为键和值,而Hashtable不允许空值)。HashMap不能保证映射的顺序随时间保持不变。HashMap是非同步的,而Hashtable是同步的。HashMap中的迭代器是故障安全的,而Hashtable的枚举器不是,如果任何其他线程通过添加或删除迭代器自己的remove()方法以外的任何元素来从结构上修改映射,则引发ConcurrentModificationException。但这不是一种保证的行为,将由JVM尽最大努力完成。

关于一些重要条款的说明:

同步意味着只有一个线程可以在一个时间点修改哈希表。基本上,这意味着在对Hashtable执行更新之前,任何线程都必须获取对象的锁,而其他线程将等待释放锁。故障安全在迭代器的上下文中是相关的。如果在集合对象上创建了迭代器,并且其他线程试图“从结构上”修改集合对象,则将引发并发修改异常。但其他线程也可以调用set方法,因为它不会“从结构上”修改集合。但是,如果在调用set之前,集合已经在结构上进行了修改,则将抛出IllegalArgumentException。结构修改意味着删除或插入可以有效改变地图结构的元素。

HashMap可以通过

Map m=Collections.synchronizedMap(hashMap);

Map提供集合视图,而不是通过枚举对象直接支持迭代。集合视图极大地增强了界面的表现力,这将在本节稍后讨论。Map允许您遍历键、值或键值对;哈希表不提供第三个选项。Map提供了一种在迭代过程中删除条目的安全方法;哈希表没有。最后,Map修复了Hashtable接口中的一个小缺陷。Hashtable有一个名为contains的方法,如果Hashtable包含给定值,则返回true。给定它的名称,如果Hashtable包含给定的键,您会希望该方法返回true,因为该键是Hashtable的主要访问机制。Map接口通过重命名方法containsValue消除了这种混淆。此外,这提高了接口的一致性-containsValue与containsKey类似。

地图界面

除了已经提到的差异之外,应该注意的是,自从Java8以来,HashMap动态地用TreeNodes(红黑树)替换每个bucket中使用的Nodes(链表),因此即使存在高哈希冲突,搜索时最坏的情况也是

HashMap的O(log(n))与Hashtable中的O(n)。

*上述改进尚未应用于Hashtable,而仅应用于HashMap、LinkedHashMap和ConcurrentHashMap。

仅供参考,目前,

TREEIFY_THRESHOLD=8:如果存储桶包含8个以上的节点,则链接列表将转换为平衡树。UNTREEIFY_THRESHOLD=6:当存储桶太小(由于删除或调整大小)时,树将转换回链接列表。

HashMap和Hashtable也有显著的算法差异。以前没有人提到过这一点,所以我才提出来。HashMap将构造一个大小为两倍的哈希表,动态地增加它,以便在任何bucket中最多有八个元素(冲突),并且对于一般的元素类型,可以很好地搅拌这些元素。然而,如果你知道你在做什么,Hashtable实现提供了对哈希的更好和更精细的控制,也就是说,你可以使用最接近你的值域大小的素数来固定表大小,这将导致比HashMap更好的性能,即在某些情况下更少的冲突。

除了在这个问题中广泛讨论的明显差异之外,我认为Hashtable是一款“手动驾驶”汽车,您可以更好地控制哈希,而HashMap是“自动驾驶”的对应车型,通常表现良好。

HashTable是同步的,如果您在单个线程中使用它,则可以使用HashMap,这是一个未同步的版本。未同步的对象通常性能更高。顺便说一句,如果多个线程同时访问HashMap,并且至少有一个线程在结构上修改了该映射,那么它必须在外部同步。您可以使用以下方法将未同步的映射打包到已同步的映射中:Map m=Collections.synchronizedMap(新HashMap(…));HashTable只能包含非空对象作为键或值。HashMap可以包含一个空键和空值。Map返回的迭代器是快速失败的,如果在创建迭代器之后的任何时候对映射进行了结构修改,则除了通过迭代器自己的remove方法之外,迭代器将抛出ConcurrentModificationException。因此,在并发修改的情况下,迭代器会快速而干净地失败,而不是在未来某个不确定的时间冒着任意、非确定性行为的风险。而Hashtable的键和元素方法返回的枚举不会快速失败。HashTable和HashMap是Java集合框架的成员(自从Java 2平台v1.2以来,HashTable被改进以实现Map接口)。HashTable被认为是遗留代码,如果需要线程安全的高度并发实现,文档建议使用ConcurrentHashMap代替HashTable。HashMap不保证返回元素的顺序。对于HashTable,我想它是相同的,但我不完全确定,我找不到明确说明这一点的ressource。

集合(有时称为容器)只是将多个元素分组为单个单元的对象。集合用于存储、检索、操作和传递聚合数据。集合框架W是用于表示和操作集合的统一架构。

HashMap JDK1.2和Hashtable JDK1.0都用于表示一组对象,这些对象在<Key,Value>对中表示。每个<Key,Value>对都称为Entry对象。条目集合由HashMap和Hashtable的对象引用。集合中的键必须是唯一的或与众不同的。[因为它们用于检索特定键的映射值。集合中的值可以重复。]


«超级类、传统和收藏框架成员

Hashtable是JDK1.0中引入的一个遗留类,它是Dictionary类的一个子类。从JDK1.2哈希表被重新设计以实现Map接口,从而成为集合框架的成员。HashMap从JDK1.2引入之初就是Java集合框架的成员。HashMap是AbstractMap类的子类。

public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, Serializable { ... }

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { ... }

«初始容量和负载系数

容量是哈希表中桶的数量,初始容量只是创建哈希表时的容量。注意,哈希表是开放的:在“哈希冲突”的情况下,一个存储桶存储多个条目,这些条目必须按顺序搜索。负载因子是在哈希表的容量自动增加之前,允许哈希表达到多满的度量。

HashMap使用默认初始容量(16)和默认负载因子(0.75)构造一个空哈希表。Where as Hashtable使用默认初始能力(11)和负载因子/填充比(0.75)构建空哈希表。

«哈希冲突时的结构修改

HashMap,Hashtable在哈希冲突的情况下,它们将映射条目存储在链接列表中。从Java8 for HashMap中,如果哈希桶增长超过某个阈值,该桶将从条目链接列表切换到平衡树。这将最坏情况性能从O(n)提高到O(logn)。在将列表转换为二进制树时,哈希代码用作分支变量。如果同一个存储桶中有两个不同的哈希码,一个被认为更大,并位于树的右侧,另一个位于左侧。但是,当两个哈希码相等时,HashMap假设键是可比较的,并比较键以确定方向,以便保持某种顺序。使HashMap的键具有可比性是一个很好的做法。在添加条目时,如果存储桶大小达到TREEIFY_THRESHOLD=8,则将条目的链接列表转换为平衡树;在删除小于TREEIFY_THRESHOLD且最多为UNTREEIFY_THRESHOLD=6的条目时,将重新将平衡树转换为条目的链接表。Java 8 SRC,堆栈

«集合视图迭代、快速故障和安全故障

    +--------------------+-----------+-------------+
    |                    | Iterator  | Enumeration |
    +--------------------+-----------+-------------+
    | Hashtable          | fail-fast |    safe     |
    +--------------------+-----------+-------------+
    | HashMap            | fail-fast | fail-fast   |
    +--------------------+-----------+-------------+
    | ConcurrentHashMap  |   safe    |   safe      |
    +--------------------+-----------+-------------+

迭代器本质上是一种快速故障。即,如果在迭代而不是自己的remove()方法时修改了集合,则会抛出ConcurrentModificationException。其中作为枚举本质上是故障安全的。如果在迭代时修改了集合,则不会引发任何异常。

根据JavaAPI文档,迭代器总是优先于枚举。

注意:枚举接口的功能与迭代器接口重复。此外,Iterator添加了一个可选的移除操作,并且方法名称更短。新的实现应该考虑使用迭代器而不是枚举。

在Java5中引入了ConcurrentMap接口:ConcurrentHashMap——一个由哈希表支持的高度并发、高性能的ConcurrentMap实现。此实现在执行检索时从不阻塞,并允许客户端选择更新的并发级别。它旨在作为Hashtable的替代品:除了实现ConcurrentMap之外,它还支持Hashtable特有的所有“遗留”方法。

每个HashMapEntrys值都是可变的,从而确保争用修改和后续读取的细粒度一致性;每次读取都反映最近完成的更新迭代器和枚举是故障安全的-反映自创建迭代器/枚举以来的某个点的状态;这允许以降低一致性为代价同时进行读取和修改。它们不会引发ConcurrentModificationException。然而,迭代器被设计为一次只能由一个线程使用。与Hashtable类似,但与HashMap不同,此类不允许将null用作键或值。

public static void main(String[] args) {

    //HashMap<String, Integer> hash = new HashMap<String, Integer>();
    Hashtable<String, Integer> hash = new Hashtable<String, Integer>();
    //ConcurrentHashMap<String, Integer> hash = new ConcurrentHashMap<>();
    
    new Thread() {
        @Override public void run() {
            try {
                for (int i = 10; i < 20; i++) {
                    sleepThread(1);
                    System.out.println("T1 :- Key"+i);
                    hash.put("Key"+i, i);
                }
                System.out.println( System.identityHashCode( hash ) );
            } catch ( Exception e ) {
                e.printStackTrace();
            }
        }
    }.start();
    new Thread() {
        @Override public void run() {
            try {
                sleepThread(5);
                // ConcurrentHashMap  traverse using Iterator, Enumeration is Fail-Safe.
                
                // Hashtable traverse using Enumeration is Fail-Safe, Iterator is Fail-Fast.
                for (Enumeration<String> e = hash.keys(); e.hasMoreElements(); ) {
                    sleepThread(1);
                    System.out.println("T2 : "+ e.nextElement());
                }
                
                // HashMap traverse using Iterator, Enumeration is Fail-Fast.
                /*
                for (Iterator< Entry<String, Integer> > it = hash.entrySet().iterator(); it.hasNext(); ) {
                    sleepThread(1);
                    System.out.println("T2 : "+ it.next());
                    // ConcurrentModificationException at java.util.Hashtable$Enumerator.next
                }
                */
                
                /*
                Set< Entry<String, Integer> > entrySet = hash.entrySet();
                Iterator< Entry<String, Integer> > it = entrySet.iterator();
                Enumeration<Entry<String, Integer>> entryEnumeration = Collections.enumeration( entrySet );
                while( entryEnumeration.hasMoreElements() ) {
                    sleepThread(1);
                    Entry<String, Integer> nextElement = entryEnumeration.nextElement();
                    System.out.println("T2 : "+ nextElement.getKey() +" : "+ nextElement.getValue() );
                    //java.util.ConcurrentModificationException at java.util.HashMap$HashIterator.nextNode
                    //                                          at java.util.HashMap$EntryIterator.next
                    //                                          at java.util.Collections$3.nextElement
                }
                */
            } catch ( Exception e ) {
                e.printStackTrace();
            }
        }
    }.start();
    
    Map<String, String> unmodifiableMap = Collections.unmodifiableMap( map );
    try {
        unmodifiableMap.put("key4", "unmodifiableMap");
    } catch (java.lang.UnsupportedOperationException e) {
        System.err.println("UnsupportedOperationException : "+ e.getMessage() );
    }
}
static void sleepThread( int sec ) {
    try {
        Thread.sleep( 1000 * sec );
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

«空键和空值

HashMap最多允许一个空键和任意数量的空值。如果as Hashtable甚至不允许单个null键和null值,那么如果键或值为null,则会引发NullPointerException。实例

«同步,线程安全

哈希表是内部同步的。因此,在多线程应用程序中使用Hashtable非常安全。其中as HashMap未在内部同步。因此,在没有外部同步的多线程应用程序中使用HashMap是不安全的。可以使用Collections.synchronizedMap()方法从外部同步HashMap。

«性能

由于Hashtable是内部同步的,这使得Hashtable比HashMap稍慢。


@See

红黑树是一种自平衡的二叉搜索树Java8中HashMap的性能改进