HashMap相关类:Hashtable、LinkHashMap、TreeMap
前言
很高兴遇见你~
在 深入剖析HashMap 文章中我从散列表的角度解析了HashMap,在 深入解析ConcurrentHashMap:感受并发编程智慧 解析了ConcurrentHashMap的底层实现原理。本文是HashMap系列文章的第三篇,主要内容是讲解与HashMap相关的集合类。
HashMap本身功能已经相对完善,但在某些特殊的情景下,他就显得无能为力,如高并发、需要记住key插入顺序、给key排序等。实现这些功能往往需要付出一定的代价,在没有必然的需求情景下,增添这些功能是没必要的。因而,为了提高性能,Java并没有把这些特性直接集成到HashMap中,拓展了拥有这些特性的其他集合类作为补充:
- 线程安全的ConcurrentHashMap、Hashtable、SynchronizeMap
 - 记住插入顺序的LinkedHashMap
 - 记录key顺序的TreeMap
 
这样,我们就可以在特定的需求情景下,选择最适合我们的集合框架,从而来提高性能。那么今天这篇文章,主要就是分析这些其他的集合类的特性、付出的性能代价、与HashMap的区别。
那么,我们开始吧~
Hashtable
Hashtable是属于JDK1.1的第一批集合框架其中之一,其他的还有Vector、Stack等。这些集合框架由于设计上的缺陷,导致了性能的瓶颈,在jdk1.2之后就被新的一套集合框架取代,也就是HashMap、ArrayList这些。HashMap在jdk1.8之后进行了全面的优化,而Hashtable依旧保持着旧版本的设计,在很多方面都落后于HashMap。下面主要分析Hashtable在:接口继承、哈希函数、哈希冲突、扩容方案、线程安全等方面解析他们的不同。
接口继承
Hashtable继承自Dictionary类而不是AbstractMap,类图如下(jdk1.8)
Hashtable诞生的时间是比Map早,但为了兼容新的集合在jdk1.2之后也继承了Map接口。Dictionary在目前已经完全被Map取代了,所以更加建议使用继承自AbstractMap的HashMap。为了兼容新版本接口还有Hashtable的迭代器:Enumerator。他的接口继承结构如下:
他不仅实现了旧版的Enumeration接口,同时也实现了Iteractor接口,兼容了新的api与使用习惯。这里关于Hashtable还有一个问题:Hashtable是fast-fail的吗 ?
fast-fail指的是在使用迭代器遍历集合过程中,如果集合发生了结构性改变,如添加数据、扩容、删除数据等,迭代器会抛出异常。Enumerator本身的实现是没有fast-fail设计的,但他继承了Iteractor接口之后,就有了fast-fail。看一下源码:
public T next() {
    // 这里在Enumerator的基础上,增加了fast-fail
    if (Hashtable.this.modCount != expectedModCount)
        throw new ConcurrentModificationException();
    //  nextElement()是Enumeration的接口方法
    return nextElement();
}
private void addEntry(int hash, K key, V value, int index) {
    ...
    // 在添加数据之后,会改变modCount的值
    modCount++;
}
所以,Hashtable本身的设计是有fastfail的,但如果使用的Enumerator,则享受不到这个设计了。
哈希算法
Hashtable的哈希算法非常简单粗暴,如下代码
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
获取key的hashcode,通过直接对数组长度求余来获取下标。这里还有一步是hash & 0x7FFFFFFF,目的是把最高位变成0,把hashcode变成一个非负数。为了使得hash可以分布更加均匀,Hashtable默认控制数组的长度为一个素数:初始值为11,每次扩容为原来的两倍+1  。
冲突解决
Hashtable使用的是链表法,也称为拉链法。发生冲突之后会转换为链表。HashMap在jdk1.8之后增加了红黑树,所以在剧烈冲突的情况下,Hashtable的性能下降会比HashMap明显非常多。
Hashtable的装载因子与HashMap一致,默认都是0.75,且建议非特殊情况不要进行修改。
扩容方案
Hashtable的扩容方案也非常简单粗暴,新建一个长度为原来的两倍+1长度的数组,遍历所有的旧数组的数据,重新hash插入新的数组。他的源码非常简单,有兴趣可以看一下:
protected void rehash() {
    int oldCapacity = table.length;
    Entry<?,?>[] oldMap = table;
    // 设置数组长度为原来的2倍+1
    int newCapacity = (oldCapacity << 1) + 1;
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        if (oldCapacity == MAX_ARRAY_SIZE)
            // 如果长度达到最大值,则直接返回
            return;
        // 超过最大值设置长度为最大
        newCapacity = MAX_ARRAY_SIZE;
    }
    // 新建数组
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
	// modcount++,表示发生结构性改变
    modCount++;
    // 初始化装载因子,改变table引用
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    table = newMap;
	// 遍历所有的数据,重新hash后插入新的数组,这里使用的是头插法
    for (int i = oldCapacity ; i-- > 0 ;) {
        for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
            Entry<K,V> e = old;
            old = old.next;
            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            e.next = (Entry<K,V>)newMap[index];
            newMap[index] = e;
        }
    }
}
线程安全
Hashtable和HashMap最大的不同就是线程安全了。jdk1.1的第一批集合框架都被设计为线程安全,但手段都非常粗暴:直接给所有方法上锁 。但我们知道,锁是一个非常重量级的操作,会严重影响性能。Hashtable直接对整个对象上锁的缺点有:
- 同一时间只能有一个线程在读或者写,并发效率极低
 - 频繁上锁进行系统调用,严重影响性能
 
