HashMap和HashTable区别

转载 2018年04月16日 15:02:25

原文:http://blog.csdn.net/seu_calvin/article/details/52653711

0. 前言

HashMapHashTable的区别一种比较简单的回答是:

1HashMap非线程安全的,HashTable是线程安全的。

2HashMap的键和值都允许有null存在,而HashTable则都不行。

3)因为线程安全、哈希效率的问题,HashMap效率HashTable的要高。

但是如果继续追问:Java中的另一个线程安全的与HashMap功能极其类似的类是什么?

同样是线程安全,它与HashTable在线程同步上有什么不同?带着这些问题,开始今天的文章。

本文为原创,相关内容会持续维护,转载请标明出处:http://blog.csdn.net/seu_calvin/article/details/52653711


1.  HashMap概述

Java中的数据存储方式有两种结构,一种是数组,另一种就是链表,前者的特点是连续空间,寻址迅速,但是在增删元素的时候会有较大幅度的移动,所以数组的特点是查询速度快,增删较慢

而链表由于空间不连续,寻址困难,增删元素只需修改指针,所以链表的特点是查询速度慢、增删快

那么有没有一种数据结构来综合一下数组和链表以便发挥他们各自的优势?答案就是哈希表。哈希表的存储结构如下图所示:

 

从上图中,我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点,通过功能类似于hash(key.hashCode())%len的操作,获得要添加的元素所要存放的的数组位置

HashMap的哈希算法实际操作是通过位运算,比取模运算效率更高,同样能达到使其分布均匀的目的,后面会介绍。

键值对所存放的数据结构其实是HashMap中定义的一个Entity内部类,数组来实现的,属性有keyvalue和指向下一个Entitynext


2.  HashMap初始化

HashMap有两种常用的构造方法:

第一种是不需要参数的构造方法:

[java] view plain copy
  1. static final int DEFAULT_INITIAL_CAPACITY = 16//初始数组长度为16  
  2. static final int MAXIMUM_CAPACITY = 1 << 30//最大容量为2的30次方  
  3. //装载因子用来衡量HashMap满的程度  
  4. //计算HashMap的实时装载因子的方法为:size/capacity  
  5. static final float DEFAULT_LOAD_FACTOR = 0.75f; //装载因子  
  6.   
  7. public HashMap() {    
  8.     this.loadFactor = DEFAULT_LOAD_FACTOR;    
  9. threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);  
  10. //默认数组长度为16   
  11.     table = new Entry[DEFAULT_INITIAL_CAPACITY];  
  12.     init();    
  13. }   

第二种是需要参数的构造方法:

[java] view plain copy
  1. public HashMap(int initialCapacity, float loadFactor) {    
  2.         if (initialCapacity < 0)    
  3.             throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);    
  4.         if (initialCapacity > MAXIMUM_CAPACITY)    
  5.             initialCapacity = MAXIMUM_CAPACITY;    
  6.         if (loadFactor <= 0 || Float.isNaN(loadFactor))    
  7.             throw new IllegalArgumentException("Illegal load factor: " + loadFactor);    
  8.   
  9.         // Find a power of 2 >= initialCapacity    
  10.         int capacity = 1;    
  11.         while (capacity < initialCapacity)    
  12.             capacity <<= 1;    
  13.     
  14.         this.loadFactor = loadFactor;    
  15.         threshold = (int)(capacity * loadFactor);    
  16.         table = new Entry[capacity];    
  17.         init();    
  18. }   

从源码可以看出,初始化的数组长度为capacitycapacity的值总是2N次方,大小比第一个参数稍大或相等。


3.  HashMap的put操作
[java] view plain copy
  1. public V put(K key, V value) {    
  2.         if (key == null)    
  3.           return putForNullKey(value);    
  4.         int hash = hash(key.hashCode());    
  5.         int i = indexFor(hash, table.length);    
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {    
  7.             Object k;    
  8.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {    
  9.                 V oldValue = e.value;    
  10.                 e.value = value;    
  11.                 e.recordAccess(this);    
  12.                 return oldValue;    
  13.             }    
  14.         }          
  15. modCount++;    
  16.         addEntry(hash, key, value, i);    
  17.         return null;    
  18. }   
