hashmap hashtable
链接:https://zhuanlan.zhihu.com/p/37607299
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
在面试的时候,java集合最容易被问到的知识就是HashMap与Hashtable的比较,通常我们也很容易回答出一下几点:
1、HashMap是线程不安全的,在多线程环境下会容易产生死循环,但是单线程环境下运行效率高;Hashtable线程安全的,很多方法都有synchronized修饰,但同时因为加锁导致单线程环境下效率较低。
2、HashMap允许有一个key为null,允许多个value为null;而Hashtable不允许key或者value为null。
如果你觉得会到处这两点就对HashMap、HashTable有很深的了解的话,那就大错特错!本文将基于JDK1.8的HashMap和Hashtable的源码进行详细的比较。
构造函数的比较
HashMap:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
//该方法返回大于等于cap的最小2次幂的整数
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
Hashtable:
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
public Hashtable() {
this(11, 0.75f);
}
可以看出HashMap的底层数组的长度必须为2^n,这样做的好处是为以后的hash算法做准备,而Hashtable底层数组的长度可以为任意值,这就造成了当底层数组长度为合数的时候,Hashtable的hash算法散射不均匀,容易产生hash冲突。所以,可以清楚的看到Hashtable的默认构造函数底层数组长度为11(质数),至于为什么Hashtable的底层数组用质数较好,请参考博文:http://blog.csdn.net/liuqiyao_01/article/details/14475159;
Hash算法的比较
hash算法的区别
HashMap:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//手写的,源码在不存在这一句,但是原理是类似的,详情可以去看putVal方法
int i = (table.length-1) & hash(key)
HashMap的hash算法通过非常规的设计,将底层table长度设计为2^n(合数),这是HashMap的一处优化。它使用了&运算来代替%运算以减少性能上面的损耗。为何&运算可以代替%运算呢?
如果两个整数做&运算,实质是两个整数转换为2进制数后每一个bit位的分别做&运算,所以其最终的运算结果的值不会超过最小的那个数,这个时候只需要搞清楚三点就能明白其实现原理:
1、合数2^n转换为2进制的数之后,最高位为1其余的位数都为0,比如16-->10000,32-->100000。那么,2^n-1转换为2进制后,所有的bit位都为1,比如31-->11111,127-->1111111。所以,hashcode与(2^n-1)做&运算每一个bit位都可以保持原来的值。
2、当hash()方法得到的值<=(table.length-1),其运算结果就在[0,table.length-1]范围内均匀散射。当hash()方法得到的值小于table.length-1的时候,运算结果就是该方法的原值。当hash()方法得到的值等于table.length-1的的时候,运算结果为0。
3、当hash()得到的值>(table.length-1),此时table.length-1为较小的数,所以&运算的结果还是在[0,table.length-1]之间。具体实现是这样的,table.length-1转化为2进制的数之后位数小于hash()方法得到的2进制数,所以它的高位只能用0去补齐,又由于&运算的特性,只要有一个为0那么都为0,所以此时相当于转化为情况1。
而hash()方法的具体作用是使得table的length较小的时候高低bit都能参与运算,具体分析请参考:https://tech.meituan.com/java-hashmap.html
Hashtable:
int hash = key.hashCode();
//0x7FFFFFFF转换为10进制之后是Intger.MAX_VALUE,也就是2^31 - 1
int index = (hash & 0x7FFFFFFF) % tab.length;
很容易看出Hashtable的hash算法首先使得hash的值小于等于整型数的最大值,再通过%运算实现均匀散射。
由于计算机是底层的运算是基于2进制的,所以HashMap的hash算法使用&运算代替%运算,在运算速度上明显HashMap的hash算法更优。
扩容的机制的区别
因为无论是HasHMap或者HashTable的扩容都是基于底层的hash算法的,所以将扩容机制放在hash算法部分讲。
HashMap扩容:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 将阈值扩大为2倍
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // 当threshold的为0的使用默认的容量,也就是16
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//新建一个数组长度为原来2倍的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
//HashMap在JDK1.8的时候改善了扩容机制,原数组索引i上的链表不需要再反转。
// 扩容之后的索引位置只能是i或者i+oldCap(原数组的长度)
// 所以我们只需要看hashcode新增的bit为0或者1。
// 假如是0扩容之后就在新数组索引i位置,新增为1,就在索引i+oldCap位置
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 新增bit为0,扩容之后在新数组的索引不变
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else { //新增bit为1,扩容之后在新数组索引变为i+oldCap(原数组的长度)
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
//数组索引位置不变,插入原索引位置
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
//数组索引位置变化为j + oldCap
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
从源码中可以看出,HashMap数组的扩容的整体思想就是创建一个长度为原先2倍的数组。然后对原数组进行遍历和复制。只不过jdk1.8对扩容进行优化,使得扩容不再需要进行链表的反转,只需要知道hashcode新增的bit位为0还是1。如果是0就在原索引位置,新增索引是1就在oldIndex+oldCap位置。
可能有些人对新增bit位感到困惑。在这里解释一下,这里的新增指的是有效bit位。在上面说到过,两个整数做&运算,转换为2进制的后,看bit位较短的那个数。也就是说bit位较长的数与bit位较短的数做&运算,多出来的bit需要用0来补齐,由于是&运算(只有一个为0那么其结果就为0),所以,新增的0位不是有效的bit位。对应于hash算法来说,通常hashcode的值比较大(转换为2进制数后bit为较多),扩容之后将数组的长度扩大为2倍,那么n(数组的长度),转换为2进制数后相较于未扩容之前的n多增加了一个1的有效bit位。简化版的例子如下:
初始容量为16,那么15转换为二进制数位1111,现在进行一次扩容之后容量变为32,那么31转换为2进制是为11111。现有两个key,一个hashcode为107转换为二进制数后为1101011,另一个的hashcode是379转换为二进制数后为101111011。在容量为16的时候,这两个key,具体计算索引过程为:
0001111 & 1101011 = 1011 000001111 & 101111011 = 1011 转换为10进制数后都为11。
现在来看一下扩容之后两个key的索引:
0011111 & 1101011 = 1011 000011111 & 101111011 = 11011 一个对应的索引仍然是11,而另一个却变为27(27 = 11+16)
Hashtable扩容:
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
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的扩容将先创建一个长度为原长度2倍的数组,再使用头插法将链表进行反序。
结构的区别
HashMap在jdk1.8在原先的数组+链表的结构进行了优化,将实现结构变为数组+链表+红黑树,有关红黑树的文章详细请参考博文:http://www.cnblogs.com/skywang12345/p/3245399.html。这里只需要之后,红黑树是近似平衡树。做这样的优化,是为了防止在一个哈希桶位置链表过长,影响get等方法的时间。详细分析用put方法来做举例:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//HashMap在构造方法中只是设置了一些参数,只有到put方法才会创建底层数组
//这使用的是懒加载策略
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
// 节点为红黑树节点,那么使用红黑树的插入方式
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//默认情况下当一条链的节点个数大于8的时候就需要转换为红黑树节点
//当然对底层数组的长度也有要求,最低长度为64,否则会先进行扩容
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//put一个元素后检查size是否大于阈值,大于则需要进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
从源码中可以看出基于HashMap为了防止链表过长影响get等方法的性能,在一条链表节点元素大于8的时候,会将链表封装成红黑树。
再来看一下Hashtable
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
可以看出Hashtable到了jdk1.8了内部结构并没有实质优化,继续使用数组+链表的方式实现。
总结
可以看出到jdk1.8 HashMap和Hashtable的区别越来越大,HashMap相较与之前的jdk做了很多的优化,最重要的是在内部实现结构上引进了红黑数还有扩容上的优化。Hashtable作为jdk1.2遗留下来的类,到jdk1.8没有大改,所以对数据的一致性要求较低的话可以使用ConcurrentHashMap来替代Hashtable。
hashmap hashtable的更多相关文章
- Collections+Iterator 接口 | Map+HashMap+HashTable+TreeMap |
Collections+Iterator 接口 1. Collections 是一个操作 Set.List 和 Map 等集合的工具类 Collections 中提供了大量方法对集合元素进行排序.查询 ...
- HashMap,Hashtable,TreeMapMap
package com.wzy.list; import java.util.HashMap; import java.util.Hashtable; import java.util.Iterato ...
- HashMap & HashTable的区别
HashMap & HashTable的区别主要有以下: 1.HashMap是线程不安全的,HashTable是线程安全的.由这点区别可以知道,不考虑线程安全的情况下使用HashMap的效率明 ...
- [Java集合] 彻底搞懂HashMap,HashTable,ConcurrentHashMap之关联.
注: 今天看到的一篇讲hashMap,hashTable,concurrentHashMap很透彻的一篇文章, 感谢原作者的分享. 原文地址: http://blog.csdn.net/zhanger ...
- HashMap Hashtable区别
http://blog.csdn.net/java2000_net/archive/2008/06/05/2512510.aspx 我们先看2个类的定义 public class Hashtable ...
- Java 集合系列14之 Map总结(HashMap, Hashtable, TreeMap, WeakHashMap等使用场景)
概要 学完了Map的全部内容,我们再回头开开Map的框架图. 本章内容包括:第1部分 Map概括第2部分 HashMap和Hashtable异同第3部分 HashMap和WeakHashMap异同 转 ...
- HashMap,HashTable,TreeMap区别和用法
开始学HashTable,HashMap和TreeMap的时候比较晕,觉得作用差不多,但是到实际运用的时候又发现有许多差别的.需要大家注意,在实际开发中以需求而定. java为数据结构中的映射定义了一 ...
- 基于散列的集合 HashSet\HashMap\HashTable
HashSet\HashMap\HashTable 1 基于散列的集合 2 元素会根据hashcode散列,因此,集合中元素的顺序不一定与插入的顺序一致. 3 根据equals方法与hashCode方 ...
- 杨晓峰-Java核心技术-9 HashMap Hashtable TreeMap MD
目录 第9讲 | 对比Hashtable.HashMap.TreeMap有什么不同? 典型回答 考点分析 知识扩展 Map 整体结构 有序 Map HashMap 源码分析 容量.负载因子和树化 精选 ...
- Map总结(HashMap, Hashtable, TreeMap, WeakHashMap等使用场景)
概要 学完了Map的全部内容,我们再回头开开Map的框架图. 本章内容包括:第1部分 Map概括第2部分 HashMap和Hashtable异同第3部分 HashMap和WeakHashMap异同 转 ...
随机推荐
- 一个初学者的辛酸路程-继续Django
问题1:HTTP请求过来会先到Django的那个地方? 先到urls.py ,里面写的是对应关系,1个URL对应1个函数名. 如果发URL请求过来,到达这里,然后帮你去执行指定的函数,函数要做哪些事 ...
- 融合模型Aggregation
从一堆弱分类器融合得到强分类器. 比如假设现在你只能水平或竖直线分割,那么无论如何都分不好,但是假设组合三次分割,就会得到如图所示的一个较好的分割线. 再比如,PLA 融合后有large margin ...
- Java 实现二叉树的构建以及3种遍历方法
转载自http://ocaicai.iteye.com/blog/1047397 大二下学期学习数据结构的时候用C介绍过二叉树,但是当时热衷于java就没有怎么鸟二叉树,但是对二叉树的构建及遍历一直耿 ...
- Android记事本09
昨天: Activity的数据传递. 今天: 从Activity中返回数据 请求码和返回码的作用 遇到的问题: 无.
- pytorch:EDSR 生成训练数据的方法
Pytorch:EDSR 生成训练数据的方法 引言 Winter is coming 正文 pytorch提供的DataLoader 是用来包装你的数据的工具. 所以你要将自己的 (numpy arr ...
- Java 多线程(Thread)学习
多线程:就是进程的扩展,实现并发.一个进程可以包含多个线程,进程一般是由操作系统控制,而线程就是由程序员控制的,所以作为编程人员做好线程是我们的重点. 线程和进程一样分为五个阶段:创建.就绪.运行.阻 ...
- (总结)统计Apache或Nginx访问日志里的独立IP访问数量的Shell
1.把IP数量直接输出显示:cat access_log_2011_06_26.log |awk '{print $1}'|uniq -c|wc -l 2.把IP数量输出到文本显示:cat acces ...
- 基于eclipse创建maven工程
一.创建项目 1.Eclipse中用Maven创建项目 上图中Next 2.继续Next 3.选maven-archetype-webapp后,next 4.填写相应的信息,Packaged是默认创建 ...
- [CF463D]Gargari and Permutations
题目大意:给你$k(2\leqslant k\leqslant5)$个$1\sim n(n\leqslant10^3)$的排列,求它们的最长子序列 题解:将$k$个排列中每个元素的位置记录下来.如果是 ...
- 【距离GDOI:128天】【POJ2778】DNA Sequence(AC自动机+矩阵加速)
已经128天了?怎么觉得上次倒计时150天的日子还很近啊 ....好吧为了把AC自动机搞透我也是蛮拼的..把1030和这道题对比了无数遍...最终结论是...无视时间复杂度,1030可以用这种写法解. ...