面经:java集合

阿里P5初级Android面经:Java集合源码、ArrayList扩容、HashMap原理、线程安全集合 本文总结了阿里巴巴Java面试的核心考点,重点剖析了Java集合框架的底层实现原理。文章对比了阿里与腾讯的面试风格差异,指出阿里更注重技术深度挖掘。核心内容包括:ArrayList扩容机制(1.5倍增长、数组拷贝)、LinkedList与ArrayList的性能对比、HashMap在JDK1.8的优化(红黑树、扰动函数)、线程安全集合的实现差异(ConcurrentHashMap的CAS+synchronized机制)等。特别强调阿里面试要求源码级理解,如HashMap的put过程、扩容策略等实现细节。 阅读详情

文章目录

一、集合概述

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个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。

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

AI 代码赋能短视频带货:服装及多赛道批量内容生产全流程实操 本文聚焦 AI 技术在短视频带货领域的落地应用,以服装赛道为核心,延伸至花卉绿植、图书、茶具等热门领域,系统拆解 AI 工具在选品、素材处理、视频制作(含动态效果、场景搭建)、运营优化等环节的实操方法。针对从业者普遍面临的 “内容生产低效”“无专业技术难出优质视频”“流量运营踩坑” 等痛点,提供从基础工具使用到高阶视频制作的全流程技术方案,助力相关从业者借助 AI 实现批量、高效的短视频内容产出。 阅读详情

相关推荐

SAP MTO实战:从销售订单到生产订单的精准触发与配置详解

本文深入解析SAP系统中按订单生产(MTO)模式的核心流程与配置。详细阐述了如何通过精准配置物料主数据策略组、需求类型及计划策略(如策略50),实现从销售订单自动触发生产订单的完整链路。文章结合实战案例,指导用户打通销售与生产环节,并提供了关键事务代码与常见问题排查指南,助力企业实现以销定产,优化库存与成本控制。

weixin_29300931的博客 212

mapengpeng1999@163.com java集合容器

Java集合容器 一、Java集合框架 java集合概述: 1.面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。但使用Arrays(数组)存储对象有一些弊端,而java集合就像一种容器,可以动态的把多个对象的引用放入容器中。 2.java集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组。 我们之前使用数组存储相同各类型的数据,但是数组的缺点: - 数组的长度不能够任意扩充。集合的长度是可变的。 - 同一个数组不能存放不同类型的数据【集合可以,但

ma1782072的博客 468

国密证书实战:在CentOS 7上构建GmSSL私有CA与完整证书链

本文详细介绍了在CentOS 7操作系统上,使用GmSSL工具构建私有国密证书颁发机构(CA)与完整证书链的实战指南。内容涵盖从部署GmSSL、创建国密根CA、签发服务器与客户端证书,到生产环境安全加固与最佳实践的全流程,旨在帮助系统管理员和安全工程师建立统一、可控的内部国密安全通信体系。

jenkins8butler的博客 495

JavaGuide集合汇总

一 ,List Set Map三者的区别 List(注重有序):List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象。 Set(注重独一无二):不允许重复的集合,不会有多个元素引用相同的对象。 Map(注重key-value):使用键值对进行存储。Map会维护与Key有关联的value,两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何...

weixin_43624024的博客 5351

esim分布式云卡池技术

