热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Java集合——HashMap、HashTable以及ConCurrentHashMap异同比较

0.前言HashMap和HashTable的区别一种比较简单的回答是:(1)HashMap是非线程安全的,HashTable是线程安全的。(2)HashMap的键和值都允许有null存在,而Hash

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有两种常用的构造方法:

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

static final int DEFAULT_INITIAL_CAPACITY = 16; //初始数组长度为16static final int MAXIMUM_CAPACITY = 1 <<30; //最大容量为2的30次方
//装载因子用来衡量HashMap满的程度
//计算HashMap的实时装载因子的方法为:size/capacity
static final float DEFAULT_LOAD_FACTOR = 0.75f; //装载因子

public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
//默认数组长度为16
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}

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

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);          // Find a power of 2 >= initialCapacity          int capacity = 1;          while (capacity 

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


3.   HashMap的put操作
public V put(K key, V value) {          if (key == null)            return putForNullKey(value);          int hash = hash(key.hashCode());          int i = indexFor(hash, table.length);          for (Entry e = table[i]; e != null; e = e.next) {              Object k;              if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {                  V oldValue = e.value;                  e.value = value;                  e.recordAccess(this);                  return oldValue;              }          }        modCount++;          addEntry(hash, key, value, i);          return null;  } 
3.1   put进的key为null

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

private V putForNullKey(V value) {          for (Entry e = table[0]; e != null; e = e.next) {              if (e.key == null) {                  V oldValue = e.value;                  e.value = value;                  e.recordAccess(this);                  return oldValue;              }          }          modCount++;          addEntry(0, null, value, 0);          return null;  } void addEntry(int hash, K key, V value, int bucketIndex) {      Entry e = table[bucketIndex];          table[bucketIndex] = new Entry(hash, key, value, e);          if (size++ >= threshold)              resize(2 * table.length);      }  

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方法中的相关代码复制一下方便查看:

int hash = hash(key.hashCode());  int i = indexFor(hash, table.length);  for (Entry e = table[i]; e != null; e = e.next) {      Object k;      if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {          V oldValue = e.value;          e.value = value;          e.recordAccess(this);          return oldValue;         }  }        modCount++;  addEntry(hash, key, value, i);  return null;  }

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

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


4.   HashMap的get操作
public V get(Object key) {     if (key == null)         return getForNullKey();     int hash = hash(key.hashCode());     for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) {              Object k;              if (e.hash == hash && ((k = e.key) == key || key.equals(k)))                return e.value;          }      return null;  }  private V getForNullKey() {     for (Entry e = table[0]; e != null; e = e.next) {     if (e.key == null)       return e.value;      }      return null;  }  

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

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


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

public class HashMapTest {      public static void main(String[] args) {          int [] a = {2,1,3,2,0,4,2,1,2,3,1,5,6,2,2,3};          Map map = new HashMap();          for(int i=0; i set = map.keySet();        for (Integer s : set) {              if(map.get(s)>=a.length/2){                  System.out.println(s);              }          }    }  }  


5.  HashMap和HashTable的对比

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

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

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

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

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

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

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

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

6.  HashTable和ConCurrentHashMap的对比

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

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

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

public ConcurrentHashMap(int paramInt1, float paramFloat, int paramInt2)  {      //…    int i = 0;      int j = 1;      while (j >> this.segmentShift & this.segmentMask)].put(paramK, i, paramV, false);  }  

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



推荐阅读
  • HashTable与ConcurrentHashMap均可实现HashMap的功能,对外提供了键值对存储的数据结构。但是在内部结构及实现上有何区别,性能上的差异到底在哪里又是如何导致的 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 类Hashtable<K,V>所有已实现的接口:Serializable,Cloneable,Map<K,V>此类实现一个哈希表,该哈希表将键映 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了Java中Hashtable的clear()方法,该方法用于清除和移除指定Hashtable中的所有键。通过示例程序演示了clear()方法的使用。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • HashMap的相关问题及其底层数据结构和操作流程
    本文介绍了关于HashMap的相关问题,包括其底层数据结构、JDK1.7和JDK1.8的差异、红黑树的使用、扩容和树化的条件、退化为链表的情况、索引的计算方法、hashcode和hash()方法的作用、数组容量的选择、Put方法的流程以及并发问题下的操作。文章还提到了扩容死链和数据错乱的问题,并探讨了key的设计要求。对于对Java面试中的HashMap问题感兴趣的读者,本文将为您提供一些有用的技术和经验。 ... [详细]
  • Map主要用于存储健值对,根据键得到值,因此不允许键重复(重复了覆盖了),但允许值重复hashMap是hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区 ... [详细]
  • hashmap线程不安全允许有null的键和值效率高一点、方法不是Synchronize的要提供外同步有containsvalue和containsKey方法HashMap是Java1 ... [详细]
  • 一、HashMap1.HashMap概述:HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是 ... [详细]
  • Whatsthedifferencebetweento_aandto_ary?to_a和to_ary有什么区别? ... [详细]
  • 集合类中只能存放对象,而不能存放原始数据类型的元素,所以当有原始数据类型需要存放时,只能将其转换成相应的包装类对象。1)访问和遍历数组元素时,ArrayList的 ... [详细]
author-avatar
流行天王MJ
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有