HashMap和HashTable浅谈

HashMap与HashTable深入解析
本文详细对比了HashMap和HashTable的数据结构、操作机制及性能特点,包括它们的散列算法、同步机制、fast-fail机制等,揭示了HashMap为何在多数场景下表现更优。

两者都实现了Map接口,基本特性相同

-          对同一个Key,只会有一个对应的value值存在

-          如何算是同一个Key? 首先,两个key对象的hash值相同,其次,key对象的equals方法返回真

1、 HashMap的数据结构:

162213_hnAN_989269.png

当我们往hashmap中put元素的时候,先根据key的hash值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。 如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。

2、HashTable的数据结构:

162259_hD99_989269.png

其基本内部数据结构是一个Entry数组 (transient Entry[] table)

-          数组元素为实现Map.Entry<K,V>接口的类,Hashtable和HashMap各自实现了自己的Entry类。

-          Entry包含一个Key-value对,以及一个next指针指向另一个Entry。多个Entry可以组成一个单向链表。

Iterator iter = temp.entrySet().iterator();
(iter.hasNext()) {
    Map.Entry entry = (Map.Entry)iter.next();
    Object key = entry.getKey();
    Object value = entry.getValue();
    System..(key.toString() + + value.toString() + + key.hashCode());
}

(HashMap的循环也可这么写)

3、HashTable和HashMap的继承

public class Hashtable extends Dictionary implements Map
public class HashMap  extends AbstractMap implements Map

4、常用操作

数据插入操作: put(key,value)

-          根据Key的hash值计算出该Entry所应存放的位置(数组下标)

-          若该数组元素为空,直接放置Entry到此处

-          若多个不同的Key所计算得到的数组下标相同,新加入的Key-value对(Entry)会被加入到Entry单向链表中。Hashtable和HashMap都是将其插入链表首部.

-          若已经有相同的Key存在于这个链表中,则,新的value值会取代老的value

-          当Map中存放的Entry数量超过其限制( 数组长度 * 负荷因子)时,Map将自动重新调整数组大小并重新对Entry进行散列

 

数据查找:get(key)

-          根据Key的hash值计算出该Entry对所应存放的位置(数组下标)

-          得到该位置的第一个Entry对象,比较key和Entry.key,若hash值相同,并且equals为真,则该Entry是我们要找的Key-value对,否则继续沿next指针构成的单向链表查找

 

数据移除:remove(key)

-          按照上述数据查找的方式找到key所在的Entry对象,将其移除,并保持Entry单向链表的连通性


Hashtable

HashMap

并发操作

使用同步机制,

实际应用程序中,仅仅是Hashtable本身的同步并不能保证程序在并发操作下的正确性,需要高层次的并发保护。

下面的代码试图在key所对应的value值等于x的情况下修改value为x+1

{

 value = hashTable.get(key);

   if(value.intValue()== x){

hashTable.put(key,      new Integer(value.intValue()+1));

   }

}

如2个线程同时执行以上代码,可能放入不是x+1,而是x+2.

没有同步机制,需要使用者自己进行并发访问控制

Map Collections.synchronizedMap(Map m)或者ThreadLocal

数据遍历的方式

Iterator 和 Enumeration

Iterator

是否支持fast-fail

用Iterator遍历,支持fast-fail

用Enumeration不支持fast-fail.

支持fast-fail

是否接受值为null的Key 或Value?

不接受

接受

根据hash值计算数组下标的算法

当数组长度较小,并且Key的hash值低位数值分散不均匀时,不同的hash值计算得到相同下标值的几率较高

 

hash = key.hashCode();

index=(hash&0x7FFFFFFF) % tab.length;

优于hashtable,通过对Key的hash做移位运算和位的与运算,使其能更广泛地分散到数组的不同位置

 

hash = hash (k);

index = indexFor(hash, table.length);

 

static int hash(Object x) {

 int h = x.hashCode();

h += ~(h << 9);

 h ^= (h >>> 14);

  h += (h << 4);

 h ^= (h >>> 10);

 return h;

}

static int indexFor(int h, int length) {

return h & (length-1);

}

 

Entry数组的长度

Ø         缺省初始长度为11,

Ø         初始化时可以指定initial capacity

Ø         缺省初始长度为16,

Ø         长度始终保持2的n次方

Ø         初始化时可以指定initial capacity,若不是2的次方,HashMap将选取第一个大于initial capacity 的2n次方值作为其初始长度

LoadFactor负荷因子

0.75


负荷超过(loadFactor * 数组长度)时,内部数据的调整方式

扩展数组:2*原数组长度+1

扩展数组: 原数组长度 * 2

两者都会重新根据Key的hash值计算其在数组中的新位置,重新放置。算法相似,时间、空间效率相同



一般情况下,HashMap能够比Hashtable工作的更好、更快,主要得益于它的散列算法,以及没有同步。应用程序一般在更高的层面上实 现了保护机制,而不是依赖于这些底层数据结构的同步,因此,HashMap能够在大多应用中满足需要。推荐使用HashMap,如果需要同步,可以使用同 步工具类将其转换成支持同步的HashMap。



