hashMap源码中那些你不注意的事

  首先来说一个非常形象的例子,来说明下数组和链表。


  上体育课的时候,老师说:你们站一队,每个人记住自己是第几个,我喊到几,那个人就举手,这就是数组
  老师说,你们每个人记住自己前面的人和后面的人,然后老师只知道第一人是谁。 然后你们各自由活动,老师要找某一个人,是不是每次都是从第一个开始往自己身后的人开始传达?这就是链表
  老师说: 大家1,2,3,4报数,凡是报1,为1队,凡是报2的为2队.......  这就是散列(哈希)。而这个4就相当于预定义好的桶的个数。

 

程序中,存放指定的数据最常用的数据结构有两种:数组和链表。

数组和链表的区别:
  1、  数组是将元素在内存中连续存放。
         链表中的元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起。
  2、  数组必须事先定义固定的长度,不能适应数据动态的增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费;
    链表动态地进行存储分配,可以适应数据动态地增减的情况。
  3、(静态)数组从栈中分配空间,对于程序员方便快速,但是自由度小;
    链表从堆中分配空间,自由度大但是申请管理比较麻烦。

 


数组和链表在存储数据方面到底谁好?根据数组和链表的特性,分两种情况讨论:
  1、当进行数据查询时,数组可以直接通过下标迅速访问数组中的元素。
        而链表则需要从第一个元素开始一直找到需要的元素位置,
        显然,数组的查询效率会比链表的高。

  2、当进行增加或删除元素时,在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。

             同样,如果想删除一个元素,需要移动大量去填掉被移动的元素,而链表只需改动元素中的指针即可实现增加或删除元素。
 

 

 


  那么哈希表,是既能具备数组的快速查询的优点,又能融合链表方便快捷的增加删除元素的优势。
  所谓的hash,简单的说就是散列,即将输入的数据通过hash函数得到一个key值,输入的数据存储到数组中下标的key值的数组单元中去。 

 

 

 

 

  Java中数据存储方式最底层的两种结构,一种是数组,另一种就是链表。

  数组的特点:连续空间,寻址迅速,但是在删除或者添加元素的时候需要有较大幅度的移动,所以查询速度快,增删较慢。

  而链表正好相反,由于空间不连续,寻址困难,增删元素只需修改指针,所以查询慢、增删快。

  有没有一种数据结构来综合一下数组和链表,以便发挥他们各自的优势?答案是肯定的!就是:哈希表

  哈希表具有较快(常量级)的查询速度,及相对较快的增删速度,所以很适合在海量数据的环境中使用。一般实现哈希表的方法采用“拉链法”,我们可以理解为“链表的数组”,如下图:

  哈希表。如拿HashMap来说。

 

  从上图中,我们可以发现哈希表是由数组  +   链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。它的内部其实是用一个Entity数组来实现的,属性有key、value、next。

 

在Java 8 之前,HashMap和其他基于map的类都是通过链地址法解决冲突,它们使用单向链表来存储相同索引值的元素。在最坏的情况下,这种方式会将HashMap的get方法的性能从O(1)降低到O(n)。为了解决在频繁冲突时hashmap性能降低的问题,Java 8中使用平衡树来替代链表存储冲突的元素。这意味着我们可以将最坏情况下的性能从O(n)提高到O(logn)。在Java 8中使用常量TREEIFY_THRESHOLD来控制是否切换到平衡树来存储。目前,这个常量值是8,这意味着当有超过8个元素的索引一样时,HashMap会使用树来存储它们。这一改变是为了继续优化常用类。在Java 7中为了优化常用类对ArrayList和HashMap采用了延迟加载的机制,在有元素加入之前不会分配内存,这会减少空的链表和HashMap占用的内存。这一动态的特性使得HashMap一开始使用链表,并在冲突的元素数量超过指定值时用平衡二叉树替换链表。不过这一特性在所有基于hash table的类中并没有,例如Hashtable和WeakHashMap。目前,只有ConcurrentHashMap,LinkedHashMapHashMap会在频繁冲突的情况下使用平衡树。

