HashMap,HashSet

HashMapHashSet html

摘自:https://www.cnblogs.com/skywang12345/p/3310887.html#a1 java

 

目录算法

1、    HashMap(键值对形式存取,键值不能相同)    2 编程

1.    HashMap的数据结构    2数组

2.    HashMap的存取实现    3缓存

3.    疑问:若是两个key经过hash%Entry[].length获得的 index相同,会不会有覆盖的危险?    4安全

4.    解决hash冲突的方法    5数据结构

5.    Hash冲突是什么?    5多线程

6.    如何解决哈希冲突?    5并发

7.    HashMapput()remove()方法    6

2、    HashSet(是一个没有重复元素的集合)    6

1.    HashSet简介    6

2.    HashSet的数据结构    7

3.    HashSet源码解析(基于JDK1.6.0_45    8

4.    HashSet的遍历方式    12

3、    TreeMapHashMap的区别和共同点    13

4、    HashMapHashTable的区别    14

5、    HashMapHashTable的区别    14

1.    Hashtable的遍历:    15

6、    ConcurrentHashMap的应用    16

1.    concurrentHashMap的优点    16

2.    深刻理解ConcurrentHashMap原理分析即线程安全问题    18

1)    ConcurrentHashMapHashTable的区别    18

2)    ConcurrentHashMap详解    19

7、    HashMapHashTableConcurrentHashMap的区别    21

1.    HashMapConcurrentHashMap的区别    21

2.    ConcurrentHashMap vs Hashtable vs Synchronized Map区别    21

 

 

  1. HashMap(键值对形式存取,键值不能相同)
  1. HashMap的数据结构

    数组的特色是:寻址容易,插入和删除困难。

    链表的特色是:寻址困难,插入和删除容易。

    综合这二者的特性,获得一种寻址容易,插入删除也容易的数据结构:这就是咱们要提起的哈希表。

    哈希表有多种不一样的实现方法,咱们接下来解释的是最经常使用的方法——拉链法,咱们能够理解为"链表的数组":如图:

    从上图咱们能够发现哈希表是由数组+链表组成的,一个长度为16的数组中,每一个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中的呢?通常状况下是经过hash(key)%len得到,也就是元素的key的哈希值对数组的长度取余获得。好比上述哈希表中,12%16=1228%16=12108%16=12140%16=12。因此1228108140都存储在数组下标为12的链表的位置。

    HashMap其实也是一个线性的数组实现的,因此能够理解为其存储数据的容器就是一个线性数组,这可能让咱们很不解,一个线性的数据怎么实现按键值对来存取数据呢?这里HashMap有作一些处理。

    首先HahsMap里面实现了一个静态内部类,其重要的属性有keyvaluenext,从属性keyvalue咱们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,咱们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[]Map里面的内容都保存在Entry[]里面

  2. HashMap的存取实现

    既然是线性数组,为何能随机存取呢?这里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];

这里的话咱们:

对于存储:

  1. 经过hashCode()计算keyhash值;
  2. 经过keyhash值对数组长度取余获得该keyvalue在数组中的下标;
  3. value赋值给Entry[index]实现键值对的存储

