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

转发:https://www.cnblogs.com/zx-bob-123/archive/2017/12/26/8118074.html

0. 前言

 

HashMap和HashTable的区别一种比较简单的回答是:

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

(2)HashMap的键和值都允许有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内部类,数组来实现的,属性有key、value和指向下一个Entity的next。

 

 

2.  HashMap初始化

 

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

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

复制代码

static final int DEFAULT_INITIAL_CAPACITY = 16; //初始数组长度为16  
static 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 < initialCapacity)    capacity <<= 1;    this.loadFactor = loadFactor;    threshold = (int)(capacity * loadFactor);    table = new Entry[capacity];    init();    
}   

复制代码

 

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

 

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<K,V> 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

 

复制代码

private V putForNullKey(V value) {    for (Entry<K,V> 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<K,V> e = table[bucketIndex];    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);    if (size++ >= threshold)    resize(2 * table.length);    } 

复制代码

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

 

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

 

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

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

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

 

3.2  put进的key不为null

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

复制代码

int hash = hash(key.hashCode());    
int i = indexFor(hash, table.length);    
for (Entry<K,V> 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;    
}

复制代码

 

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

至此HashMap的put操作已经介绍完毕了。

 

4.  HashMap的get操作

复制代码

public V get(Object key) {    if (key == null)    return getForNullKey();    int hash = hash(key.hashCode());    for (Entry<K,V> 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<K,V> e = table[0]; e != null; e = e.next) {    if (e.key == null)    return e.value;    }    return null;    
}  

复制代码

 

 

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

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

 

了解了上面HashMap的put和get操作原理,可以通过下面这个小例题进行知识巩固,题目是打印在数组中出现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<Integer, Integer> map = new HashMap<Integer,Integer>();    for(int i=0; i<a.length; i++){    if(map.containsKey(a[i])){    int tmp = map.get(a[i]);    tmp+=1;    map.put(a[i], tmp);    }else{    map.put(a[i], 1);    }    }    Set<Integer> set = map.keySet();          
for (Integer s : set) {    if(map.get(s)>=a.length/2){    System.out.println(s);    }    }  }    
} 

复制代码

5.  HashMap和HashTable的对比

 

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

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

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

(3) HashMap允许有null值的存在,而在HashTable中put进的键值只要有一个null,直接抛出NullPointerException。

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

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

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

复制代码

//HashMap的散列函数,这里传入参数为键值对的key  
static 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算是对上述问题的优化,其构造函数如下,默认传入的是16,0.75,16。

复制代码

public ConcurrentHashMap(int paramInt1, float paramFloat, int paramInt2)  {    //…  int i = 0;    int j = 1;    while (j < paramInt2) {    ++i;    j <<= 1;    }    this.segmentShift = (32 - i);    this.segmentMask = (j - 1);    this.segments = Segment.newArray(j);    //…  int k = paramInt1 / j;    if (k * j < paramInt1)    ++k;    int l = 1;    while (l < k)    l <<= 1;    for (int i1 = 0; i1 < this.segments.length; ++i1)    this.segments[i1] = new Segment(l, paramFloat);    }    public V put(K paramK, V paramV)  {    if (paramV == null)    throw new NullPointerException();    int i = hash(paramK.hashCode()); //这里的hash函数和HashMap中的不一样  return this.segments[(i >>> this.segmentShift & this.segmentMask)].put(paramK, i, paramV, false);    
}  

复制代码

 

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

 

7.  HashMap和ConCurrentHashMap的对比

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/423594.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java面试题39 给定includel.isp文件代码片段,如下

java面试题39 给定includel.isp文件代码片段&#xff0c;如下&#xff1a; <% pageContext.setAttribute(“User”,”HAHA”);%> ______ // 此处填写代码 给定include2.jsp文件代码片段如下&#xff1a; <%pageContext.getAttribute(“User”)%> 要求运行i…

圣诞快乐啊

Let It Snow! Let It Snow! Let It Snow! 原由 *Sakura* 上載 马上就圣诞了&#xff0c;早早祝福大家&#xff0c;圣诞快乐啊&#xff01; 转载于:https://www.cnblogs.com/joypen/archive/2008/12/24/1693224.html

不用AJAX框架实现AJAX效果

AJAX( Asynchronous JavaScript and XML),即&#xff1a;javascript和XML; 是一种进行页面局部异步处理数据的技术&#xff0c;用AJAX向服务器发送请求和获取服务器返回的数据并且更新到界面中&#xff0c;不是整个页面的刷新&#xff0c;而是HTML页面中使用JAVASCRIPT创建XMLH…

Java 面试题及答案

JAVA基础 JAVA中的几种基本类型&#xff0c;各占用多少字节&#xff1f; 下图单位是bit,非字节 1B8bit String能被继承吗&#xff1f;为什么&#xff1f; 不可以&#xff0c;因为String类有final修饰符&#xff0c;而final修饰的类是不能被继承的&#xff0c;实现细节不允许…

java面试题40 当编译并运行下面程序时会发生什么结果()

java面试题40 当编译并运行下面程序时会发生什么结果&#xff08;&#xff09; 1 2 3 4 5 6 7 8 9 10 11 public class Bground extends Thread{ public static void main(String argv[]){ Bground b new Bground(); b.run(); } public void sta…

穷人最缺少的是什么?