什么时候会产生冲突

HashMap中调用hashCode()方法来计算hashCode。由于在Java中两个不同的对象可能有一样的hashCode,所以不同的键可能有一样hashCode,从而导致冲突的产生。

总结

HashMap在处理冲突时使用链表存储相同索引的元素。
从Java 8开始,HashMap,ConcurrentHashMap和LinkedHashMap在处理频繁冲突时将使用平衡树来代替链表,当同一hash桶中的元素数量超过特定的值便会由链表切换到平衡树,这会将get()方法的性能从O(n)提高到O(logn)。
当从链表切换到平衡树时,HashMap迭代的顺序将会改变。不过这并不会造成什么问题,因为HashMap并没有对迭代的顺序提供任何保证。
从Java 1中就存在的Hashtable类为了保证迭代顺序不变,即便在频繁冲突的情况下也不会使用平衡树。这一决定是为了不破坏某些较老的需要依赖于Hashtable迭代顺序的Java应用。
除了Hashtable之外,WeakHashMap和IdentityHashMap也不会在频繁冲突的情况下使用平衡树。
使用HashMap之所以会产生冲突是因为使用了键对象的hashCode()方法,而equals()和hashCode()方法不保证不同对象的hashCode是不同的。需要记住的是,相同对象的hashCode一定是相同的,但相同的hashCode不一定是相同的对象。
在HashTable和HashMap中,冲突的产生是由于不同对象的hashCode()方法返回了一样的值。

以上就是Java中HashMap如何处理冲突。
JDK8之前,这种方法被称为链地址法,因为使用链表存储同一桶内的元素。通常情况HashMap,HashSet,LinkedHashSet,LinkedHashMap,ConcurrentHashMap,HashTable,IdentityHashMap和WeakHashMap均采用这种方法处理冲突。
从JDK 8开始,HashMap,LinkedHashMap和ConcurrentHashMap为了提升性能,在频繁冲突的时候使用平衡树来替代链表。因为HashSet内部使用了HashMap,LinkedHashSet内部使用了LinkedHashMap,所以他们的性能也会得到提升。

 

 

HashMap扩容

前言:

HashMap的size大于等于(容量*加载因子)的时候,会触发扩容的操作,这个是个代价不小的操作。

为什么要扩容呢?HashMap默认的容量是16,随着元素不断添加到HashMap里,出现hash冲突的机率就更高,那每个桶对应的链表就会更长,

这样会影响查询的性能,因为每次都需要遍历链表,比较对象是否相等,一直到找到元素为止。

为了提升查询性能,只能扩容,减少hash冲突,让元素的key尽量均匀的分布。

扩容基本点

加载因子默认值是0.75

1

static final float DEFAULT_LOAD_FACTOR = 0.75f;

容量的默认值是16

1

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 等于16

HashMap提供了一个构造参数,可以在创建的时候指定容量和加载因子。

1

public HashMap(int initialCapacity, float loadFactor)

默认的情况下,HashMap 的size一旦大于等于16*0.75=12的话,

同时每个Entry(或者叫桶)里面至少有一个元素的时候就会进行扩容。

1

2

3

4

5

if ((size >= threshold) && (null != table[bucketIndex])) {

      resize(2 * table.length);

      hash = (null != key) ? hash(key) : 0;

      bucketIndex = indexFor(hash, table.length);

}

扩容的时候,容器容量翻倍

1

resize(2 * table.length);

扩容的时候需要重新计算元素的数组下标

1、重新分配一个新的Entry数组
2、重新计算原来元素的在新数组中的下标(比较耗资源)

 

另外有一个比较的文章参考:

https://blog.csdn.net/luanlouis/article/details/41576373?utm_source=tuicool&utm_medium=referral 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值