面经:java集合

文章目录

一、集合概述

1. 集合介绍

Collection:
Map:
在这里插入图片描述

2. List, Set, Queue, Map区别

有序重复
List
Set××
Queue
Mapkey × value ×key× value √
  • list 对付顺序的好帮手
  • Set(注重独一无二的性质)
  • Queue(实现排队功能的叫号机)
  • Map(用 key 来搜索的专家)

3. 集合框架底层数据结构总结

Collection接口

  • List
    ArrayList --------------- Object[] 数组
    Vector ----------------- Object[] 数组
    LinkedList ----------------- 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
  • Set
    HashSet (无序,唯一) -----------------HashMap
    LinkedHashSet -----------------是 HashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的
    TreeSet(有序,唯一) --------------红黑树
  • Queue
    priorityQueue -------------- Object[] 数组来实现二叉堆
    ArrayQueue --------------------Object[] 数组 + 双指针

Map接口

  • HashMap 数组+链表(用于解决哈希冲突,JDK1.8后当链表长度大于阈值8变为红黑树)
  • LinkedHashMap 基于拉链式散列结构即由数组和链表或红黑树组成,并且增加了一条双向链表
  • Hashtable 数组+链表
  • TreeMap 红黑树

线程安全的

  • vector:基于synchronized
  • HashTable:基于synchronized
  • ConcurrentHashMap:分段锁/node锁
  • CopyOnWriteArrayList和CopyOnWriteArraySet 加了写锁的ArrayList和ArraySet,锁住的是整个对象,但读操作可以并发执行

线程不安全的

  • ArrayList
  • LinkedList

4. 如何选用集合

  • 根据键值获取到元素值时 Map接口
    需要排序时选择TreeMap ,不需要排序时就选择 HashMap
    需要保证线程安全就选用 ConcurrentHashMap
  • 只需要存放元素值时 Collection接口
    需要保证元素唯一时选择实现 Set
    不需要保证元素唯一时选择实现 List

5. 为什么使用集合

当我们需要保存一组类型相同的数据的时候,我们应该是用一个容器来保存,这个容器就是数组。数组存在几个问题:

  • 实际开发,存储的数据的类型是多种多样的
  • 数组数据长度不可变
  • 声明数组时的数据类型也决定了该数组存储的数据的类型
  • 数组存储的数据是有序的、可重复的,特点单一
    Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据

6. Collections.sort()底层原理

事实上Collections.sort方法底层就是调用的Arrays.sort方法,而Arrays.sort使用了两种排序方法,快速排序和优化的归并排序。

快速排序主要是对那些基本类型数据(int,short,long等)排序, 而归并排序用于对Object类型进行排序。
使用不同类型的排序算法主要是由于快速排序是不稳定的,而归并排序是稳定的。这里的稳定是指比较相等的数据在排序之后仍然按照排序之前的前后顺序排列。对于基本数据类型,稳定性没有意义,而对于Object类型,稳定性是比较重要的,因为对象相等的判断可能只是判断关键属性,最好保持相等对象的非关键属性的顺序与排序前一致;另外一个原因是由于归并排序相对而言比较次数比快速排序少,移动(对象引用的移动)次数比快速排序多,而对于对象来说,比较一般比移动耗时。

二、Collection子接口之List

1. ArrayList 和 Vector 的区别

  • ArrayList
    主要实现类 底层Object[] 线程不安全 适用于频繁地查找
  • Vector
    古老实现类 底层Object[] 线程安全

2. ArrayList 和 LinkedList的区别

  • 线程安全
    都不保证线程安全

  • 底层数据结构
    Array 为 Object[ ]数组 Linked 为双线链表(JDK1.6之前为循环链表,1.7后取消)

  • 插入和删除元素是否受元素位置的影响
    在这里插入图片描述

  • 是否支持快速随机访问
    快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
    Linked不支持,Array支持
    RandomAccess 接口是一个标识罢了。标识实现这个接口的类具有随机访问功能。
    是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。

  • 内存空间占用
    ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间
    LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间
    我们在项目中一般是不会使用到 LinkedList 的,需要用到 LinkedList 的场景几乎都可以使用 ArrayList 来代替,并且,性能通常会更好