所以虽然Hashtable实现了一定程度上的线程安全,但是却付出了非常大的性能代价。这也是为什么在jdk1.2他们马上就被淘汰了。
不允许空键值
允许空键值这个设计有利也有弊,在ConcurrentHashMap中也禁止插入空键值,但HashMap是允许的。允许value空值会导致get方法返回null时有两种情况:
- 找不到对应的key
 - 找到了但是value为null;
 
当get方法返回null时无法判断是哪种情况,在并发环境下containsKey方法已不再可靠,需要返回null来表示查询不到数据。允许key空值需要额外的逻辑处理,占用了数组空间,且并没有多大的实用价值。HashMap支持键和值为null,但基于以上原因,ConcurrentHashMap是不支持空键值。
小结
总体来说,Hashtable属于旧版本的集合框架,他的设计已经落后了,官方更加推荐使用HashMap;而Hashtable线程安全的特性的同时,也带来了极大的性能代价,更加推荐使用ConcurrentHashMap来代替Hashtable。
SynchronizeMap
SynchronizeMap这个集合类可能并不太熟悉,他是Collections.synchronizeMap()方法返回的对象,如下:
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
    return new SynchronizedMap<>(m);
}
SynchronizeMap的作用是保证了线程安全,但是他的方法和Hashtable一致,也是简单粗暴,直接加锁,如下图:
这里的mutex是什么呢?直接看到构造器:
final Object      mutex;        // Object on which to synchronize
SynchronizedMap(Map<K,V> m) {
    this.m = Objects.requireNonNull(m);
    // 默认为本对象
    mutex = this;
}
SynchronizedMap(Map<K,V> m, Object mutex) {
    this.m = m;
    this.mutex = mutex;
}
可以看到默认锁的就是对象本身,效果和Hashtable其实是一样的。所以,一般情况下也是不推荐使用这个方法来保证线程安全。
ConcurrentHashMap
前面讲到的两个线程安全的Map集合框架,由于性能低下而不被推荐使用。ConcurrentHashMap就是来解决这个问题的。关于ConcurrentHashMap的详细内容,在深入解析ConcurrentHashMap:感受并发编程智慧 一文中已经有了具体的介绍,这里简单介绍一下ConcurrentHashMap的思路。
ConcurrentHashMap并不是和Hashtable一样采用直接对整个数组进行上锁,而是对数组上的一个节点上锁,这样如果并发访问的不是同个节点,那么就无需等待释放锁。如下图:
不同线程之间的访问不同的节点不互相干扰,提高了并发访问的性能。ConcurrentHashMap读取内容是不需要加锁的,所以实现了可以边写边读,多线程共读,提高了性能。
这是jdk1.8优化之后的设计结构,jdk1.7之前是分为多个小数组,锁的粒度比Hashtable稍小了一些。如下:
锁的是Segment,每个Segment对应一个数组。而jdk1.8之后锁的粒度进一步降低,性能也进一步提高了。
LinkedHashMap
HashMap是无法记住插入顺序的,在一些需要记住插入顺序的场景下,HashMap就显得无能为力,所以LinkHashMap就应运而生。LinkedHashMap内部新建一个内部节点类LinkedHashMapEntry继承自HashMap的Node,增加了前后指针。每个插入的节点,都会使用前后指针联系起来,形成一个链表,这样就可以记住插入的顺序,如下图:
图中的红色线表示双向链表的引用。遍历时从head出发可以按照插入顺序遍历所有节点。
LinkedHashMap继承于HashMap,完全是基于HashMap进行改造的,在HashMap中就能看到LinkedMap的身影,如下:
HashMap.java
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
HashMap本身已经预留了接口给LinkedHashMap重写。LinkedHashMap本身的put、remove、get等等方法都是直接使用HashMap的方法。
LinkedHashMap的好处就是记住Node的插入顺序,当使用Iteractor遍历LinkedHashMap时,会按照Node的插入顺序遍历,HashMap则是按照数组的前后顺序进行遍历。
TreeMap
有没有发现前面两个集合框架的命名都是 xxHashMap,而TreeMap并不是,原因就在于TreeMap并不是散列表,只是实现了散列表的功能。
HashMap的key排列是无序的,hash函数把每个key都随机散列到数组中,而如果想要保持key有序,则可以使用TreeMap。TreeMap的继承结构如下:
他继承自Map体系,实现了Map的接口,同时还实现了NavigationMap接口,该接口拓展了非常多的方便查找key的接口,如最大的key、最小的key等。
TreeMap虽然拥有映射表的功能,但是他底层并不是一个映射表,而是一个红黑树。他可以将key进行排序,但同时也失去了HashMap在常数时间复杂度下找到数据的优点,平均时间复杂度是O(logN)。所以若不是有排序的需求,常规情况下还是使用HashMap。
需要注意的是,TreeMap中的元素必须实现Comparable接口或者在TreeMap的构造函数中传入一个Comparator对象,他们之间才可以进行比较大小。
TreeMap本身的使用和特性是比较简单的,核心的重点在于他的底层数据结构:红黑树。这是一个比较复杂的数据结构,限于篇幅,笔者会在另外的文章中详解红黑树。
最后
文章详解了Hashtable这个旧版的集合框架,同时简单介绍了SynchronizeMap、ConcurrentHashMap、LinkedHashMap、TreeMap。这个类都在HashMap的基础功能上,拓展了一些新的特性,同时也带来一些性能上的代价。HashMap并没有称为功能的集大成者,而是把具体的特性分发到其他的Map实现类中,这样做得好处是,我们不需要在单线程的环境下却要付出线程安全的代价。所以了解这些相关Map实现类的特性以及付出的性能代价,则是我们学习的重点。
希望文章对你有帮助~
全文到此,原创不易,觉得有帮助可以点赞收藏评论关注转发。
笔者才疏学浅,有任何想法欢迎评论区交流指正。
如需转载请评论区或私信交流。另外欢迎光临笔者的个人博客:传送门
HashMap相关类:Hashtable、LinkHashMap、TreeMap的更多相关文章
- 【转】java 容器类使用 Collection,Map,HashMap,hashTable,TreeMap,List,Vector,ArrayList的区别
		