法国一位年轻人很穷&#xff0c;很苦。后来&#xff0c;他以推销装饰肖像画起家&#xff0c;在不到十年的时间里&#xff0c;迅速跃身为法国50大富翁之列&#xff0c;成为一位年轻的媒体大亨。不幸&#xff0c;他因患上前列腺癌&#xff0c;1998年在医院去世。他去世后&#xf…

java面试题41 下面字段声明中哪一个在interface主体内是合法的? ()

java面试题41 下面字段声明中哪一个在interface主体内是合法的? &#xff08;&#xff09; A private final static int answer 42; B public static int answer 42; C final static answer 42; D int answer; 1、接口&#xff1a;一种把类抽象的更彻底&#xff0c;接…

java面试题42从以下哪一个选项中可以获得Servlet的初始化参数?

java面试题42从以下哪一个选项中可以获得Servlet的初始化参数? A Servlet B ServletContext C ServletConfig D GenericServlet 蒙蔽树上蒙蔽果&#xff0c;蒙蔽树下你和我 ServletContext对象&#xff1a;servlet容器在启动时会加载web应用&#xff0c;并为每个web应用…

asp.net如何生成图片验证码

新建一个页面image.aspx,添加命名空间&#xff1a; usingSystem.Drawing.Imaging;usingSystem.IO;然后在Page_load事件拷入如下代码&#xff1a; //生成4位的验证码stringtmp RndNum(4); HttpCookie a newHttpCookie("ImageV",tmp); Response.Cookies.Add(a…

Java中如何实现代理机制(JDK、CGLIB)

代理分为两种&#xff1a; 1.静态代理 2.动态代理 动态代理又分为两种&#xff1a;jdk 实现 &#xff1b;Cglib 实现 3.Java中如何实现代理机制(JDK、CGLIB) JDK动态代理&#xff1a;代理类和目标类实现了共同的接口&#xff0c;用到InvocationHandler接口。CGLIB动态代理…

java面试题43要使某个类能被同一个包中的其他类访问,但不能被这个包以外的类访问,可以( )

java面试题43要使某个类能被同一个包中的其他类访问&#xff0c;但不能被这个包以外的类访问&#xff0c;可以( ) A让该类不使用任何关键字 B使用private关键字 C 使用protected关键字 D 使用void关键字 答案为A 我是歌谣&#xff0c;如果有什么不合理之处指出。我是歌谣&…

app.config数据库路径问题

最近做一个小东西&#xff0c;用到了subsonic&#xff0c;因为用的是sqlite&#xff0c;数据库的路径问题又被提了上来。在connectionStrings里面只能指定绝对路径。对于sqlite,access这样的数据库来说&#xff0c;却需要使用到相对路径&#xff0c;否则发布后就会出问题。自己…

java面试题44关于 Socket 通信编程,以下描述正确的是:( )

java面试题44关于 Socket 通信编程&#xff0c;以下描述正确的是&#xff1a;&#xff08; &#xff09; A 客户端通过new ServerSocket()创建TCP连接对象 B 客户端通过TCP连接对象调用accept()方法创建通信的Socket对象 C 客户端通过new Socket()方法创建通信的Socket对象 …

Java弱引用(WeakReference)的理解与使用

转:https://blog.csdn.net/zmx729618/article/details/54093532 看到篇帖子, 国外一个技术面试官在面试senior java developer的时候, 问到一个weak reference相关的问题. 他没有期望有人能够完整解释清楚weak reference是什么, 怎么用, 只是期望有人能够提到这个concept和j…

在VB应用程序中调用Excel2000

Visual Basic简称(VB)是设计Windows应用程序强有力的开发工具&#xff0c;“全球绝大多数Windows应用程序都是用VB开发的”&#xff1b; Excel是目前使用最广泛的办公应用软件之一&#xff0c;它具有强大的数学分析与计算功能&#xff0c;包括很多VB没有的求值数学表达式的函数…

shiro学习(7):shiro连接数据库 方式二

工具idea 先看看数据库 shiro_role_permission 数据 shiro_user shiro_user_role 数据 我们先看一下目录结构 首先 jar包引入 pom.xml文件 <?xml version"1.0" encoding"UTF-8"?><project xmlns"http://maven.apache.org/POM/4.0.0&quo…

java中的4种reference的差别和使用场景(含理论、代码和执行结果)

转:https://blog.csdn.net/aitangyong/article/details/39453365 我们知道java语言提供了4种引用类型&#xff1a;强引用、软引用(SoftReference)、弱引用&#xff08;WeakReference&#xff09;和幽灵引用&#xff08;PhantomReference&#xff09;&#xff0c;与引用密切相关…

【vue开发】 父组件传值给子组件时 ,watch props 监听不到解决方案

解决方案&#xff1a; watch:{  data:{ immediate:true,handler:function(){ } }} 示例&#xff1a; 转载于:https://www.cnblogs.com/xiaohuizhang/p/11393444.html

shiro学习(8):shiro连接数据库 三

工具idea 先看看数据库 shiro_role_permission 数据 shiro_user shiro_user_role 数据 我们先看一下目录结构 首先 log4j.properties ### \u914D\u7F6E\u6839 ### log4j.rootLogger error,console ,fileAppender,dailyRollingFile,ROLLING_FILE,MAIL,DATABASE### \u8BBE\u7…

Java 中的 Reference

1、强引用&#xff08;StrongReference&#xff09; 强引用不会被GC回收&#xff0c;并且在java.lang.ref里也没有实际的对应类型。举个例子来说&#xff1a;     Object obj new Object();     这里的obj引用便是一个强引用&#xff0c;不会被GC回收。 2、软引用&…