3.1  put进的key为null

从源码中可以看出,HashMap是允许keynull,会调用putForNullKey()方法:

[java] view plain copy
  1. private V putForNullKey(V value) {    
  2.         for (Entry<K,V> e = table[0]; e != null; e = e.next) {    
  3.             if (e.key == null) {    
  4.                 V oldValue = e.value;    
  5.                 e.value = value;    
  6.                 e.recordAccess(this);    
  7.                 return oldValue;    
  8.             }    
  9.         }    
  10.         modCount++;    
  11.         addEntry(0null, value, 0);    
  12.         return null;    
  13. }   
  14.   
  15. void addEntry(int hash, K key, V value, int bucketIndex) {    
  16.     Entry<K,V> e = table[bucketIndex];    
  17.         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);    
  18.         if (size++ >= threshold)    
  19.             resize(2 * table.length);    
  20.     }    

putForNullKey方法会遍历以table[0]为链表头的链表,如果存在keynull的KV,那么替换其value值并返回旧值。否则调用addEntry方法,这个方法也很简单,将[null,value]放在table[0]的位置,并将新加入的键值对封装成一个Entity对象,将next指向原table[0]处的Entity实例。


size表示HashMap中存放的所有键值对的数量

threshold = capacity*loadFactor,最后几行代码表示当HashMapsize大于threshold时会执行resize操作,将HashMap扩容为原来的2。扩容需要重新计算每个元素在数组中的位置indexFor()方法中的table.length参数也证明了这一点。

但是扩容是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。比如说我们有1000个元素,那么我们就该声明new HashMap(2048),因为需要考虑默认的0.75的扩容因子和数组数必须是2N次方。若使用声明new HashMap(1024)那么put过程中会进行扩容。


3.2  put进的key不为null

将上述put方法中的相关代码复制一下方便查看:

[java] view plain copy
  1. int hash = hash(key.hashCode());    
  2. int i = indexFor(hash, table.length);    
  3. for (Entry<K,V> e = table[i]; e != null; e = e.next) {    
  4.     Object k;    
  5.     if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {    
  6.         V oldValue = e.value;    
  7.         e.value = value;    
  8.         e.recordAccess(this);    
  9.         return oldValue;    
  10.        }    
  11. }          
  12. modCount++;    
  13. addEntry(hash, key, value, i);    
  14. return null;    
  15. }  

从源码可以看出,第12行计算将要put进的键值对的数组的位置i。第4行判断加入的key是否和以table[i]为链表头的链表中所有的键值对有重复,若重复则替换value并返回旧值,若没有重复则调用addEntry方法,上面对这个方法的逻辑已经介绍过了。

至此HashMapput操作已经介绍完毕了。


4.  HashMap的get操作
[java] view plain copy
  1. public V get(Object key) {    
  2.    if (key == null)    
  3.        return getForNullKey();    
  4.    int hash = hash(key.hashCode());    
  5.    for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {    
  6.             Object k;    
  7.             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
  8.                 return e.value;    
  9.         }    
  10.     return null;    
  11. }    
  12.   
  13. private V getForNullKey() {    
  14.    for (Entry<K,V> e = table[0]; e != null; e = e.next) {    
  15.    if (e.key == null)    
  16.      return e.value;    
  17.     }    
  18.     return null;    
  19. }    

如果了解了前面的put操作,那么这里的get操作逻辑就很容易理解了,源码中的逻辑已经非常非常清晰了。

需要注意的只有当找不到对应value时,返回的是null。或者value本身就是null。这是可以通过containsKey()来具体判断。


了解了上面HashMapputget操作原理,可以通过下面这个小例题进行知识巩固,题目是打印在数组中出现n/2以上的元素,我们便可以使用HashMap的特性来解决。

