Hashtable、HashMap、TreeMap 都是最常见的一些 Map 实现,是以键值对的形式存储和操作数据的容器类型。
(1) 元素特性
HashTable中的key、value都不能为null;HashMap中的key、value可以为null,很显然只 能有一个key为null的键值对,但是允许有多个值为null的键值对;TreeMap当未实现 Co mparator 接口时,key 不可以为null;当实现 Comparator 接口时,若未对null情况进行判 断,则key不可以为null,反之亦然。
(2)顺序特性
HashTable、HashMap具有无序特性。TreeMap是利用红黑树来实现的(树中的每个节点的 值,都会大于或等于它的左子树中的所有节点的值,并且小于或等于它的右子树中的所有节 点的值),实现了SortMap接口,能够对保存的记录根据键进行排序。所以一般需要排序的 情况下是选择TreeMap来进行,默认为升序排序方式(深度优先搜索),可自定义实现Com parator接口实现排序方式。
(3)初始化与增长方式
初始化时:HashTable在不指定容量的情况下的默认容量为11,且不要求底层数组的容量一 定要为2的整数次幂;HashMap默认容量为16,且要求容量一定为2的整数次幂。扩容时:Hashtable将容量变为原来的2倍加1;HashMap扩容将容量变为原来的2倍。
(4)线程安全性
HashTable其方法函数都是同步的(采用synchronized修饰),不会出现两个线程同时对数 据进行操作的情况,因此保证了线程安全性。也正因为如此,在多线程运行环境下效率表现 非常低下。因为当一个线程访问HashTable的同步方法时,其他线程也访问同步方法就会进 入阻塞状态。比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作 也必须被阻塞,大大降低了程序的运行效率,在新版本中已被废弃,不推荐使用。HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据 的不一致。
如果需要同步
(1)可以用 Collections的synchronizedMap方法;
(2)使用Co ncurrentHashMap类,相较于HashTable锁住的是对象整体, ConcurrentHashMap基于lo ck实现锁分段技术。首先将Map存放的数据分成一段一段的存储方式,然后给每一段数据分 配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访 问。ConcurrentHashMap不仅保证了多线程运行环境下的数据访问安全性,而且性能上有 长足的提升。
HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用 键对象的hashCode()方法来计算hashcode,然后后找到bucket位置来储存值对象。当获取对 象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表 来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。HashMap在每个 链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个 bucket位置的链表中,可通过键对象的equals()方法用来找到键值对。如果链表大小超过阈 值(TREEIFY_THRESHOLD, 8),链表就会被改造为树形结构。
1、对Key求Hash值,然后再计算下标
2、如果没有碰撞,直接放入桶中(碰撞的意思是计算得到的Hash值相同,需要放到同一个bucket中)
3、如果碰撞了,以链表的方式链接到后面
4、如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表
5、如果节点已经存在就替换旧值
6、如果桶满了(容量16*加载因子0.75),就需要 resize(扩容2倍后重排)
调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。
1、开放定址法
基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈 希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不 冲突的哈希地址pi ,将相应元素存入其中。
2、再哈希法
这种方法是同时构造多个不同的哈希函数:Hi=RH1(key) i=1,2,…,k 当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
3、链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链 表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地 址法适用于经常进行插入和删除的情况。
4、建立公共溢出区
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元 素,一律填入溢出表。
1、扰动函数可以减少碰撞。原理是如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这就意味着存链表结构减小,这样取值的话就不会频繁调用equal方法,这样就能提高HashMap的性能。(扰动即Hash方法内部的算法实现,目的是让不同对象返回不同hashcode。)
2、使用不可变的、声明为final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。为什么String, Interger这样的wrapper类适合作为键?因为String是final的,而且已经重写了equals()和hashCode()方法了。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。
1、每个节点非红即黑
2、根节点总是黑色的
3、如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
4、每个叶子节点都是黑色的空节点(NIL节点)
5、从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)
之所以选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>>16;
注意,为什么这里需要将高位数据移位到低位进行异或?这是因为有些数据计算出的哈希值差异主要在高位,而 HashMap 里的哈希寻址是忽 略容量以上的高位的,那么这种处理就可以有效避免类似情况下的哈希碰撞。
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。
所以为了提高查询的效率,就要对HashMap的数组进行扩容:
数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当 HashMap中元素个数超过160.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。