面试准备-打卡第十四天-Java篇

108 阅读4分钟

HashMap的底层数据机构是什么?

在JDK1.7中,由“数组和链表”组成,数组为主体,链表主要是为了解决哈希冲突而存在

在JDK1.8中,由“数组+链表+红黑树”组成,当链表过长时,会严重影响HashMap的性能,红黑树搜索时间复杂度是O(logn),而链表是O(n),因此,引入了红黑树,链表和红黑树在达到一定条件会进行转换:

  • 当链表超过 8 且数据总量超过 64 才会转红黑树。
  • 将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。

image.png

解决hash冲突的方法有哪些?HashMap用的哪一种?

解决hash冲突有:开放定址法、再哈希法、拉链法、建立公共溢出区,HashMap中采用的是拉链法

  • 开放定址法也称为再散列法,基本思想就是,如果p=H(key)出现冲突时,则以p为基础,再次hash,p1=H(p),如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地址pi。 因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素,而且因为存在再次hash,所以只能在删除的节点上做标记,而不能真正删除节点。
  • 再哈希法(双重散列,多重散列),提供多个不同的hash函数,当R1=H1(key1)发生冲突时,再计算R2=H2(key1),直到没有冲突为止。 这样做虽然不易产生堆集,但增加了计算的时间。
  • 链地址法(拉链法),将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。
  • 建立公共溢出区,将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。

为什么在解决hash冲突的时候,不直接用红黑树?而选择先用链表再用红黑树?

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。

HashMap和HashTable的区别?

  1. 容器整体结构:

    HashMap 的 key 和 value 都允许为 null, HashMap 遇到 key 为 null 的时候, 调用 putForNullKey 方法进行处理,而对 value 没有处理。 Hashtable 的 key 和 value 都不允许为 null。Hashtable 遇到 null,直接 返回NullPointerException。

  2. 容量设定与扩容机制:

    HashMap 默认初始化容量为 16,并且容器容量一定是 2 的 n 次方,扩容 时,是以原容量 2 倍 的方式 进行扩容。 Hashtable 默认初始化容量为 11,扩容时,是以原容量 2 倍 再加 1 的 方式进行扩容。即 int newCapacity = (oldCapacity << 1) + 1

  3. 散列分布方式(计算存储位置)

    HashMap 是先将 key 键的 hashCode 经过扰动函数扰动后得到 hash 值,然 后再利用 hash & (length - 1)的方式代替取模,得到元素的存储位置。 Hashtable 则是除留余数法进行计算存储位置的(因为其默认容量也不是 2 的 n 次方。所以也无法用位运算替代模运算),int index = (hash & 0x7FFFFFFF) % tab.length;。 由于 HashMap 的容器容量一定是 2 的 n 次方,所以能使用 hash & (length - 1)的方式代替取模的方式计算元素的位置提高运算效率,但 Hashtable 的容器容量不一定是 2 的 n 次方,所以不能使用此运算方式代替

  4. 线程安全(最重要):

    HashMap 不是线程安全,如果想线程安全,可以通过调用 synchronizedMap(Map<K,V> m)使其线程安全。
    但是使用时的运行效率会 下降,所以建议使用 ConcurrentHashMap 容器以此达到线程安全。 
    Hashtable 则是线程安全的,每个操作方法前都有 synchronized 修饰使其同步,但运行效率也不高,
    所以还是建议使用 ConcurrentHashMap 容器以此达到线程安全。 
    

​ 因此,Hashtable 是一个遗留容器,如果我们不需要线程同步,则建议使用 HashMap,如果需要线程同步,则建议使用 ConcurrentHashMap。

HashMap 的put方法流程?

简要流程如下:

  1. 首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;

  2. 如果数组是空的,则调用 resize 进行初始化;

  3. 如果没有哈希冲突直接放在对应的数组下标里;

  4. 如果冲突了,且 key 已经存在,就覆盖掉 value;

  5. 如果冲突后,发现该节点是红黑树,就将这个节点挂在树上;

  6. 如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value。

image.png