对于取值:

  1. 首先也是计算keyhash值;
  2. 计算keyhash值对数组长度取余获得该keyvalue在数组中的下标;
  3. 经过返回return Entry[index]获得键key所对应的值。
  1. 疑问:若是两个key经过hash%Entry[].length获得的 index相同,会不会有覆盖的危险?

    这样占用的内存会很大

    这里HashMap里面用到链式数据结构的一个概念。上面咱们提到过Entry类里面有一个next属性,做用是指向下一个Entry。打个比方,第一个键值对A进来,经过计算其keyhash获得的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里面设置一个因素(也称为因子),随着mapsize愈来愈大,Entry[]会以必定的规则加长长度。

  2. 解决hash冲突的方法
    1. 开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
    2. 再哈希法
    3. 链地址法
    4. 创建一个公共溢出区

    JavahashMap的解决方法就是采用链地址法。

  3. Hash冲突是什么?

    若两个不相等的 key 产生了相等的哈希值,这时则须要采用哈希冲突

    首先,HashMap是由线性数组组成的,如今咱们假设初始数组的长度为5;而后咱们存储数据,假设存储的第一个数据的键值的hashcode计算出来的值为6,而后咱们经过hashcode计算出来的值与数组长度取余获得存储第一个数据的下标,即6%5=1;当咱们存储另外的数据,若是经过键值的hashcode计算出来的值是11,那么此时计算出数据的下标11%5=1也是1。这就是哈希冲突。

  4. 如何解决哈希冲突?

    Java采用拉链法解决哈希冲突。

    1. 获得一个key
    2. 计算keyhashValue
    3. 根据 hashValue 值定位到 data[hashValue] ( data[hashValue] 是一条链表)
    4. data[hashValue] 为空则直接插入,否则则添加到链表头部。
  5. HashMapput()remove()方法

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

 

  • 两次插入的键相同时不是哈希冲突

    方法:则直接更新该键的值

  • 两次插入的键不一样时,可是获得相同的hashValue时,是哈希冲突

    方法:将值插入到单链表的头结点。

  • 删除关键字值为k的记录,应先在该关键字值的哈希地址处的单链表中找到该记录,而后删除之。

     

  1. HashSet(是一个没有重复元素的集合)
  1. HashSet简介

    HashSet是一个没有重复元素的集合,它是由HashMap实现的,不保证元素的顺序,并且HashSet容许使用null元素。

    HashSet是非同步的,若是多个线程同时访问一个HashSet,而其中至少一个线程修改了该set,那么它必须保持外部同步。这一般是经过对天然封装该set的对象执行同步操做来完成的。若是不存在这样的对象,则应该使用Collections.synchronizedSet方法来包装set,最好在建立完成时完成这一操做,以防止对该set进行意外的不一样步访问:

    Set s = Collections.synchronizedSet(new HashSet(...));

    HashSet经过iterator()迭代器进行访问。

  2. HashSet的数据结构

    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 { }

    从上图能够看出:

    1. HashSet继承与AbstractSet,而且实现了Set接口
    2. HashSet的本质是一个"没有重复元素"的集合,它是经过HashMap实现的。HashSet中含有一个"HashMap类型的成员变量"mapHashSet的操做函数,实际上都是经过map实现的。
  3. HashSet源码解析(基于JDK1.6.0_45

    为了更了解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,而HashMapkey-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就很是容易了。

  1. HashSet的遍历方式
    1. 经过Iterator遍历HashSet

第一步:根据iterator()获取HashSet的迭代器

遍历迭代器获取各个元素

// 假设set是HashSet对象

for(Iterator iterator = set.iterator();

iterator.hasNext(); ) {

iterator.next();

}

  1. 经过for-each遍历HashSet
  • 第一步:根据toArray()获取HashSet的元素集合对应的数组
  • 遍历数组,获取各个元素

// 假设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

  1. TreeMapHashMap的区别和共同点

     

TreeMap

HashMap

TreeMap实现了SortMap接口,是基于红黑树的

HashMap实现了Map接口,是基于哈希散列表的

TreeMap默认按键的升序排序

HashMap随机存储

TreeMap的遍历是Iterator按顺序遍历的

HahsMap的遍历是Iterator随机遍历的

TreeMap键和值都不能为空

HashMap键只能有一个null,值能够有多个null

TreeMap插入删除查找的效率比较低

HashMap插入删除查找的效率比较高

非线程安全的

非线程安全的

 

  1. HashMapHashTable的区别

    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值(不会返回truefalse

HashSet使用成员对象来计算hashcode值,对于两个对象来讲hashcode可能相同,因此equals()方法用来判断对象的相等性,若是两个对象不一样的话,那么返回false

HashMap查找比较快,由于是使用惟一的键来获取对象

HashSetHashMap来讲比较慢

 

  1. HashMapHashTable的区别

HashMap

HashTable

HashMap是基于AbstractMap

HashTable基于Dictionary

HashMap能够容许存在一个为nullkey和任意个为nullvalue

HashTable中的keyvalue都不容许为null

HashMap时单线程安全的,多线程是不安全的

Hashtable是多线程安全的

HashMap仅支持Iterator的遍历方式

Hashtable支持IteratorEnumeration两种遍历方式

Hashtable 的函数都是同步的,这意味着它是线程安全的。它的keyvalue都不能够为nullHashtable的方法都用synchronized来修饰,因此它是线程同步的。 
  1. Hashtable的遍历:
    1. 遍历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(); 
}

 

  1. 经过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);

}

 

  1. 经过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();

}

 

  1. 经过Enumeration遍历Hashtable的键
  • 第一步:根据keys()获取Hashtable的集合。
  • 第二步:经过Enumeration遍历"第一步"获得的集合

Enumeration enu = table.elements();

while(enu.hasMoreElements()) {

System.out.println(enu.nextElement());

}

 

  1. 经过Enumeration遍历Hashtable的值
  • 第一步:根据elements()获取Hashtable的集合。
  • 第二步:经过Enumeration遍历"第一步"获得的集合