原文网址:http://www.360doc.com/content/15/0427/22/1709014_466468021.shtml java 容器类使用 Collection,Map,Hash ...
 - hashMap、hashTable、treeMap的区别
		
1.hashTable是线程安全的.hashMap不是线程安全的 hashmap 线程不安全 允许有null的键和值 效率高一点. 方法不是Synchronize的要提供外同步 有containsva ...
 - HashMap 、HashTable、TreeMap、WeakHashMap的区别是什么
		
Java为数据结构中的映射定义了一个接口java.util.Map,它有4个实现类:HashTable.HashMap.TreeMap.WeakHashMap. HashMap和HashTable的区 ...
 - 牛客网Java刷题知识点之Map的两种取值方式keySet和entrySet、HashMap 、Hashtable、TreeMap、LinkedHashMap、ConcurrentHashMap 、WeakHashMap
		
不多说,直接上干货! 这篇我是从整体出发去写的. 牛客网Java刷题知识点之Java 集合框架的构成.集合框架中的迭代器Iterator.集合框架中的集合接口Collection(List和Set). ...
 - HashMap、HashTable、TreeMap 深入分析及源代码解析
		
在Java的集合中Map接口的实现实例中用的比較多的就是HashMap.今天我们一起来学学HashMap,顺便学学和他有关联的HashTable.TreeMap 在写文章的时候各种问题搞得我有点迷糊尤 ...
 - hashMap、ConcurrentHashMap、hashTable、TreeMap、LinkedHashMap用法区别详解
		
