Java集合包(六)——Map实现类之HashMap、HashTable 原理分析

Java集合包(六)——Map实现类之HashMap、HashTable 原理分析

转载请注明原文地址:https://www.cnblogs.com/ygj0930/p/13565926.html 

一:HashMap特征

  1、HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。

  2、HashMap是无序的。

  3、HashMap是非线程安全的,只适合在单线程环境下使用。

  4、HashMap的key和value允许为空。

  

二:HashMap继承与实现关系

Java集合包(六)——Map实现类之HashMap、HashTable 原理分析

  1、继承层次

java.lang.Object
   ↳     java.util.AbstractMap<K, V>
         ↳     java.util.HashMap<K, V>

  2、类定义与实现

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable { }

  继承AbstractMap,实现了Map、Cloneable、java.io.Serializable接口,具备了通用的键值对操作能力、克隆能力、序列化能力。

三:HashMap原理

  HashMap的重要成员与概念解析

  1、table数组:一个Entry[]数组,每个数组位置称之为一个“桶”,每个桶位都对应一个hash值,每个桶位存储一个Entry实例。而Entry实例实际上是一个单向链表,每个链表节点存储一个键值对。

         在jdk1.8之前,Entry数组中存储的是链表的表头,一个链表存储hash值相同的键值对们。键值对的hash值相同称之为“hash冲突”,解决hash冲突有多种方法,HashMap采用的是拉链法。

        1)拉链法

           这种方法的思路是:把同一个散列槽(在我们这里就是数组的每一个桶位)中的所有元素放到一个链表中,也就是说:把hash值相同的键值对,保存到同一个链表中。

          往HashMap中put一个元素时:首先通过散列函数计算键值对的hash值,得到其在table数组中的桶位。如果该桶位中没有元素,则生成一个Entry实例保存键值对后插入该桶位;

                       如果有元素,则检查key值的唯一性:如果key值和我们要插入的数据不一样,则生成一个Entry实例保存键值对后,插入到该桶位的链表中;如果存在和我们要插入数据相同key的键值对,我们就把value进行更换即可。

        2)再哈希法

          这种方法的思路是:当出现hash值相同的键值对时,进行再次hash计算,直到出现不冲突的位置为止。这样会导致时间成本增加,不采用。

        3)开放地址法

          这种方法的思路是:插入一个元素的时候,先通过哈希函数进行判断,若是发生哈希冲突,就以当前地址为基准,根据再寻址的方法(探查序列),去寻找下一个地址,若发生冲突再去寻找,直至找到一个为空的地址为止。

          所以这种方法又称为再散列法。

       jdk1.8引入了新的实现方式来解决hash冲突,当某个entry链表长度大于8时,会转化成红黑树进行存储;当某个entry树的小于6个元素时,又会转回链表存储。

        这样做的原因是:

        1)原因:
        红黑树的平均查找长度是log(n),当长度为8,查找长度为log(8)=3;

        链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,此时开始有转换成树的必要;

        链表长度如果是小于等于6,6/2=3,因此小于等于6时链表形式的速度已足够快,虽然红黑树的速度会更快,但是转化为树结构也需要时间开销,因此最终性能表现差距不大,无谓再转成红黑树。

        2)选择6和8的原因是:
        中间有个差值7可以防止链表和树之间频繁的转换。

        3)引入红黑树而不是AVL平衡二叉树的原因

        红黑树相比avl树,在检索的时候效率其实差不多,都是通过平衡来二分查找。

        但对于插入删除等操作,红黑树不像avl树一样追求绝对的平衡,他允许局部很少的不完全平衡,省去了很多没有必要的平衡调整操作,因此性能更优。

  2、初始容量:容量 是指哈希表中桶的数量,一个桶存储一个键值对。最大容量必须是2的幂且小于2的30次方,传入容量过大将被这个值替换。

       初始容量 指哈希表在创建时的一开始的容量,默认的初始容量是16,必须是2的幂。

  3、加载因子loadFactor:指触发哈希表扩容的一个当前大小的临界值。

       当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作进行扩容,将容量扩大一倍。

          默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本。

  4、阈值threshold:用于判断是否需要调整HashMap的容量,threshold="容量*加载因子",当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。

  5、size:存储当前键值对的数量,即HashMap的大小。

  6、modCount:实现fail-fast机制的判断依据。

四:HashMap关键方法

  1、遍历键值对

Integer integ = null;
Iterator iter = map.entrySet().iterator();
while(iter.hasNext()) {
    Map.Entry entry = (Map.Entry)iter.next();
    // 获取key
    key = (String)entry.getKey();
        // 获取value
    integ = (Integer)entry.getValue();
}

  第一步:根据entrySet()获取HashMap的“键值对”的Set集合,并获取其迭代器。
  第二步:通过Iterator迭代器遍历键值对的Set集合,并通过entry提供的getKey和getValue方法提取键、值。

  2、遍历键

Iterator iter = map.keySet().iterator();
while (iter.hasNext()) {
        // 获取key
    key = (String)iter.next();
}

  第一步:根据keySet()获取HashMap的“键”的Set集合。
  第二步:通过Iterator迭代器遍历“第一步”得到的集合。

  3、遍历值

Collection c = map.values();
Iterator iter= c.iterator();
while (iter.hasNext()) {
    value = (Integer)iter.next();
}

  第一步:根据value()获取HashMap的“值”的Collection集合。【此处是Collection而不是Set,因为值是允许重复的】
  第二步:通过Iterator迭代器遍历“第一步”得到的集合。

五:HashTable【已过时】

  1)HashTable也是存储键值对和操作键值对的,其底层数据结构原理与HashMap几乎一样,也有容量、加载因子、自动扩容机制等,也是通过拉链法解决哈希冲突,用链表保存键值对。

  2)HashTable的key和value不允许为空,这是与HashMap的不同之处。

  3)HashTable提供的键值对操作方法大多与HashMap一致,不同的地方在于它的方法都被 synchronized 修饰,从而实现线程安全。

  4)HashTable继承了Dictionary类,因此其迭代方式比HashMap多出一种选择:除了使用Iterator迭代器,还可以使用Enumeration枚举类迭代。

  Iterator和Enumeration的不同之处:

  Enumeration:

package java.util;

public interface Enumeration<E> {

    boolean hasMoreElements();

    E nextElement();
}

  Iterator:

package java.util;

public interface Iterator<E> {
    boolean hasNext();

    E next();

    void remove();
}

  可见:

  1)函数接口不同

        Enumeration只有2个函数接口。通过Enumeration,我们只能读取集合的数据,而不能对数据进行修改。
        Iterator只有3个函数接口。Iterator除了能读取集合的数据之外,也能数据进行删除操作。

  2)Iterator支持fail-fast机制,而Enumeration不支持。

  3)Enumeration 比 Iterator 的遍历速度更快

  因为,Iterator添加了对fail-fast机制的支持,所以遍历要慢一点点。