补充:java中ArrayList和 int[]的区别

Array本质上是静态的,即创建后就无法更改其大小,但是ArrayList是动态数组,如果ArrayList中的元素数大于其阈值,则可以调整自身大小。
ArrayList的size()增长方式:初始化长度为10,一段超过1.5倍自动扩容。arr.minCapacity=arr.minCapacity+arr.minCapacity/2;
长度计算:array.length 和arraylist,size();

3. ArrayList源码&扩容机制分析

3.1 ArrayList 简介

  • 底层: 数组队列,相当于动态数组
  • 容量问题: 容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量
  • 实现接口:ArrayList继承于 AbstractList ,实现了 List, RandomAccess, Cloneable, java.io.Serializable 这些接口。
    RandomAccess 是一个标志接口,表明实现这个接口的 List 集合是支持快速随机访问
    Cloneable 接口 ,即覆盖了函数clone(),能被克隆
    java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输

3.2 ArrayList 源码

添加链接描述

3.3 扩容机制分析

在这里插入图片描述

- 构造函数(3种)

   /**
     * 默认初始容量大小
     */
    private static final int DEFAULT_CAPACITY = 10;


    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     *默认构造函数,使用初始容量10构造一个空列表(无参数构造)
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 带初始容量参数的构造函数。(用户自己指定容量)
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {//初始容量大于0
            //创建initialCapacity大小的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {//初始容量等于0
            //创建空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {//初始容量小于0,抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }


   /**
    *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
    *如果指定的集合为null,throws NullPointerException。
    */
     public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。
- 一步步分析(这里以无参构造函数创建的 ArrayList 为例分析)
(1)add方法 : 将指定的元素追加到此列表的末尾。

    public boolean add(E e) {
   //添加元素之前,先调用ensureCapacityInternal方法
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //这里看到ArrayList添加元素的实质就相当于为数组赋值
        elementData[size++] = e;
        return true;
    }

(2) ensureCapacityInternal( )方法: 得到最小扩容量

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
              // 获取默认的容量和传入参数的较大值
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }

(3) ensureExplicitCapacity( )方法:判断是否需要扩容

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            //调用grow方法进行扩容,调用此方法代表已经开始扩容了
            grow(minCapacity);
    }

