HashMap,HashSet html
摘自:https://www.cnblogs.com/skywang12345/p/3310887.html#a1 java
目录算法
1、 HashMap(键值对形式存取,键值不能相同) 2 编程
3. 疑问:若是两个key经过hash%Entry[].length获得的 index相同,会不会有覆盖的危险? 4安全
4. 解决hash冲突的方法 5数据结构
3. HashSet源码解析(基于JDK1.6.0_45) 8
2. 深刻理解ConcurrentHashMap原理分析即线程安全问题 18
1) ConcurrentHashMap与HashTable的区别 18
7、 HashMap,HashTable和ConcurrentHashMap的区别 21
1. HashMap与ConcurrentHashMap的区别 21
2. ConcurrentHashMap vs Hashtable vs Synchronized Map区别 21
数组的特色是:寻址容易,插入和删除困难。
链表的特色是:寻址困难,插入和删除容易。
综合这二者的特性,获得一种寻址容易,插入删除也容易的数据结构:这就是咱们要提起的哈希表。
哈希表有多种不一样的实现方法,咱们接下来解释的是最经常使用的方法——拉链法,咱们能够理解为"链表的数组":如图:
从上图咱们能够发现哈希表是由数组+链表组成的,一个长度为16的数组中,每一个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中的呢?通常状况下是经过hash(key)%len得到,也就是元素的key的哈希值对数组的长度取余获得。好比上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。因此12,28,108,140都存储在数组下标为12的链表的位置。
HashMap其实也是一个线性的数组实现的,因此能够理解为其存储数据的容器就是一个线性数组,这可能让咱们很不解,一个线性的数据怎么实现按键值对来存取数据呢?这里HashMap有作一些处理。
首先HahsMap里面实现了一个静态内部类,其重要的属性有key,value,next,从属性key,value咱们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,咱们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。
既然是线性数组,为何能随机存取呢?这里HashMap用了一个小算法,大体HashMap 采用一种所谓的"Hash 算法"来决定每一个元素的存储位置。当程序执行 map.put(String,Obect)方法时,系统将调用String的 hashCode() 方法获得其 hashCode 值——每一个 Java 对象都有 hashCode() 方法,均可经过该方法得到它的 hashCode 值。获得这个对象的 hashCode 值以后,系统会根据该 hashCode 值来决定该元素的存储位置。是这样实现:
//存储时: int hash = key.hashCode();// 这个hashCode方法这里不详述,只要理解每一个key的hash是一个固定的int值 int index = hash % Entry[].length; Entry[index] = value;
//取值时: int hash = key.hashCode(); int index = hash % Entry[].length; return Entry[index]; |
这里的话咱们:
对于存储:
对于取值:
这样占用的内存会很大
这里HashMap里面用到链式数据结构的一个概念。上面咱们提到过Entry类里面有一个next属性,做用是指向下一个Entry。打个比方,第一个键值对A进来,经过计算其key的hash获得的index=0,记做:Entry[0]=A。一会又进来一个键值对B,经过计算其index也等于0;如今怎么办?HashMap会这样作:B.next = A,Entry[0] = B,若是又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样咱们发现index=0的地方其实存取了A,B,C三个键值对,他们经过next这个属性连接在一块儿。因此疑问不用担忧。也就是说数组中存储的是最后插入的元素。到这里为止,HashMap的大体实现,咱们应该已经清楚了。
固然HashMap里面也包含一些优化方面的实现,这里也说一下。好比:Entry[]的长度必定后,随着map里面数据的愈来愈长,这样同一个index的链就会很长,会不会影响性能?HashMap里面设置一个因素(也称为因子),随着map的size愈来愈大,Entry[]会以必定的规则加长长度。
Java中hashMap的解决方法就是采用链地址法。
若两个不相等的 key 产生了相等的哈希值,这时则须要采用哈希冲突。
首先,HashMap是由线性数组组成的,如今咱们假设初始数组的长度为5;而后咱们存储数据,假设存储的第一个数据的键值的hashcode计算出来的值为6,而后咱们经过hashcode计算出来的值与数组长度取余获得存储第一个数据的下标,即6%5=1;当咱们存储另外的数据,若是经过键值的hashcode计算出来的值是11,那么此时计算出数据的下标11%5=1也是1。这就是哈希冲突。
Java采用拉链法解决哈希冲突。
HashMap<String, Integer> map = new HashMap<String, Integer>(); map.put("wang", 01); map.put("wang",02); System.out.println(map.get("wang")); System.out.println("----------------"); map.remove("wang"); System.out.println(map.get("wang")); |
*************** 2 ---------------- null |
方法:则直接更新该键的值
方法:将值插入到单链表的头结点。
HashSet是一个没有重复元素的集合,它是由HashMap实现的,不保证元素的顺序,并且HashSet容许使用null元素。
HashSet是非同步的,若是多个线程同时访问一个HashSet,而其中至少一个线程修改了该set,那么它必须保持外部同步。这一般是经过对天然封装该set的对象执行同步操做来完成的。若是不存在这样的对象,则应该使用Collections.synchronizedSet方法来包装set,最好在建立完成时完成这一操做,以防止对该set进行意外的不一样步访问:
Set s = Collections.synchronizedSet(new HashSet(...)); |
HashSet经过iterator()迭代器进行访问。
HashSet的继承关系以下:
java.lang.Object ↳ java.util.AbstractCollection<E> ↳ java.util.AbstractSet<E> ↳ java.util.HashSet<E>
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { } |
从上图能够看出:
为了更了解HashSet的原理,下面对HashSet源码代码做出分析。
package java.util;
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { static final long serialVersionUID = -5024744406713321676L;
// HashSet是经过map(HashMap对象)保存内容的 private transient HashMap<E,Object> map;
// PRESENT是向map中插入key-value对应的value // 由于HashSet中只须要用到key,而HashMap是key-value键值对; // 因此,向map中添加键值对时,键值对的值固定是PRESENT private static final Object PRESENT = new Object();
// 默认构造函数 public HashSet() { // 调用HashMap的默认构造函数,建立map map = new HashMap<E,Object>(); }
// 带集合的构造函数 public HashSet(Collection<? extends E> c) { // 建立map。 // 为何要调用Math.max((int) (c.size()/.75f) + 1, 16),从 (c.size()/.75f) + 1 和 16 中选择一个比较大的树呢? // 首先,说明(c.size()/.75f) + 1 // 由于从HashMap的效率(时间成本和空间成本)考虑,HashMap的加载因子是0.75。 // 当HashMap的"阈值"(阈值=HashMap总的大小*加载因子) < "HashMap实际大小"时, // 就须要将HashMap的容量翻倍。 // 因此,(c.size()/.75f) + 1 计算出来的正好是总的空间大小。 // 接下来,说明为何是 16 。 // HashMap的总的大小,必须是2的指数倍。若建立HashMap时,指定的大小不是2的指数倍; // HashMap的构造函数中也会从新计算,找出比"指定大小"大的最小的2的指数倍的数。 // 因此,这里指定为16是从性能考虑。避免重复计算。 map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16)); // 将集合(c)中的所有元素添加到HashSet中 addAll(c); }
// 指定HashSet初始容量和加载因子的构造函数 public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<E,Object>(initialCapacity, loadFactor); }
// 指定HashSet初始容量的构造函数 public HashSet(int initialCapacity) { map = new HashMap<E,Object>(initialCapacity); }
HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor); }
// 返回HashSet的迭代器 public Iterator<E> iterator() { // 实际上返回的是HashMap的"key集合的迭代器" return map.keySet().iterator(); }
public int size() { return map.size(); }
public boolean isEmpty() { return map.isEmpty(); }
public boolean contains(Object o) { return map.containsKey(o); }
// 将元素(e)添加到HashSet中 public boolean add(E e) { return map.put(e, PRESENT)==null; }
// 删除HashSet中的元素(o) public boolean remove(Object o) { return map.remove(o)==PRESENT; }
public void clear() { map.clear(); }
// 克隆一个HashSet,并返回Object对象 public Object clone() { try { HashSet<E> newSet = (HashSet<E>) super.clone(); newSet.map = (HashMap<E, Object>) map.clone(); return newSet; } catch (CloneNotSupportedException e) { throw new InternalError(); } }
// java.io.Serializable的写入函数 // 将HashSet的"总的容量,加载因子,实际容量,全部的元素"都写入到输出流中 private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { // Write out any hidden serialization magic s.defaultWriteObject(); // Write out HashMap capacity and load factor s.writeInt(map.capacity()); s.writeFloat(map.loadFactor()); // Write out size s.writeInt(map.size()); // Write out all elements in the proper order. for (Iterator i=map.keySet().iterator(); i.hasNext(); ) s.writeObject(i.next()); } // java.io.Serializable的读取函数 // 将HashSet的"总的容量,加载因子,实际容量,全部的元素"依次读出 private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // Read in any hidden serialization magic s.defaultReadObject();
// Read in HashMap capacity and load factor and create backing HashMap int capacity = s.readInt(); float loadFactor = s.readFloat(); map = (((HashSet)this) instanceof LinkedHashSet ? new LinkedHashMap<E,Object>(capacity, loadFactor) : new HashMap<E,Object>(capacity, loadFactor)); // Read in size int size = s.readInt(); // Read in all elements in the proper order. for (int i=0; i<size; i++) { E e = (E) s.readObject(); map.put(e, PRESENT); } } } |
说明: HashSet的代码实际上很是简单,经过上面的注释应该很可以看懂。它是经过HashMap实现的,若对HashSet的理解有困难,建议先学习如下HashMap;学完HashMap以后,在学习HashSet就很是容易了。
第一步:根据iterator()获取HashSet的迭代器
遍历迭代器获取各个元素
// 假设set是HashSet对象 for(Iterator iterator = set.iterator(); iterator.hasNext(); ) { iterator.next(); } |
// 假设set是HashSet对象,而且set中元素是String类型 String[] arr = (String[])set.toArray(new String[0]); for (String str:arr) System.out.printf("for each : %s\n", str); |
向HashSet中添加元素,若是set中元素已存在,则返回false;若是不存在,则返回true。
TreeMap |
HashMap |
TreeMap实现了SortMap接口,是基于红黑树的 |
HashMap实现了Map接口,是基于哈希散列表的 |
TreeMap默认按键的升序排序 |
HashMap随机存储 |
TreeMap的遍历是Iterator按顺序遍历的 |
HahsMap的遍历是Iterator随机遍历的 |
TreeMap键和值都不能为空 |
HashMap键只能有一个null,值能够有多个null |
TreeMap插入删除查找的效率比较低 |
HashMap插入删除查找的效率比较高 |
非线程安全的 |
非线程安全的 |
在HashSet中,元素都存到HashMap键值对的Key上面,而Value时有一个统一的值private static final Object PRESENT = new Object();,
当有新值加入时,底层的HashMap会判断Key值是否存在(HashMap细节请移步深刻理解HashMap),若是不存在,则插入新值,同时这个插入的细节会依照HashMap插入细节;若是存在就不插入
HashMap |
HashSet |
HashMap实现了Map接口 |
HashSet实现了Set接口 |
HashMap存储键值对 |
HashSet仅仅存储对象,存储的是键,他们的值是相同的。 |
使用pub()方法将元素放入map中 |
使用add()方法将元素放入set中 |
HashMap中使用键对象来计算hashcode值(不会返回true和false) |
HashSet使用成员对象来计算hashcode值,对于两个对象来讲hashcode可能相同,因此equals()方法用来判断对象的相等性,若是两个对象不一样的话,那么返回false |
HashMap查找比较快,由于是使用惟一的键来获取对象 |
HashSet较HashMap来讲比较慢 |
HashMap |
HashTable |
HashMap是基于AbstractMap |
HashTable基于Dictionary类 |
HashMap能够容许存在一个为null的key和任意个为null的value |
HashTable中的key和value都不容许为null |
HashMap时单线程安全的,多线程是不安全的 |
Hashtable是多线程安全的 |
HashMap仅支持Iterator的遍历方式 |
Hashtable支持Iterator和Enumeration两种遍历方式 |
Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不能够为null。Hashtable的方法都用synchronized来修饰,因此它是线程同步的。
Hashtable的遍历:
遍历Hashtable的键值对
第一步:根据entrySet()获取Hashtable的"键值对"的Set集合。
经过Iterator迭代器遍历"第一步"获得的集合。
// 假设table是Hashtable对象 // table中的key是String类型,value是Integer类型 Integer integ = null; Iterator iter = table.entrySet().iterator(); while(iter.hasNext()) { Map.Entry entry = (Map.Entry)iter.next(); // 获取key key = (String)entry.getKey(); // 获取value integ = (Integer)entry.getValue(); } |
经过Iterator遍历Hashtable的键
第一步:根据keySet()获取Hashtable的"键"的Set集合。
第二步:经过Iterator迭代器遍历"第一步"获得的集合。
// 假设table是Hashtable对象 // table中的key是String类型,value是Integer类型 String key = null; Integer integ = null; Iterator iter = table.keySet().iterator(); while (iter.hasNext()) { // 获取key key = (String)iter.next(); // 根据key,获取value integ = (Integer)table.get(key); } |
经过Iterator遍历Hashtable的值
第一步:根据value()获取Hashtable的"值"的集合。
第二步:经过Iterator迭代器遍历"第一步"获得的集合
// 假设table是Hashtable对象 // table中的key是String类型,value是Integer类型 Integer value = null; Collection c = table.values(); Iterator iter= c.iterator(); while (iter.hasNext()) { value = (Integer)iter.next(); } |
经过Enumeration遍历Hashtable的键
第一步:根据keys()获取Hashtable的集合。
第二步:经过Enumeration遍历"第一步"获得的集合
Enumeration enu = table.elements(); while(enu.hasMoreElements()) { System.out.println(enu.nextElement()); } |
经过Enumeration遍历Hashtable的值
第一步:根据elements()获取Hashtable的集合。
第二步:经过Enumeration遍历"第一步"获得的集合
Enumeration enu = table.elements(); while(enu.hasMoreElements()) { System.out.println(enu.nextElement()); } |
首先经常使用的三种HashMap包括HashMap,HashTable和concurrentHashMap:
HashTable 的put()源代码
从代码能够看出来在全部put 的操做的时候都须要用 synchronized 关键字进行同步。而且key 不能为空。
这样至关于每次进行put 的时候都会进行同步当10个线程同步进行操做的时候,就会发现当第一个线程进去其余线程必须等待第一个线程执行完成,才能够进行下去。性能特别差。
分段锁技术:ConcurrentHashMap相比 HashTable而言解决的问题就是的它不是锁所有数据,而是锁一部分数据,这样多个线程访问的时候就不会出现竞争关系。不须要排队等待了。
从图中能够看出来ConcurrentHashMap的主干是个Segment数组。、
它把区间按照并发级别(concurrentLevel),分红了若干个segment。默认状况下内部按并发级别为16来建立。对于每一个segment的容量,默认状况也是16。
ConcurrentHashMap是由Segment数组和HashEntry数组组成.
Segment是一种可重入锁,在ConcurrentHashMap里扮演锁的角色;
HashEntry则用于存储键值对数据.
一个ConcurrentHashMap里包含一个Segment数组.
Segment的结构和HashMap相似,是一种数组和链表结构.
一个Segment里包含一个HashEntry数组,每一个HashEntry是一个链表结构的元素,每一个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,
必须首先得到与它对应的Segment锁
这就是为何ConcurrentHashMap支持容许多个修改同时并发进行,缘由就是采用的Segment分段锁功能,每个Segment 都想的于小的hash table而且都有本身锁,只要修改再也不同一个段上就不会引发并发问题。
虽然三个集合类在多线程并发应用中都是线程安全的,可是他们有一个重大的差异,就是他们各自实现线程安全的方式。