首页 > 其他 > 详细

HashMap的工作原理

时间:2019-05-27 00:07:16      阅读:88      评论:0      收藏:0      [点我收藏+]

标签:cas   属性。   使用   final   通过   res   定位   函数分析   新的   

  

  HashMap基于hashing原理,我们通过put()get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。

 

1.HashMap介绍

 

  HashMapMap接口的一个实现类,实现了所有Map的操作。HashMap除了允许keyvalue保存null值和非线程安全外,其他实现几乎和HashTable一致。

 

  HashMap使用散列存储的方式保存kay-value键值对,因此其不支持数据保存的顺序。如果想要使用有序容器可以使用LinkedHashMap

 

  在性能上当HashMap中保存的key的哈希算法能够均匀的分布在每个bucket中的是时候,HashMap在基本的getset操作的的时间复杂度都是O(n)

 

  在遍历HashMap的时候,其遍历节点的个数为bucket的个数+HashMap中保存的节点个数。因此当遍历操作比较频繁的时候需要注意HashMap的初始化容量不应该太大。 这一点其实比较好理解:当保存的节点个数一致的时候,bucket越少,遍历次数越少。

 

  另外HashMapresize的时候会有很大的性能消耗,因此当需要在保存HashMap中保存大量数据的时候,传入适当的默认容量以避免resize可以很大的提高性能。 具体的resize操作请参考下面对此方法的分析

 

  HashMap是非线程安全的类,当作为共享可变资源使用的时候会出现线程安全问题。需要使用线程安全容器:

 

  Map m = new ConcurrentHashMap();或者

 

  Map m = Collections.synchronizedMap(new HashMap());

 

  

2.数据结构介绍

 

  HashMap使用数组+链表+树形结构的数据结构。

 

3.HashMap源码分析(基于JDK1.8

 

3.1关键属性分析

 

transient Node<K,V>[] table; //Node类型的数组,记我们常说的bucket数组,其中每个元素为链表或者树形结构

 

transient int size;//HashMap中保存的数据个数

 

int threshold;//HashMap需要resize操作的阈值

 

final float loadFactor;//负载因子,用于计算threshold。计算公式为:threshold = loadFactor * capacity

 

其中还有一些默认值得属性,有默认容量2^4,默认负载因子0.75.用于构造函数没有指定数值情况下的默认值。

 

 3.2构造函数分析

 

HashMap提供了三个不同的构造函数,主要区别为是否传入初始化容量和负载因子。分别文以下三个。

 

//此构造函数创建一个空的HashMap,其中负载因子为默认值0.75

 

public HashMap() {

 

    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted

 

}

 

//传入默认的容量大小,创造一个指定容量大小和默认负载因子为0.75HashMap

 

 public HashMap(int initialCapacity) {

 

    this(initialCapacity, DEFAULT_LOAD_FACTOR);

 

}

 

//创建一个指定容量和指定负载因为HashMap,以下代码删除了入参检查

 

public HashMap(int initialCapacity, float loadFactor) {

 

    this.loadFactor = loadFactor;

 

    this.threshold = tableSizeFor(initialCapacity);

 

}

 

注意:此处的initialCapacity为数组table的大小,即bucket的个数。

 

其中在指定初始化容量的时候,会根据传入的参数来确定HashMap的容量大小。

 

初始化this.threshold的值为入参initialCapacity距离最近的一个2n次方的值。取值方法如下:

 

case initialCapacity = 0:  

 

     this.threshold = 1;       

 

case initialCapacity为非0且不为2n次方:  

 

    this.threshold = 大于initialCapacity中第一个2n次方的数。  

 

case initialCapacity = 2^n:  

 

    this.threshold = initialCapacity

 

具体的计算方法为tableSizeFor(int cap)函数。计算方法是将入参的最高位下面的所有位都设置为1,然后加1

 

下面以入参为134217729为例分析计算过程。

 

首先将int转换为二进制如下:

 

cap = 0000 1000 0000 0000 0000 0000 0000 0001

 

另外此处赋值为this.threshold,是因为构造函数的时候并不会创建table,只有实际插入数据的时候才会创建。目的应该是为了节省内存空间吧。

 

在第一次插入数据的时候,会将tablecapacity设置为threshold,同时将threshold更新为loadFactor * capacity

 

3.3关键函数源码分析

 

3.3.1 第一次插入数据的操作

 

HashMap在插入数据的时候传入key-value键值对。使用hash寻址确定保存数据的bucket。当第一次插入数据的时候会进行HashMap中容器的初始化。具体操作如下:

 

Node<K,V>[] tab;


        int n, i;


        if ((tab = table) == null || (n = tab.length) == 0)


            n = (tab = resize()).length;

 

 其中resize函数的源码如下,主要操作为根据caploadFactory创建初始化table

 

Node<K, V>[] oldTab = table;


    int oldThr = threshold;  //oldThr 根据传入的初始化cap决定 2的n次方


    int newCap, newThr = 0;


    if (oldThr > 0) // 当构造函数中传入了capacity的时候


        newCap = oldThr;  //newCap = threshold  2的n次方,即构造函数的时候的初始化容量


     else {               // zero initial threshold signifies using defaults


        newCap = DEFAULT_INITIAL_CAPACITY;


        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);


    }


    float ft = (float)newCap * loadFactor;  // 2的n次方 * loadFactory


    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?


            (int)ft : Integer.MAX_VALUE);


    threshold = newThr; //新的threshold== newCap * loadFactory


    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //长度为2的n次方的数组


    table = newTab;

 