Java集合中设计了一个接口Java.util.Map,它实现类中hashMap.hashTable.TreeMap.ConcurrentHashMap.LinkedHashMap. Map类型的集合 ...
 - Java集合 之Map(HashMap、Hashtable 、TreeMap、WeakHashMap )理解(new)
		
HashMap 说明: 在详细介绍HashMap的代码之前,我们需要了解:HashMap就是一个散列表,它是通过“拉链法”解决哈希冲突的.还需要再补充说明的一点是影响HashMap性能的有两个参数:初 ...
 - 论HashMap、Hashtable、TreeMap、LinkedHashMap的内部排序
		
参考文章 论HashMap.Hashtable.TreeMap.LinkedHashMap的内部排序
 - HashMap相关总结
		
1.HashMap:根据键值hashCode值存储数据,大多数情况下可以直接定位到它的值,但是遍历顺序不确定.所有哈希值相同的值存储到同一个链表中 Ha ...
 
随机推荐
- <连接器和加载器>——概述连接器和加载器
			
0.涉及术语 (1)地址绑定 将抽象的符号与更抽象的符号绑定,如 sqrt 符号与地址 0x0020010绑定. (2)符号解析 程序相互作用通过符号进行,如主程序调用库函数sqrt,连接器通过表明分 ...
 - mybatis insert转update,duplicate关键字的使用示例,及返回情况说明
			
主键存在时又insert转为update某个关键字段,示例如下,注意,如果这条数据曾经不存在,此时执行insert返回条目是1,如果已存在,执行update返回条目是2!!!<insert id ...
 - MySQL 的常用引擎
			
1. InnoDB InnoDB 的存储文件有两个,后缀名分别是 .frm 和 .idb,其中 .frm 是表的定义文件,而 idb 是数据文件. InnoDB 中存在表锁和行锁,不过行锁是在命中索引 ...
 - kali 系列学习01 - 安装、vmtools、ssh服务和共享文件夹
			
Kali介绍Kali Linux是基于Debian的Linux发行版, 设计用于数字取证操作系统.面向专业的渗透测试和安全审计,超过300个渗透测试工具一.安装 1.在虚拟机中安装,详见 https: ...
 - ESP定律脱壳——NsPack3.x脱壳
			
首先进行查壳,NsPack 将程序拖入x64dbg 程序入口处标志性的push F8单步,发现仅有esp寄存器有变化 在esp上右键,在内存窗口查看,下硬件断点 F9运行程序,程序断在pop之后. 使 ...
 - 如何使用iMindMap的组织图表视图?
			
除了常规的发散型思维导图外,iMindMap思维导图软件还提供了辐射导图.灵感导图.组织图表等多种导图形式,供用户更好地展现事项间的层级关系,而其中的组织图表更有助于展现上下级层级关系. 组织图表可以 ...
 - 用思维导图软件MindManager整理假期
			
今天带大家使用MindManager2020软件构建出2020年的节假日思维导图. 既然是做2020年的节假日思维导图,那么有个MindManager技巧就是,关于这一类思维导图我们都可以选择时间线导 ...
 - 关于Linux虚拟机连接不上网络的问题
			
前阵子自学Linux(版本是CentOS6 -VMware ),因为连不上网的问题搁置了一段时间,昨天又重新拾起来,花了一下午时间终于搞定.下面说几点,给自己学习历程一个记录,也希望能帮到其他初学者. ...
 - python3 Failed to establish a new connection: [WinError 10061] 由于目标计算机积极拒绝,无法连接
			
报错源代码from selenium import webdriverimport unittestimport timefrom HTMLTestRunner import HTMLTestRunn ...
 - Goland 2020.2.x 激活码永久破解教程 (最新Goland激活码!2020.11.26亲测可用!)
			
在2020.11.26 Goland的用户们又迎来了一次更新,这就导致很多软件打开时候就提示Goland激活码已经失效,码小辫第一时间给各位分享了关于最新Goland激活破解教程! goland已经更 ...