Enumeration enu = table.elements();

while(enu.hasMoreElements()) {

System.out.println(enu.nextElement());

}

 

  1. ConcurrentHashMap的应用
  1. concurrentHashMap的优点

    首先经常使用的三种HashMap包括HashMapHashTableconcurrentHashMap

    1. HashMap在并发编程过程当中使用可能致使死循环,由于插入过程不是原子操做,每一个HashEntry是一个链表节点,极可能在插入的过程当中,已经设置了后节点,实际还未插入,最终反而插入在后节点以后,形成链中出现环,破坏了链表的性质,失去了尾节点,出现死循环。
    2. HashTable由于内部是采用synchronized来保证线程安全的,但在线程竞争激烈的状况下HashTable的效率降低得很快由于synchronized关键字会形成代码块或方法成为为临界区(对同一个对象加互斥锁),当一个线程访问临界区的代码时,其余线程也访问同一临界区时,会进入阻塞或轮询状态。究其缘由,其实是有获取锁意向的线程的数目增长,可是锁仍是只有单个,致使大量的线程处于轮询或阻塞,致使同一时间段有效执行的线程的增量远不及线程整体增量。
    3. 在查询时,尤为可以体现出CocurrentHashMap在效率上的优点,HashTable使用Sychronized关键字,会致使同时只能有一个查询在执行,而Cocurrent则不采起加锁的方法,而是采用volatile关键字,虽然也会牺牲效率,可是因为Sychronized,于该文末尾继续讨论。
    4. CocurrentHashMap利用锁分段技术增长了锁的数目,从而使争夺同一把锁的线程的数目获得控制。
  • 锁分段技术就是对数据集进行分段,每段竞争一把锁,不一样数据段的数据不存在锁竞争,从而有效提升高并发访问效率。
  • CocurrentHashMapget方法是无需加锁的,由于用到的共享变量都采用volatile关键字修饰,巴证共享变量在线程之间的可见性(每次读取都先同步缓存和内存,直接从内存中获取值,虽然不是原子操做,但根据JAVA内存模型的happen before原则,对volatile字段的写入操做先于读操做,可以保证不会脏读),volatile为了让变量提供线程之间的内存可见性,会禁止程序执行结果的重排序(致使缓存优化的效果下降)
  1. 深刻理解ConcurrentHashMap原理分析即线程安全问题
  1. ConcurrentHashMapHashTable的区别

HashTable put()源代码

从代码能够看出来在全部put 的操做的时候都须要用 synchronized 关键字进行同步。而且key 不能为空。

这样至关于每次进行put 的时候都会进行同步10个线程同步进行操做的时候,就会发现当第一个线程进去其余线程必须等待第一个线程执行完成,才能够进行下去。性能特别差。

 

  1. ConcurrentHashMap详解

    分段锁技术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而且都有本身锁,只要修改再也不同一个段上就不会引发并发问题

 

  1. HashMapHashTableConcurrentHashMap的区别
  1. HashMapConcurrentHashMap的区别
  • 他们之间的第一个重要的区别就是ConcurrentHashMap是线程安全的和在并发环境下不须要加额外的同步。
  • 你可使用Collections.synchronizedMap(HashMap)来包装HashMap做为同步容器,这时它的做用几乎与Hashtable同样,当每次对Map作修改操做的时候都会锁住这个Map对象,而ConcurrentHashMap会基于并发的等级来划分整个Map来达到线程安全,它只会锁操做的那一段数据而不是整个Map都上锁。
  • ConcurrentHashMap有很好的扩展性,在多线程环境下性能方面比作了同步的HashMap要好,可是在单线程环境下,HashMap会比ConcurrentHashMap好一点。
  1. ConcurrentHashMap vs Hashtable vs Synchronized Map区别

    虽然三个集合类在多线程并发应用中都是线程安全的,可是他们有一个重大的差异,就是他们各自实现线程安全的方式。

  • Hashtablejdk1的一个遗弃的类,它把全部方法都加上synchronized关键字来实现线程安全,全部的方法都同步这样形成多个线程访问效率特别低。
  • Synchronized MapHashTable差异不大,也是在并发中做相似的操做,二者的惟一区别就是Synchronized Map没被遗弃,它能够经过使用Collections.synchronizedMap()来包装Map做为同步容器使用。
  • ConcurrentHashMap的设计有点特别,表如今多个线程操做上。ConcurrentHashMap不须要锁整个Map,相反它划分了多个段(segments),要操做哪一段才上锁那段数据。
相关文章
相关标签/搜索