[java] view plain copy
  1. public class HashMapTest {    
  2.     public static void main(String[] args) {    
  3.         int [] a = {2,1,3,2,0,4,2,1,2,3,1,5,6,2,2,3};    
  4.         Map<Integer, Integer> map = new HashMap<Integer,Integer>();    
  5.         for(int i=0; i<a.length; i++){    
  6.             if(map.containsKey(a[i])){    
  7.                 int tmp = map.get(a[i]);    
  8.                 tmp+=1;    
  9.                 map.put(a[i], tmp);    
  10.             }else{    
  11.                 map.put(a[i], 1);    
  12.             }    
  13.         }    
  14.         Set<Integer> set = map.keySet();          
  15. for (Integer s : set) {    
  16.             if(map.get(s)>=a.length/2){    
  17.                 System.out.println(s);    
  18.             }    
  19.         }  
  20.     }    
  21. }    


5.  HashMap和HashTable的对比

HashTableHashMap采用相同的存储机制,二者的实现基本一致,不同的是:

1HashMap是非线程安全的,HashTable是线程安全的,内部的方法基本都经过synchronized修饰

2)因为同步、哈希性能等原因,性能肯定是HashMap更佳,因此HashTable已被淘汰。

3 HashMap允许有null值的存在,而在HashTableput进的键值只要有一个null直接抛出NullPointerException

4HashMap默认初始化数组的大小为16HashTable11。前者扩容时乘2,使用位运算取得哈希,效率高于取模。而后者为乘21,都是素数和奇数,这样取模哈希结果更均匀。

这里本来我没有仔细看两者的具体哈希算法过程,打算粗略比较一下区别就过的,但是最近师姐面试美团移动开发时被问到了稍微具体一些的算法过程,我也是醉了不过还是恭喜师姐面试成功,起薪20W,真是羡慕,希望自己一年后找工作也能顺顺利利的。

言归正传,看下两种集合的hash算法。看源码也不难理解。

[java] view plain copy
  1. //HashMap的散列函数,这里传入参数为键值对的key  
  2. static final int hash(Object key) {  
  3.     int h;  
  4.     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  
  5. }   
  6. //返回hash值的索引,h & (length-1)操作等价于 hash % length操作, 但&操作性能更优  
  7. static int indexFor(int h, int length) {  
  8.     // length must be a non-zero power of 2  
  9.     return h & (length-1);  
  10. }  
  11.   
  12. //HashTable的散列函数直接在put方法里实现了  
  13. int hash = key.hashCode();  
  14. int index = (hash & 0x7FFFFFFF) % tab.length;  

6.  HashTable和ConCurrentHashMap的对比

先对ConcurrentHashMap进行一些介绍吧,它是线程安全的HashMap的实现。

HashTable里使用的是synchronized关键字,这其实是对对象加锁,锁住的都是对象整体,当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。

ConcurrentHashMap算是对上述问题的优化,其构造函数如下,默认传入的是160.7516

[java] view plain copy
  1. public ConcurrentHashMap(int paramInt1, float paramFloat, int paramInt2)  {    
  2.     //…  
  3.     int i = 0;    
  4.     int j = 1;    
  5.     while (j < paramInt2) {    
  6.       ++i;    
  7.       j <<= 1;    
  8.     }    
  9.     this.segmentShift = (32 - i);    
  10.     this.segmentMask = (j - 1);    
  11.     this.segments = Segment.newArray(j);    
  12.     //…  
  13.     int k = paramInt1 / j;    
  14.     if (k * j < paramInt1)    
  15.       ++k;    
  16.     int l = 1;    
  17.     while (l < k)    
  18.       l <<= 1;    
  19.     
  20.     for (int i1 = 0; i1 < this.segments.length; ++i1)    
  21.       this.segments[i1] = new Segment(l, paramFloat);    
  22.   }    
  23.   
  24. public V put(K paramK, V paramV)  {    
  25.     if (paramV == null)    
  26.       throw new NullPointerException();    
  27.     int i = hash(paramK.hashCode()); //这里的hash函数和HashMap中的不一样  
  28.     return this.segments[(i >>> this.segmentShift & this.segmentMask)].put(paramK, i, paramV, false);    
  29. }    

