【Java面试】HashMap

一、哈希表

    HashTable,也即散列表:根据键值(key)而直接访问在内存中存储位置的数据结构

二、HashMap

    概念:基于哈希表的Map接口的非同步实现;也即,HashMap基于hashing原理,通过put和get方法存储和获取对象;

    特点:提供所有可选的映射操作,但是不保证映射的顺序,也不保证顺序不随时间变化;允许使用null键和null值;

【问1】??HashMap的原理,内部数据结构?

    答:->:HashMap底层是一个数组结构,该数组的每一项元素均为一个链表,aka:Hashmap = 数组 + 链表;当链表过长时,会将链表转成红黑树

    ->:HashMap在put一个K,V时,【存储的是键对象和值对象

    (1)先重新计算hash值【(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16),高16bit不变,低16bit和高16bit做了一个异或,即加入了高位计算防止低位不变高位变化时造成的hash冲突】,

    (2)然后计算在数组中的存储下标index【i = (n - 1) & hash】(说明:其中n为该数组的长度也即容量,若没有高16bit和低16bit异或一下,i = (n - 1) & hash计算时真正有效的是低位,会导致因为高位没有参与下标的计算(table长度比较小时),大大增加了引起的碰撞的概率问题),

    (3)如果没有碰撞,则直接放在数组bucket里;如果碰撞了,则以链表形式存储在bucket里,且后加入的放在链尾【jdk8之前是插入头部的,jdk8是插入尾部的】;如果碰撞导致链表过长(>=TREEIFY_THRESHOLD,默认8,也即超过阈值),则会把链表转换成红黑树;如果节点已经存在,则替换old value(保证key唯一性);如果bucket的数量满了(超过了负载因子(默认0.75)*当前容量(初始默认16)),则resize(即调整buckets的数量为当前的2倍); -> JDK8相对于JDK7主要是增加了链表转树(树转链表)和红黑树平衡的逻辑,以进一步优化HashMap的性能

   (4)resize扩大容量一倍,一个比较牺牲性能的操作,这是由于需要重新计算位置,因此应当尽可能精确地预设HashMap的容量,避免resize以有效提高性能;

【问2】??说一下HashMap中put方法过程?

    答:也即:对Key求hash值,然后计算下标;如果没有碰撞,则直接放入桶中;如果碰撞了,则以链表方式放在尾部(jdk8,jdk8之前是放在链头的);如果链表长度超过阈值(TREEIFY_THRESHOLD == 8),则把链表转成红黑树;如果节点已经存在,则替换旧值;如果桶满了(容量*加载因子),则需要resize

【问3】??HashMap中hash函数是怎么实现的?还有那些hash的实现方式?

    答:高16bit不变,低16bit和高16bit做一个异或,得到新的hash值;然后hash值与该数组长度-1,做一个与运算,得到数组下标;???哪些实现方式?

  ->:HashMap在get(Object key)时,

    (1)先重新计算hash值,然后计算在数组中的存储下标index

    (2)如果是bucket里的第一个节点,则直接命中;否则有冲突,通过key.equals(k)查找对应的entry,若为树则O(logn),若为链表则 O(n);

   ->:碰撞探测,也即上述set和get方法中提到的hashCode相同时,则数组index位置处的bucket中以链表形式进行存储和查找

    描述:HashMap初始化时,系统会创建一个长度为capcity(16)的的Entry数组,该Entry数组中可以存储entry元素的位置被称为桶bucket(每个桶均有其索引,且只存储一个entry元素,且entry对象含有一个引用变量,用以指向下一个entry进而构成entry链表结构)

   ->:非线程安全,这是由于:

    (1)不同线程在put操作时,如果是其中一个线程改变了某一键值对的value,那么其他的线程就get不到预期的值,而且值是在不停改变的,因此这样也不是线程安全的;

    (2)在多线程中,当有两个线程在put操作的时候,如果存在扩容调用resize,那么也就是resize被两个线程同时调用,此时可能会产生环形链表,然后再执行get操作就会触发死循环引起CPU的100%问题

    (3)fail-fast,如果在使用迭代器的过程中有其他线程修改了map的结构,则将抛出ConcurrentModificationException异常,也即所谓的fail-fast策略,此时开发人员应该注意线程安全问题

   (说明)(a)避免多线程写时使用HashMap,单写多读是没有问题的;(b)或者为什么要在多线程中使用HashMap,Sun官方都不认为HashMap在多线程中的这个问题是个bug,因为HashMap本来就是不支持多线程使用的,如果需要并发的话就用ConcurrentHashMap;(c)或者通过方法Collections.synchroziedMap(Map),将HashMap转化为线程同步的Map

    注意事项:

 (1)使用String、Integer等引用类型的封箱wrapper类作为键Key使用;String最为常用,不可变且final的,已经重写的equals和hashCode方法 -> 当对象插入到HashMap之后,且Key就不会再改变了

(2)equals相等,必然hashCode相等 <=> hashCode不相等,必然equals不相等;

(3)hashCode相等,equals可能不相等;

(4)Hashtable使用了线程同步锁保护,也即Hashtable是粗暴地添加了同步锁(synchronized),导致性能会有损失 -> 更好的方式是使用ConcurrentHashMap,该类采用的是一种分段锁的机制实现线程安全的,后续将在单独一个篇幅中进行介绍!


【问4】??HashMap怎样解决冲突,讲一下扩容过程,假如一个值在原数组中,现在移动了数组,位置肯定改变了,那是什么定位到在这个新数组中的位置?

    答:将新节点加到链表后,容量扩充为原来的两倍,然后对每个节点重新计算hash值,这个值只会出现在两个地方,一个是原下标的位置,另一种是在下标为<原下标+原容量>的位置

【问5】??抛开HashMap,hash冲突有哪些解决办法?

    答:开放定址,链地址法

【问6】??针对HashMap中某个Node链太长,查找的时间复杂度可能达到O(n),怎么优化?

    答:将链表转为红黑树,JDK1.8已经实现

猜你喜欢

转载自blog.csdn.net/zorkeaccount/article/details/79874697