(4) grow()方法: 扩容的核心方法

    /**
     * 要分配的最大数组大小
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    private void grow(int minCapacity) {
        // oldCapacity为旧容量,newCapacity为新容量
        int oldCapacity = elementData.length;
        //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
        //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
       // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
       //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

(5) hugeCapacity() 方法
hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果 minCapacity 大于最大容量,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        //对minCapacity和MAX_ARRAY_SIZE进行比较
        //若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
        //若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
        //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

  • 举例分析

当我们要 add 进第 1 个元素到 ArrayList 时,elementData.length 为 0 (因为还是一个空的 list),因为执行了ensureCapacityInternal() 方法 ,所以 minCapacity 此时为 10。此时,minCapacity - elementData.length > 0成立,所以会进入 grow(minCapacity) 方法
当 add 第 2 个元素时,minCapacity 为 2,此时 elementData.length(容量)在添加第一个元素后扩容成 10 了。此时,minCapacity - elementData.length > 0 不成立,所以不会进入执行grow(minCapacity) 方法
第 3、4···到第 10 个元素时,依然不会执行 grow 方法,数组容量都为 10
直到添加第 11 个元素,minCapacity(为 11)比 elementData.length(为 10)要大。进入 grow 方法进行扩容
当 add 第 1 个元素时,oldCapacity 为 0,经比较后第一个 if 判断成立,newCapacity = minCapacity(为 10)。但是第二个 if 判断不会成立,即 newCapacity 不比 MAX_ARRAY_SIZE 大,则不会进入 hugeCapacity 方法。数组容量为 10,add 方法中 return true,size 增为 1。
当 add 第 11 个元素进入 grow 方法时,newCapacity 为 15,比 minCapacity(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 hugeCapacity 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。
int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)! 奇偶不同,比如 :10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数.
- System.arraycopy() 和 Arrays.copyOf( )
看两者源代码可以发现 copyOf()内部实际调用了 System.arraycopy() 方法
arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf() 是系统自动在内部新建一个数组,并返回该数组。
- ensureCapacity 方法
理论上来说,最好在向 ArrayList 添加大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数

4. ArrayList 遍历方式

  • 1.普通for循环 - while
  • 2.增强for循环
  • 3.迭代器
  • 4.forEach() Iterable接口中的默认方法
  • 5.Stream API forEach()

5. ArrayList的remove方法

添加链接描述

public E remove(int index)
public boolean remove(Object o)

对于int[]数组 如果要删除指定元素,需要用引用类型

nums.remove(Integer.valueOf(2))

6. ArrayList常用方法

   1. add() ——将指定元素添加到此集合的末尾或者在此集合中的指定位置插入指定元素
   2. addAll() ——将指定集合中的所有元素添加到此集合中,从指定位置开始
   3. clear() ——删除集合 中所有元素
   4. contains() ——判断此集合是否包含某个元素
   5. forEach() ——增强for循环
   6. get() ——返回此集合中指定下标的元素
   7. indexOf() ——获取指定元素在此集合中第一次出现的下标,未找到则返回 -1
   8. lastIndexOf() ——获取指定元素在此集合中最后一次出现的下标,未找到则返回 -1
   9. isEmpty() ——判断集合元素是否为空
   10. iterator() ——迭代器
   11. listIterator() ——迭代器
   12. remove() ——删除该集合中指定下标的元素/删除集合中第一次出现的指定元素
   13. removeAll() ——从该集合中删除指定集合中包含的所有元素
   14. retainAll() ——保留此集合在指定集合包含的所有元素
   15. removeIf() ——删除满足调教的所有元素
   16. set() ——将指定下标的元素替换成新元素
   17. size() ——获取此集合的元素个数
   18. sort() ——使用提供的 Comparator 对此集合进行排序
   19. toArray() ——以原顺序返回一个包含此列表所有元素的数组

三、Collection子接口之set

1. comparable 和 Comparator 的区别

comparable 接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序
comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序

2. 无序性和不可重复性的含义

  • 无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
  • 不可重复性是指添加的元素按照 equals() 判断时 ,返回 false,需要同时重写 equals() 方法和 hashCode() 方法。
    添加链接描述

3. HashSet、LinkedHashSet和TreeSet的异同

  • 都是 Set 接口的实现类,都能保证元素唯一,并且都不是线程安全
  • HashSet 底层数据结构为哈希表(HashMap) 用于不需要保证元素插入和取出顺序的场景
  • LinkedHashSet 链表和哈希表, 用于保证元素插入和取出顺序满足FIFO的场景
  • TreeSet 红黑树 用于支持对元素自定义排序规则的场景

4. HashSet底层原理

在这里插入图片描述

四、Collection子接口之Queue

1. Queue与Deque的区别

  • Queue 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则。
    在这里插入图片描述
  • Deque 是双端队列,在队列的两端均可以插入或删除元素。
    在这里插入图片描述

2. ArrayQueue与LinkedList的区别

  • 都实现了Deque接口,都具有队列的功能
  • 底层数据结构
    ArrayQueue: 数组+双指针,LinkedList:链表
  • 存储Null数据
    ArrayDeque 不支持存储 NULL 数据,但 LinkedList 支持。
  • 发行版本
    ArrayDeque 是在 JDK1.6 才被引入的,而LinkedList 早在 JDK1.2 时就已经存在。
  • 扩容问题
    ArrayDeque 插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然 LinkedList不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。
    从性能的角度上,选用 ArrayDeque 来实现队列要比 LinkedList 更好。此外,ArrayDeque 也可以用于实现栈

3. PriorityQueue

  • 其与 Queue 的区别在于元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队
  • 利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据
  • 通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素
  • 非线程安全的,且不支持存储 NULL 和 non-comparable 的对象
  • 默认小顶堆,但可以接收一个 Comparator 作为构造参数,从而来自定义元素优先级的先后

五、Map接口

1. HashMap和HashTable区别

  • 线程是否安全
    HashMap非线程安全 HashTable线程安全,经过synchronized修饰(但是建议用ConcurrentHashMap)
  • 效率
    由于线程安全问题,HashMap效率高一点
  • 对 Null key 和 Null value的支持
    HashMap不支持,Hashtable支持,但null作为键只能有一个,作为值可以有无数个
  • 初始容量和每次扩充容量的不同
    (1)初始化时不指定:hashtable默认11,扩容为2n+1;hashmap默认16,扩容为2n
    (2) 初始化时指定:hashtable直接使用,hashmap会将其扩容为2的幂次方大小
  • 底层数据结构
    JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。Hashtable 没有这样的机制。

2. HashSet 和 HashMap 区别

HashSet底层基于HashMap实现
在这里插入图片描述

3. HashMap 和 TreeMap 区别

TreeMap 和HashMap 都继承自AbstractMap ,但是需要注意的是TreeMap它还实现了NavigableMap接口和SortedMap 接口。
实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力
实现SortedMap接口让 TreeMap 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。

4. HashSet如何检查重复

  • 当你把对象加入HashSet时,HashSet 会先计算对象的hashcode值来判断对象加入的位置
  • 与其他加入的对象的 hashcode 值作比较
  • 如果没有相符的 hashcode,HashSet 会假设对象没有重复出现
  • 如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。

5. HashMap底层实现

  • JDK 1.8之前
    HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。
    (1) HashMap 通过 key 的 hashcode 经过扰动函数处理过后得到 hash 值,
    (2) 然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),
    (3 )如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,
    (4) 如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
    在这里插入图片描述
  • JDK 1.8之后
    JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
    在这里插入图片描述

6. HahsMap多线程操作导致死循环问题

一般来说,Hash表这个容器当有数据要插入时,都会检查容量有没有超过设定的thredhold,如果超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的元素都需要被重算一遍。这叫rehash,这个成本相当的大。
主要原因在于并发下的 Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。

7. HashMap的7种遍历方式

  • HashMap 遍历从大的方向来说,可分为以下 4 类:
    迭代器(Iterator)方式遍历
    For Each 方式遍历
    Lambda 表达式遍历(JDK 1.8+)
    Streams API 遍历(JDK 1.8+)
  • 但每种类型下又有不同的实现方式,因此具体的遍历方式又可以分为以下 7 种:
    使用迭代器(Iterator)EntrySet 的方式进行遍历
    使用迭代器(Iterator)KeySet 的方式进行遍历
    使用 For Each EntrySet 的方式进行遍历
    使用 For Each KeySet 的方式进行遍历
    使用 Lambda 表达式的方式进行遍历
    使用 Streams API 单线程的方式进行遍历
    使用 Streams API 多线程的方式进行遍历

8. ConcurrentHashMap和HashTable的区别

  • 底层数据结构
    JDK1.7 的ConcurrentHashMap:分段数组和链表
    JDK1.8的ConcurrentHashMap:数组+链表/红黑树
    HashTable:数组+链表
  • 实现线程安全的问题
    (1) JDK 1.7 ConcurrentHashMap
    对整个桶数组进行了分割分段(Segment,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
    在这里插入图片描述
    (2) JDK 1.8 ConcurrentHashMap
    直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作
    在这里插入图片描述
    (3) HashTable
    Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

9. ConcurrentHashMap线程安全的具体实现方式/底层具体实现

  • JDK 1.8之前
    首先将数据分为一段一段(这个“段”就是 Segment)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
    Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。
    一个 ConcurrentHashMap 里包含一个 Segment 数组,Segment 的个数一旦初始化就不能改变。 Segment 数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。
    Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁。也就是说,对同一 Segment 的并发写入会被阻塞,不同 Segment 的写入是可以并发执行的
  • JDK1.8之后
    取消了 Segment 分段锁,采用 Node + CAS + synchronized 来保证并发安全。数据结构跟 HashMap 1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。
    java 8 中,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。

10. JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同

  • 线程安全实现方式 :
    JDK 1.7 采用 Segment 分段锁来保证安全, Segment 是继承自 ReentrantLock。JDK1.8 放弃了 Segment 分段锁的设计,采用 Node + CAS + synchronized 保证线程安全,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点。Hash 碰撞解决
  • 方法 :
    JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
  • 并发度 :
    JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大

11. HashMap的长度为什么2的幂次方

说白了就是为了提高性能,把取余的运算变成取模的运算
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且 **采用二进制位操作 &,相对于%能够提高运算效率,**这就解释了 HashMap 的长度为什么是 2 的幂次方
将取余操作变成与操作,提高效率

12. HashMap的扩容原理

在这里插入图片描述
首先 hashmap的默认大小是16,负载因子是0.75 因此初始阈值16*0.75;
当大于阈值时,就会调用resize方法进行扩容,扩容后是原来大小的两倍。
扩容的话主要有两部分,创建一个新的Entry空数组,长度是原数组的2倍。由于计算位置的公式(n-1)&hash 与长度有关,因此Rehash遍历原Entry数组,把所有的Entry重新Hash到新数组。

13. 为什么HashMap默认加载因子是0.75

在这里插入图片描述

14. HashMap 源码中,计算 hash 值为什么有一个 高 16 位 和 低 16 位异或的过程?

HashMap将高16位与低16位进行异或,这样可以保证高位的数据也参与到与运算中来,以增大索引的散列程度,让数据分布得更为均匀
详细

15. JDK1.8之后,HashMap头插法改为尾插法?

1.头插法在并发下有致命问题,就是可能形成数据环,get 数据时死循环,而在 1.8 之前因为处理 hash 冲突的方式是用链表存放数据,使用头插法可以提升一定效率。
但是在 1.8 之后这个效率提升就可有可无了,链表长度超过 7 就要考虑升级红黑树了,所以哪怕进行尾插遍历次数也会很有限,效率影响不大。
2.就是因为 1.8 之后数据结构的变动,当链表长度达到阈值,升级为红黑树后头插法就不适用了,因为构建红黑树需要进行比对更新序列,也就不能去说是头插法还是尾插了

16. 解决hash冲突的方法

添加链接描述

哈希冲突:当两个不同的数经过哈希函数计算后得到了同一个结果,即他们会被映射到哈希表的同一个位置时,即称为发生了哈希冲突。简单来说就是哈希函数算出来的地址被别的元素占用了。解决方法有四种:

  • 开放定址法:我们在遇到哈希冲突时,去寻找一个新的空闲的哈希地址
    (1)线性探测再散列
    dii=1,2,3,…,m-1
    这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。
    (2)二次探测再散列
    di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 )
    这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。
    (3)伪随机探测再散列
    di=伪随机数序列。
    具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),并给定一个随机数做起点。

  • 再哈希法
    这种方法是同时构造多个不同的哈希函数
    Hi=RH1(key) i=1,2,…,k
    当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

  • 链地址法
    这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。

  • 建立公共溢出区
    这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值