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

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

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

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;
}   
  1. void addEntry(int hash, K key, V value, int bucketIndex) {
  2. Entry<K,V> e = table[bucketIndex];
  3. table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
  4. if (size++ >= threshold)
  5. resize(2 * table.length);
  6. }
 

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都不允许。

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

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

原文地址:https://www.cnblogs.com/a8457013/p/8290326.html

时间: 2024-11-07 12:37:07

Java集合——HashMap、HashTable以及ConCurrentHashMap异同比较的相关文章

java集合-HashMap

java集合-HashMap HashMap基于哈希表的 Map 接口的实现,以 key-value 的形式存在.在 HashMap 中,key-value 总是会当做一个整体来处理,系统会根据 hash 算法来来计算 key-value 的存储位置,我们总是可以通过 key 快速地存.取 value.下面就来分析 HashMap 的存取. 一.定义 HashMap 实现了 Map 接口,继承 AbstractMap.其中 Map 接口定义了键映射到值的规则,而 AbstractMap 类提供

1.Java集合-HashMap实现原理及源码分析

哈希表(Hash  Table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出现在各类的面试题中,这里对java集合框架中的对应实现HashMap的实现原理进行讲解,然后对JDK7的HashMap的源码进行分析 哈希算法,是一类算法: 哈希表(Hash  Table)是一种数据结构: 哈希函数:是支撑哈希表的一类函数: HashMap 是 Java中用哈希数据结构实现的Ma

Java集合---HashMap源码剖析

无论是在平时的练习还是项目当中,HashMap用的是非常的广,真可谓无处不在.平时用的时候只知道HashMap是用来存储键值对的,却不知道它的底层是如何实现的. 一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同.)此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 值得注意的是HashMap不是线程安全的,如果

[转载] Java集合---HashMap源码剖析

转载自http://www.cnblogs.com/ITtangtang/p/3948406.html 一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同.)此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collection

深入理解JAVA集合系列二:ConcurrentHashMap源码解读

HashMap和Hashtable的区别 在正式开始这篇文章的主题之前,我们先来比较下HashMap和Hashtable之间的差异点: 1.Hashtable是线程安全的,它对外提供的所有方法都是都使用了synchronized,是同步的,而HashMap是非线程安全的. 2.Hashtable不允许value为空,否则会抛出空指针异常: 而HashMap中key.value都可以为空. 1 public synchronized V put(K key, V value) { 2 // Mak

java集合hashmap的使用

java中集合hashmap也是一个集合,可以来存储各中数据. HashMap HashMap中元素的key值不能重复,即彼此调用equals方法,返回为false.排列顺序是不固定的. HashMap的常用方法 put<key,value>->存放对象 get(key); ->获取key所对应的数据. keySet() -> 返回此映射中所包含的键的 set 视图

java集合HashMap、HashTable、HashSet详解

一.Set和Map关系 Set代表集合元素无序,集合元素不可重复的集合,Map代表一种由多个key-value组成的集合,map集合是set集合的扩展只是名称不同,对应如下 二.HashMap的工作原理 HashMap基于hashing原理,通过put()和get()方法储存和获取对象. put()方法: 它调用键对象的hashCode()方法来计算hashcode值,系统根据hashcode值决定该元素在bucket位置.如果两个对象key的hashcode返回值相同,那他们的存储位置相同,如

Java集合HashMap和Hashtable的区别

HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别于HashMap允许空(null)键值(key),由于线程安全,效率上高于Hashtable. HashMap允许将null作为一个entry的key或者value,而Hashtable不允许.HashMap把Hashtable的contains(包含)方法去掉了,改成containsvalue和containsKey.因为contains方法容易让人引起误解. Hashtable继承Dictio

Java集合HashMap集合

基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同.)此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 此实现假定哈希函数将元素正确分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能.迭代集合视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)的和成比例.所以,如果迭代性能很重要,则不要将初始容量设