HashMap和HashMap家族比较大全

HashMap和HashMap家族比较大全

1、HashMap的底层实现原理?

哈希表:相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1),接下来我们就来看看哈希表是如何实现达到惊艳的常数阶O(1)的。、

**哈希冲突:**哈希算法存在一个缺点就是哈希冲突。例如,我们进行数据存储时,我们通过对关键字进行hash时得到的地址已经存储过数据了,这时就会出现哈希冲突。因此,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀。

**解决哈希冲突的方法:**哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。

HashMap的整体结构:
HashMap和HashMap家族比较大全_第1张图片
简单来说,HashMap由数组+链表组成的数组是HashMap的主体链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

2、HashMap的put和get原理?

HashMap和HashMap家族比较大全_第2张图片

后插入的元素(“1”,“king”)放在链表的头部

concurrentHashMap再JDK1.7和JDK1.8的区别

  • 1.7

    ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment实际继承自可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,每个Segment里包含一个HashEntry数组,我们称之为table,每个HashEntry是一个链表结构的元素。
    HashMap和HashMap家族比较大全_第3张图片

    面试常问:

    1. ConcurrentHashMap实现原理是怎么样的或者问ConcurrentHashMap如何在保证高并发下线程安全的同时实现了性能提升?

    答:ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,只要多个修改操作发生在不同的段上,它们就可以并发进行。

  • 1.8

    JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本

HashMap和HashMap家族比较大全_第4张图片

这个put的过程很清晰,对当前的table进行无条件自循环直到put成功,可以分成以下六步流程来概述

  1. 如果没有初始化就先调用initTable()方法来进行初始化过程
  2. 如果没有hash冲突就直接CAS插入
  3. 如果还在进行扩容操作就先进行扩容
  4. 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
  5. 最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环
  6. 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
  • 对比
    1. JDK1.8取消了segment数组,直接用table保存数据,锁的粒度更小,减少并发冲突的概率。
    2. JDK1.8存储数据时采用了链表+红黑树的形式,纯链表的形式时间复杂度为O(n),红黑树则为O(logn),性能提升很大。什么时候链表转红黑树?当key值相等的元素形成的链表中元素个数超过8个的时候。
    3. JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
    4. JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
    5. JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
    6. JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,我觉得有以下几点
      1. 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
      2. JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
      3. 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据

3、HashMap的扩容机制?

什么时候开始扩容?

  • capacity 即容量,默认16。
  • loadFactor 加载因子,默认是0.75
  • threshold 阈值。阈值=容量*加载因子。默认12。当元素数量超过阈值时便会触发扩容。

一般情况下,当元素数量超过阈值时便会触发扩容。每次扩容的容量都是之前容量的2倍。

HashMap的容量是有上限的,必须小于1<<30,即1073741824。如果容量超出了这个数,则不再增长,且阈值会被设置为Integer.MAX_VALUE

此外还有几个细节需要注意:

  • 首次put时,先会触发扩容(算是初始化),然后存入数据,然后判断是否需要扩容;
  • 不是首次put,则不再初始化,直接存入数据,然后判断是否需要扩容;

JDK8的元素迁移

JDK8则因为巧妙的设计,性能有了大大的提升:由于数组的容量是以2的幂次方扩容的,那么一个Entity在扩容时,新的位置要么在原位置,要么在原长度+原位置的位置。原因如下图:

img

数组长度变为原来的2倍,表现在二进制上就是多了一个高位参与数组下标确定。此时,一个元素通过hash转换坐标的方法计算后,恰好出现一个现象:最高位是0则坐标不变,最高位是1则坐标变为“10000+原坐标”,即“原长度+原坐标”。如下图:

4、HashMap为什么不安全?

HashMap底层是一个Entry数组。当发生hash冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。

此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果使用 Collections.synchronizedMap 方法来“包装”该映射,这样就会产生并发修改异常

5、HashMap和HashMap家族其他成员的区别