eSIM卡,即Embedded-SIM,嵌入式SIM卡。eSIM卡的概念就是将传统SIM卡直接嵌入到设备芯片上,而不是作为独立的可移除零部件加入设备中,用户无需插入物理SIM卡。这一做法将允许用户更加灵活的选择运营商套餐,或者在无需解锁设备、购买新设备的前提下随时更换运营商。未来通用的eSIM标准建立将为普通消费者、企业用户节省更多移动设备使用成本,并带来更多的便利、安全性。 2018年 3月7日,中国联通宣布,正式在上海、天津、广州、深圳、郑州、长沙6座城市率先启动“eSIM一号双终端”业务的办理。[.

著名博客 3707

Vite 任意文件读取漏洞复现(CVE-2026-39363)

Vite开发服务器 WebSocket中的fetchModule方法未实施server.fs访问控制,导致攻击者可通过WebSocket连接并读取服务器上的任意文件(CVE-2026-39363)

iSee857的博客 220

Java 集合类 面经

答案:Java集合框架是一个提供了数据结构和算法的标准库,包括列表、集合、映射和队列等。它的主要接口有CollectionListSet和Map。拓展:解释常用的实现类,例如ArrayListLinkedListHashSetTreeSetHashMap和TreeMap,并简要说明它们的特点和使用场景。答案fail-fast机制在检测到集合在迭代过程中被修改时,会立即抛出。例如,ArrayList的迭代器是fail-fast的。fail-safe机制在迭代过程中允许集合被修改,但不会抛出异常。

2303_79132118的博客 946

基于AI大模型实现自然语言转SQL、SQL解释、SQL优化和SQL转换

随着大数据和人工智能技术的发展,数据分析和数据挖掘在各行各业中的应用越来越广泛。SQL(Structured Query Language)作为关系数据库的查询语言,是数据分析的重要工具。然而,SQL语言的复杂性和专业性使得许多非技术背景的用户难以直接使用,阻碍了数据的广泛利用。为了降低SQL的使用门槛,许多研究者和企业开始探索如何通过自然语言处理(NLP)技术,将自然语言转化为SQL查询语句,从而让用户可以通过自然语言与数据库进行交互。

AI天才研究院 2238

【JavaGuide面试总结】Java高级特性基础篇·上

Java高级特性基础篇摘要 本文总结了Java高级特性的核心知识点: 参数传递机制:Java只有值传递,基本类型传递字面量副本,引用类型传递对象地址副本。 static关键字:用于修饰成员变量/方法(类共享)、静态代码块(类加载执行)、静态导包(直接使用静态成员)和静态内部类(不依赖外围类)。 异常体系: Exception分为Checked(必须处理)和Unchecked异常 Error是程序无法处理的严重错误 常见方法:getMessage()、toString()等 异常处理: finally块在正常

大河之犬的博客 1117

小目标检测之改进的YOLOV11算法

MASF-YOLO通过多尺度特征聚合注意力增强自适应融合三大核心创新,显著提升了无人机视角下的小目标检测性能。

hasakie的博客 1717

【项目实战】AES实施应符合NIST密码算法验证程序

AES由美国国家标准与技术研究院(NIST)制定。NIST是负责开发和维护美国政府使用的技术标准的机构,包括加密标准。

奶爸的编程之路,也就一周冷个三天 765

【面经】汇总

【面经】汇总

做点有意思的事情 1610

【系统分析师】核心考点:100个高频知识点汇总

本文系统梳理了系统分析师考试的11大领域高频考点,重点解析了计算机硬件与体系结构的7个核心知识点:计算机五大部件(CPU/存储器/总线等)、校验码类型(奇偶/海明/CRC)、CISC与RISC指令系统对比、总线分类与特性、RAID磁盘阵列级别、流水线技术原理及Flynn系统分类法。内容涵盖硬件组成、数据校验、指令架构、存储技术等关键概念,通过表格对比和公式说明,为考生提供结构化的知识框架与备考重点。

数据知道的博客 5569

面渣逆袭:Java集合连环三十问

Java集合三十问,深入全面,图文并茂,看完,保证你比面试官更懂集合!

三分恶的博客 3万+

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

面经分享:如何破解 Java 集合类面试题

今日招式:Java集合类面试题Java集合类绝对是我们的老朋友了,Java技术江湖里,谁人不知,谁人不晓,它的使用率非常高,使用难度却也不大,这也导致了很多人对它不屑一顾,殊不知其中却暗藏玄机,今天我们不妨一起来破解一下Java集合类的面试题。面试官常用招式:只见面试官微微一笑,拔出长剑,向你刺来,你赶紧拔剑相迎,几招过后,你才意识到面试官使的都是虚招,只是试探你而已。1.Java集合框架的基础接...

weixin_34389926的博客 140

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

面经分享:如何一招破解 Java 集合类面试题?

Java集合类面试题: Java集合类绝对是我们的老朋友了,Java技术江湖里,谁人不知,谁人不晓,它的使用率非常高,使用难度却也不大,这也导致了很多人对它不屑一顾,殊不知其中却暗藏玄机,今天我们不妨一起来破解一下Java集合类的面试题。 面试官常用招式: 1.Java集合框架的基础接口有哪些? Collection为集合层级的根接口。一个集合代表一组对象,这些对象即为它的元素。Java平台不提供这个接口任何直接的实现。 Set是一个不能包含重复元素的集合。这个接口对数学集合抽象进行建模,被用来代表集合.

bjyxszd9的博客 474

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

面霸篇:Java 核心集合容器全解(核心卷二)

从面试角度作为切入点提升大家的 Java 内功,所谓根基不牢,地动山摇。码哥在 《Redis 系列》的开篇 Redis 为什么这么快中说过:学习一个技术,通常只接触了零散的技术点,没有在脑...

码哥字节 354

osu-droid谱面收藏与集合管理终极指南:打造你的专属音游曲库

在手机音游的世界里,谱面(beatmap)就是你的全部家当。随着下载的歌曲越来越多,选歌列表动辄几百上千首,想要快速找到想玩的谱面变得越来越困难。**osu-droid** 作为一款功能强大的开源移动端 osu! 模拟器,内置了完善的**谱面收藏与集合管理**功能,让你可以像整理音乐播放列表一样,轻松打造属于自己的**专属音游曲库**。本文将从零开始,教你玩转 osu-droid 的收藏系统,告别

gitblog_02347的博客 303

从简历到技术面:Java面试准备清单

系统设计题是高级岗的必考题,比如“设计一个短链接系统”或“设计一个秒杀系统”。你要主动引出关键约束:估算QPS和存储量,再谈分库分表、缓存、限流、异步削峰。

searchboy2025的博客 12

Java 集合框架面经

Java 中的队列主要通过 Queue 接口和并发包下的 BlockingQueue 两个接口来实现。优先级队列 PriorityQueue 实现了 Queue 接口,是一个无界队列,它的元素按照自然顺序排序或者 Comparator 比较器进行排序。双端队列 ArrayDeque 也实现了 Queue 接口,是一个基于数组的,可以在两端插入和删除元素的队列。采用安全失败机制的集合容器,在遍历时并不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

qq_45801780的博客 1216

超全!2025年JAVA面经汇总

一开始问了一下项目,知道没有上线后就不感兴趣,直接开始拷打八股,于是就有了我从未见过的这次八股,由于这次八股的内容实在过于逆天,本人特地写下来供大家分享,也向各位求助一下这些题目。33.项目-你来思考下,你的项目里怎么引入GPT,除了GPT作为和用户的交互,能不能把核心代码逻辑直接交由LLM负责,你不需要咋写代码了,只保留数据存储层?过期时间怎么设计的?26.我们知道jdk提供了很多锁机制来保证并发安全,其中syn是一个用的最广泛的,那么,syn锁的是A.class的时候,这个时候锁的粒度是什么样的。

tangjieqing的博客 4901

[JAVA八股速成|冲击秋招SSP】JAVA集合类+Java基础+Java面经总结!【持续更新】

保证新容量仍是2的幂,使得定位索引的位运算继续有效。平衡扩容频率(避免1.5倍频繁扩容)与内存占用(避免3倍瞬间占用过大)。简化实现,哈希分布更均匀。Iterator 是 Java 集合框架中的一种接口,广泛用于遍历集合中的元素。它定义了用于顺序访问集合元素的方法,能够让我们在不暴露集合内部实现细节的情况下访问集合。承蒙大家的厚爱,以后不搞抽象了,现在我把我秋招面经的笔记分享出来,希望大家能多多支持。

程序员高级码农的博客 1790

Java集合 之 单列集合

java的单列集合,看这一篇就够了

qq_73698057的博客 3085

全网最新真实Java实习面试题面经+详细讲解

本文涵盖了Java技术面试的常见问题,主要分为技术面和面试官面两个部分。技术面部分包括Java集合、Spring框架、MySQL、Redis、Linux等知识点,重点分析了集合类型区别、Spring核心机制、数据库优化、缓存使用等问题。面试官面部分则关注个人介绍、项目经验、薪资期望等内容。文章详细解答了30个技术问题,如IOC/AOP原理、事务隔离级别、索引优化等,并提供了实际应用场景的解决方案,如订单超卖处理、缓存三兄弟问题等,适合Java开发者面试准备参考。

2301_81920327的博客 2727
上一篇: 面经:Java基础部分
下一篇: springboot后台管理系统
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值