辨析

 “Hashtable和HashMap的区别主要是前者是同步的,后者是快速失败机制保证不会出现多线程并发错误(Fast-Fail)。”,这是一个被很多文章转载过的概念,但其描述并不准确,容易引起误会。实质上,Fast-fail与同步保护的是两种不同情况下的并发,两者不能拿来做比较。

Hashtable是同步的,在执行get,put,remove,size,clear等一次性读写操作时,使用了同步机制,避免了多个线程 同时读写Hashtable。但同步机制并不能避免在iterator或Enumeration遍历过程中其他线程对Hashtable的put、 remove、clear操作,这些写操作都会被毫无阻拦得成功执行。

快速失败机制主要目的在于使iterator遍历数组的线程能及时发现其他线程对Map的修改(如put、remove、clear等),因 此,fast-fail并不能保证所有情况下的多线程并发错误,只能保护iterator遍历过程中的iterator.next()与写并发.

其次,Hashtable的iterator遍历方式也是支持fast-fail的,不能说它没有快速失败机制。写一个简单的例程就可以证明这 一点,一个线程做iterator遍历,另一个线程向hashtable中put新的key和value,很容易就会观察到fast-fail 机制报告 ConcurrentModificationException


转载于:https://my.oschina.net/cherryzhang/blog/600801

### 底层结构差异 在底层结构上,HashMap Hashtable 存在一定的差异。从 JDK 1.8 开始,HashMap 的底层实现采用了数组 + 链表 + 红黑树的结构。这种结构优化了在哈希冲突较多时的性能,当链表长度超过一定阈值时,链表会转换为红黑树以提高查找效率[^2]。而 Hashtable 的底层实现则是传统的数组 + 链表结构,没有引入红黑树优化机制,因此在处理大量数据高冲突场景时性能相对较差[^2]。 ### 线程安全性 HashMap 是非线程安全的,这意味着在单线程环境下,它的性能更优由于没有同步开销。然而,在多线程环境下,如果多个线程同时修改 HashMap 的结构(例如添加或删除元素),可能会导致数据不一致或其他并发问题。因此,在多线程环境中使用 HashMap 需要额外的同步措施[^5]。相反,Hashtable 是线程安全的,它的所有公共方法都被 `synchronized` 关键字修饰,确保了多线程环境下的安全性,但这同时也带来了额外的性能开销[^5]。 ### 对 null 键 null 值的支持 HashMap 允许一个 null 键多个 null 值。这种灵活性使得 HashMap 在某些应用场景下更加方便。例如,可以轻松地将 null 作为特殊值进行处理[^3]。相比之下,Hashtable 不允许任何 null 键或 null 值。尝试插入 null 键或值会导致抛出 `NullPointerException` 异常[^3]。 ### 继承体系 HashMap 继承自 `AbstractMap` 类,并实现了 `Map` 接口。这样的设计使得 HashMap 能够更好地融入现代 Java 集合框架中,提供更简洁灵活的 API[^4]。而 Hashtable 继承自已废弃的 `Dictionary` 类,虽然它也实现了 `Map` 接口,但由于其继承体系的陈旧性,使得其 API 设计不如 HashMap 现代灵活[^4]。 ### 迭代器行为 HashMap 的迭代器是 fail-fast 的,这意味着在迭代过程中,如果检测到集合被修改(除了通过迭代器自身的 `remove` 方法),则会立即抛出 `ConcurrentModificationException` 异常。这种机制有助于快速发现并发修改问题[^4]。而 Hashtable 的迭代器不是 fail-fast 的,这可能导致在并发修改时出现不可预测的行为。 ### 扩容机制 当 HashMap 的容量不足以容纳更多元素时,它会按照当前容量的两倍进行扩容。这种策略有助于保持较低的负载因子,从而减少哈希冲突的概率。同样,Hashtable 也会在容量不足时进行扩容,但其扩容策略是当前容量的两倍加一。这种差异虽然看似微小,但在特定情况下可能会影响性能。 ### 推荐使用场景 由于 HashMap 在单线程环境下的高性能表现,它通常被推荐用于不需要线程安全的场景。此外,HashMap 的子类 `LinkedHashMap` 提供了可预测的迭代顺序(默认为插入顺序),这在某些需要有序遍历的应用中非常有用[^3]。而 Hashtable 由于其线程安全特性,更适合用于多线程环境或必须保证线程安全的场景。不过,对于需要高性能并发操作的场景,推荐使用 `ConcurrentHashMap`,因为它提供了更好的并发性能[^4]。 ### 示例代码 以下是一个简单的示例代码,展示了如何在 Java 中使用 HashMap Hashtable: ```java import java.util.HashMap; import java.util.Hashtable; import java.util.Map; public class Main { public static void main(String[] args) { // 使用 HashMap Map<String, Integer> hashMap = new HashMap<>(); hashMap.put("one", 1); hashMap.put("two", 2); System.out.println("HashMap: " + hashMap); // 使用 Hashtable Map<String, Integer> hashTable = new Hashtable<>(); hashTable.put("one", 1); hashTable.put("two", 2); System.out.println("Hashtable: " + hashTable); } } ``` 这段代码演示了如何创建使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值