1、HashMap和ConcurrentHashMap的区别
  • 是什么
    • HashMap:数组+链表
    • ConcurrentHashMap:ConcurrentHashMap最外层不是一个大的数组,而是一个Segment的数组。每个Segment包含一个与HashMap数据结构差不多的链表数组。
  • 区别
    • HashMap线程不安全,多线程时容易产生并发修改异常
    • ConcurrentHashMap采用的是分段式锁,可以理解为把一个大的Map拆封成N个小的Segment,在put数据时会根据hash来确定具体存放在哪个Segment中,Segment内部的同步机制是基于Lock操作的,每一个Segment都会分配一把锁,当线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,也就是实现并发访问。
2、HashMap和HashTable的区别
hashmap 线程不安全 允许有null的键和值 效率高一点、 方法不是Synchronize的要提供外同步 有containsvalue和containsKey方法 HashMap 是Java1.2 引进的Map interface 的一个实现 HashMap是Hashtable的轻量级实现
hashtable 线程安全 不允许有null的键和值 效率稍低、 方法是是Synchronize的 有contains方法方法 、Hashtable 继承于Dictionary 类 Hashtable 比HashMap 要旧
3、HashMap和TreeMap区别?

1、数据结构

HashMap: 数组+列表+红黑树

HashMap和HashMap家族比较大全_第5张图片

TreeMap: 红黑树
HashMap和HashMap家族比较大全_第6张图片

2、节点类型

HashMap: 两种 Node 和 TreeNode (支持链表)

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
}

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
 
 }

TreeMap: (只是红黑树,不支持链表操作)

static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;
}

3、线程是否安全?

线程都是不安全的

4、key和value的区别

HashMap: Key和 Value 都可以为 null ( 如果key 为 null 的话, hashCode = 0 )

TreeMap: Key 不能为 null , Value 可以为 null

5、定位

HashMap:
通过 hash 算法定位, 针对 hash 冲突的值, 采用列表和红黑树的方式存储, 一定条件下可以相互转换. 红黑树中, 如果 hash 值一样,是通过 key 的 比较方法(可自定义)来计算红黑树所处的位置. 比如 String 类型的Key 是通过 String 类里的 compareTo 方法,直接返回 第一个不同字符的差值.来进行红黑树定位.

TreeMap:
红黑树存储结构, 通过自定义 key 比较器或者默认的比较算法来进行定位红黑树节点的存储位置.

4、LinkedHashMap底层实现?

1、LinkedHashMap继承于HashMap,也就是说HashMap拥有的能力LinkedHashMap也同时拥有,与HashMap不同在于LinkedHashMap中维护了一个双向链表(此链表会中包含了所有数据),并且重写Node对象,并通过记录双向链表的始终(head,tail)节点用于保证此类可以做到顺序读写(并不仅仅如此)。

2、HashMap的put和set的方法实现

  • put
    • put之前,先计算key值的hashCode
    • hashCode相同的key值在数组中的索引就是一样的。于是hashCode相同的这些值就被存放在一个链表上。查找的时候就循环这个链表进行查找。
    • 这一段是说如果table为空的时候,就给这个table初始化,也就是resize。
    • 直接查看这个hash值在数组中的位置,如果这个位置上的值为空,就把值放在这里。
    • 你新来的数据,不仅hash值相同,而且还和之前的key值equal,那肯定就是key值相同,就是说你现在put进来的key值之前的map里面就有,当然我就把这个节点返回给你了,你在这个节点上操作就行,
    • hash相同,但是不equals的数据,就循环这个链表,如果遇到的节点为空,就将数据插入到这个空节点上,如果遇到的节点值equals相同(证明又遇到了key值相同的情况),又将这个节点记录并返回。
  • get
    • get的时候,先计算key的hash值,肯定就是根据这个hash值去数组中找数据了
    • 如果table为空或者当前位置的数据为空,当然就直接就返回空值啦
    • hash相同,但是不equals的数据,就循环这个链表,如果遇到的节点为空,就将数据插入到这个空节点上,如果遇到的节点值equals相同(证明又遇到了key值相同的情况),又将这个节点记录并返回。
  • get
    • get的时候,先计算key的hash值,肯定就是根据这个hash值去数组中找数据了
    • 如果table为空或者当前位置的数据为空,当然就直接就返回空值啦
    • hash值相等并且key值相等或者equal的话,就返回这个位置的值。

你可能感兴趣的:(java,hash,面试)