ConcurrentHashMap引入了分割(Segment),上面代码中的最后一行其实就可以理解为把一个大的Map拆分成N个小的HashTable,在put方法中,会根据hash(paramK.hashCode())来决定具体存放进哪个Segment,如果查看Segmentput操作,我们会发现内部使用的同步机制是基于lock操作的,这样就可以对Map的一部分(Segment)进行上锁,这样影响的只是将要放入同一个Segment的元素的put操作保证同步的时候,锁住的不是整个MapHashTable就是这么做的),相对于HashTable提高了多线程环境下的性能,因此HashTable已经被淘汰了


7.  HashMap和ConCurrentHashMap的对比

最后对这俩兄弟做个区别总结吧:

1)经过4.2的分析,我们知道ConcurrentHashMap对整个桶数组进行了分割分段(Segment)然后在每一个分段上都用lock锁进行保护,相对于HashTable的syn关键字锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。

(2)HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。


至此对HashMap、HashTable以及ConCurrentHashMap异同比较总结完毕。

请尊重原创,转载请出自出处:http://blog.csdn.net/seu_calvin/article/details/52653711


HashMap和Hashtable的详细区别

HashMap和Hashtable的区别 两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全。Hashtable的实现方法里面都添加了synchronized关键字来确保线...
  • qq_35181209
  • qq_35181209
  • 2017-07-05 22:14:38
  • 2238

HashMap和Hashtable以及TreeMap的区别

最近用到了这三种集合类,由于不是很熟练,所以想整理下。而且前段时间面试,一般情况下面试官喜欢问HashMap和Hashtable的主要区别,其用意是想问被面试者这俩那个是线程安全的。但是HashMap...
  • horero
  • horero
  • 2016-12-19 11:47:27
  • 1245

HashTable和HashMap的区别详解

一、HashMap简介       HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。       Has...
  • fujiakai
  • fujiakai
  • 2016-06-04 19:35:53
  • 66900

HashMap、HashTable与ConcurrentHashMap的区别

1、HashTable与HashMap (1)HashTable和HashMap都实现了Map接口,但是HashTable的实现是基于Dictionary抽象类。 (2)在HashMap中,nul...
  • universe_ant
  • universe_ant
  • 2017-02-28 21:55:55
  • 550

HashMap,HashTable,HashSet之间的区别

HashMap,HashTable,HashSet之间的区别 1.实现接口的不同 3.执行效率 2.线程安全性,同步 4.key,value能否存放NULL 5.添加元素的方法不同...
  • baidu_21578557
  • baidu_21578557
  • 2016-04-18 19:35:06
  • 689

面试题_HashTable和HashMap的区别

第一,继承不同。 public class Hashtable extends Dictionary implements Map public class HashMap extends A...
  • baidu_31337243
  • baidu_31337243
  • 2015-09-16 11:25:07
  • 412

j2ee面试题

  • 2012年12月10日 13:44
  • 134KB
  • 下载

从Java源码的角度来分析HashMap与HashTable的区别

由于HashMap与HashTable都是用来存储Key-Value的键值对,所以经常拿来对比二者的区别,下面就从源码的角度来分析一下HashMap与HashTable的区别,...
  • a_woxinfeiyang_a
  • a_woxinfeiyang_a
  • 2016-06-02 17:49:11
  • 1522

HashTable, HashSet, HashMap的区别

hash是一种很常见也很重要的数据结构,是用hash函数根据键值(key)计算出存储地址,以便直接访问。由完美hash函数(即键值和地址是一一对应的)计算出的地址,在访问时,时间复杂度为O(1)。 j...
  • xxgujh
  • xxgujh
  • 2015-08-03 22:23:51
  • 5757

HashMap底层实现原理---HashMap与HashTable区别

①HashMap的工作原理 HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算...
  • w433668
  • w433668
  • 2017-07-28 23:38:59
  • 845
收藏助手
不良信息举报
您举报文章:HashMap和HashTable区别
举报原因:
原因补充:

(最多只允许输入30个字)