天天看点

hashmap原理_HashMap和HashTable底层原理以及区别

HashMap底层原理 哈希表

:在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1).

数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组。

HashMap底层是

由数组和链表两种数据结构组合而成的,采用这种数据结果既能方便地读取数据,又可以方便地进行增加和删除的操作。

如果要新增或查找某个元素,我们通过把当前元素的关键字 通过哈希函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。

存储位置 = f(关键字)

Key.hashcode,该方法会返回一个32位的int类型的值,以int h = key.hashCode()为例。获取到h的值之后,会计算该key对应的哈希表中的数组的位置,计算方法就是取模运算,h%table.length。因为table的长度为2的整数次幂,所以可以用h与table.length-1直接进行位与运算。

index = h &(table.length-1)。

得到的index就是放置新数据的位置。

hashmap原理_HashMap和HashTable底层原理以及区别
hashmap原理_HashMap和HashTable底层原理以及区别
哈希冲突:

如果两个不同的元素,通过哈希函数得出的实际存储地址相同,也就是说,当对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,出现哈希冲突。哈希函数的设计至关重要,好的哈希数会尽可能地保证 计算简单和散列地址分布均匀。

数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。

实现原理

HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。

HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度依然

为O(1)

,因为最新的Entry会插入链表头部,仅需简单改变引用链即可,而对于查找操作来讲,此时就需要遍历链表,然后通过key对象的

equals方法

逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

扩容机制

HashMap内存储数据的Entry数组默认是16,如果没有对Entry扩容机制的话,当存储的数据一多,Entry内部的链表会很长,这就失去了HashMap的存储意义了。所以HasnMap内部有自己的扩容机制。HashMap内部有:

变量size,它记录HashMap的底层数组中已用槽的数量;

变量threshold,它是HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)

变量DEFAULT_LOAD_FACTOR = 0.75f,默认加载因子为0.75

HashMap扩容的条件是:当size大于threshold时,对HashMap进行扩容。

扩容是是新建了一个HashMap的底层数组,而后调用transfer方法,将就HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。 很明显,扩容是一个相当耗时的操作,因为它需要重新计算这些元素在新的数组中的位置并进行复制处理。因此,我们在用HashMap的时,最好能提前预估下HashMap中元素的个数,这样有助于提高HashMap的性能。

HashMap共有四个构造方法。构造方法中提到了两个很重要的参数:初始容量和加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中槽的数量(即哈希数组的长度),初始容量是创建哈希表时的容量(从构造函数中可以看出,如果不指明,则默认为16),加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 resize 操作(即扩容)。

加载因子,

如果加载因子越大,对空间的利用更充分,但是查找效率会降低(链表长度会越来越长);如果加载因子太小,那么表中的数据将过于稀疏(很多空间还没用,就开始扩容了),对空间造成严重浪费。

系统默认加载因子为0.75,这是一个比较理想的值,一般情况下我们是无需修改的。

构造方法都会将实际容量设为不小于指定容量的2的次方的一个数,且最大值不能超过2的30次方。

HashTable 与hashmap的区别:

· HashMap是非同步的,没有对读写等操作进行锁保护,所以是线程不安全的,在多线程场景下会出现数据不一致的问题。而HashTable是同步的,所有的读写等操作都进行了锁(synchronized)保护,在多线程环境下没有安全问题。但是锁保护也是有代价的,会对读写的效率产生较大影响。

· HashMap结构中,是允许保存null的,Entry.key和Entry.value均可以为null。但是HashTable中是不允许保存null的。

· HashMap的迭代器(Iterator)是fail-fast迭代器,但是Hashtable的迭代器(enumerator)不是fail-fast的。如果有其它线程对HashMap进行的添加/删除元素,将会抛出ConcurrentModificationException,但迭代器本身的remove方法移除元素则不会抛出异常。这条同样也是Enumeration和Iterator的区别。

继续阅读