在初始化table之后,将数据插入到指定位置,其中bucket的确定方法为:

 

i = (n - 1) & hash // 此处n-1必定为 0000 1111 1111....的格式,取&操作之后的值一定在数组的容量范围内。


其中hash的取值方式为:


 static final int hash(Object key) {


    int h;


    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);


}

 

具体操作如下,创建Node并将node放到table的第i个元素中

 

if ((p = tab[i = (n - 1) & hash]) == null)


    tab[i] = new Node(hash, key, value, null);

 

3.3.2 非第一次插入数据的操作源码分析

 

HashMap中已有数据的时候,再次插入数据,会多出来在链表或者树中寻址的操作,和当size到达阈值时候的resize操作。多出来的步骤如下:

 

另外,在resize操作中也和第一次插入数据的操作不同,当HashMap不为空的时候resize操作需要将之前的数据节点复制到新的table中。操作如下:

 

3.4CloneableSerializable分析

 

HashMap的定义中实现了Cloneable接口,Cloneable是一个标识接口,主要用来标识 Object.clone()的合法性,在没有实现此接口的实例中调用 Object.clone()方法会抛出CloneNotSupportedException异常。可以看到HashMap中重写了clone方法。

 

HashMap实现Serializable接口主要用于支持序列化。同样的Serializable也是一个标识接口,本身没有定义任何方法和属性。另外HashMap自定义了

 

private void writeObject(java.io.ObjectOutputStream s) throws IOException


private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException

 

两个方法实现了自定义序列化操作。

 

注意:支持序列化的类必须有无参构造函数。这点不难理解,反序列化的过程中需要通过反射创建对象。

4.HashMap的遍历

以下讨论两种遍历方式,测试代码如下:

方法一:

通过map.keySet()获取key的集合,然后通过遍历key的集合来遍历map

方法二:

通过map.entrySet()方法获取map中节点集合,然后遍历此集合遍历map

测试代码如下:

public static void main(String[] args) throws Exception {

        Map<String, Object> map = new HashMap<>();

        map.put("name", "test");

        map.put("age", "25");

        map.put("address", "HZ");

        Set<String> keySet = map.keySet();

        for (String key : keySet) {

            System.out.println(map.get(key));

        }

        Set<Map.Entry<String, Object>> set = map.entrySet();

        for (Map.Entry<String, Object> entry : set) {

            System.out.println("key is : " + entry.getKey() + ".  value is " + entry.getValue());

        }

    }

 

HashMap 1.7 与 1.8 的 区别,说明 1.8 做了哪些优化,如何优化的?

Hashmap的结构,1.71.8有哪些区别

不同点:

1JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。

2)扩容后数据存储位置的计算方式也不一样:

1. JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash& length-1

2、而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。

3JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从On)变成OlogN)提高了效率)

 

HashMap的工作原理

标签:cas   属性。   使用   final   通过   res   定位   函数分析   新的   

原文:https://www.cnblogs.com/heqiyoujing/p/10928318.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 bubuko.com 版权所有 鲁ICP备09046678号-4
打开技术之扣,分享程序人生!
             

鲁公网安备 37021202000002号