Java面试-集合

  1. 说说 List,Set,Map 三者的区别?三者底层的数据结构?
  2. 有哪些集合是线程不安全的?怎么解决呢?
  3. ArrayList 和 LinkedList 的区别
  4. ArrayList的扩容机制
  5. Queue 和 Deque 的区别
  6. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
  7. HashMap 和 Hashtable 的区别?HashMap 和 HashSet 区别?HashMap 和 TreeMap 区别?
  8. HashMap 的底层实现
  9. HashMap 的长度为什么是 2 的幂次方
  10. ConcurrentHashMap 和 Hashtable 的区别?
  11. ConcurrentHashMap 线程安全的具体实现方式/底层具体实现

在这里插入图片描述

集合底层数据结构

  1. List
  • ArrayListObject[] 数组
  • VectorObject[] 数组
  • LinkedList: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
  1. Map
  • HashMap: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
  • LinkedHashMapLinkedHashMap 继承自 HashMap,由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
  • Hashtable: 数组+链表
  • TreeMap: 红黑树(自平衡的排序二叉树)
  1. Set
  • HashSet: 基于 HashMap 实现的,底层采用 HashMap 来保存元素
  • LinkedHashSet: LinkedHashSetHashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。
  • TreeSet: 红黑树(自平衡的排序二叉树)
  1. Queue
  • PriorityQueue: Object[] 数组来实现二叉堆
  • ArrayDeque: Object[] 数组 + 双指针

Collection

ArrayList扩容机制

ArrayList的扩容机制如下:

  1. 当向ArrayList中添加第一个元素时,ArrayList会创建一个大小为10的数组。
  2. 当继续向ArrayList中添加元素时,如果当前元素个数已经等于数组大小,就需要扩容。扩容的大小默认为原来的容量大小加上原来容量大小的一半,也就是新的容量大小为原来容量大小的1.5倍。
  3. ArrayList在扩容时,会创建一个新的数组,将原有的元素复制到新的数组中,并将新的元素添加到新的数组中。
  4. 扩容后,ArrayList的容量会变成新的容量大小。

需要注意的是,ArrayList的扩容是一种比较耗时的操作,因为它需要将原有的元素复制到新的数组中。因此,在预知需要存储大量元素时,可以通过指定初始容量来减少扩容的次数,从而提高ArrayList的性能。另外,如果我们已经知道要添加的元素个数,也可以使用ArrayList的构造函数来指定初始容量,以避免不必要的扩容操作。

ArrayList 与 LinkedList 区别?

  • 是否保证线程安全: ArrayListLinkedList 都是不同步的,也就是不保证线程安全;
  • 底层数据结构: ArrayList 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
  • 插入和删除是否受元素位置的影响:
    • ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
    • LinkedList 采用链表存储,所以,如果是在头尾插入或者删除元素不受元素位置的影响,时间复杂度为 O(1),如果是要在指定位置 i 插入和删除元素的话(add(int index, E element)remove(Object o)), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入。
  • 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList(实现了RandomAccess接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
  • 内存空间占用: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间。

我们在项目中一般是不会使用到 LinkedList 的,需要用到 LinkedList 的场景几乎都可以使用 ArrayList 来代替,并且,性能通常会更好!就连 LinkedList 的作者约书亚 · 布洛克(Josh Bloch)自己都说从来不会使用 LinkedList

HashSet、LinkedHashSet、TreeSet

HashSetLinkedHashSetTreeSet 的主要区别在于底层数据结构不同。
HashSet 的底层数据结构是哈希表。
LinkedHashSet 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。
TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。

Queue 与 Deque 的区别

Queue 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则。

Queue 扩展了 Collection 的接口,根据 因为容量问题而导致操作失败后处理方式的不同 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值。

在这里插入图片描述

Deque 是双端队列,在队列的两端均可以插入或删除元素。

Deque 扩展了 Queue 的接口, 增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理方式的不同分为两类:

在这里插入图片描述

事实上,Deque 还提供有 push()pop() 等其他方法,可用于模拟栈。

PriorityQueue

PriorityQueue 是在 JDK1.5 中被引入的, 其与 Queue 的区别在于元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队。

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

Map

负载因子和阈值

负载因子:static final float DEFAULT_LOAD_FACTOR = 0.75f
map扩容判断时用的,也能够在构造函数中进行指定(不推荐),每次进行put的时候都会进行判断是否需要扩容,当size超过了阈值=总容量 * 负载因子,则会扩容,默认情况下初始总容量是16,负载因子是0.75,0.75是经过大量的实验证明该系数是最合适的。

如果设置过小,HashMap扩容频率太高,消耗大量的性能。

如果设置过大的话,当HashMap存储的数据数量接近总容量时,发生hash碰撞的概率将达到接近1,这违背的HashMap减少hash碰撞的原则。

头插与尾插

1.8采用的尾插法,1.7采用的头插法

当1.8中的桶中元素处于链表的情况,遍历的同时最后如果没有匹配的,直接将节点添加到链表尾部;而1.7在遍历的同时没有添加数据,而是另外调用了addEntry()方法,将节点添加到链表头部。

  1. put元素时为了计算链表长度,判断是否要转为红黑树

  2. rehash时,头插法会打乱节点顺序。容易造成死循环

HashMap的长度为什么是2的幂次方

数组下标的计算方法是“ (n - 1) & hash”。(n 代表数组长度)。

取余(%)操作中如果除数是 2 的幂次方则等价于与其除数减一的与(&)操作。
采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。

HashMap 和 Hashtable 的区别

  • 线程是否安全:HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰。
  • 效率: 因为线程安全的问题,HashMap 要比 Hashtable 效率高。另外,Hashtable 基本被淘汰,不要在代码中使用它;
  • 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException
  • 初始容量大小和每次扩充容量大小的不同 : 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小。
  • 底层数据结构: JDK1.8 以后的 HashMap 为数组+链表+红黑树,以减少搜索时间。Hashtable 没有这样的机制。

HashMap 和 TreeMap 区别

TreeMapHashMap 都继承自AbstractMap ,但是需要注意的是TreeMap它还实现了NavigableMap接口和SortedMap 接口。

在这里插入图片描述

实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。

实现SortedMap接口让 TreeMap 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。

ConcurrentHashMap 和 Hashtable 的区别

  • 底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 采用 数组+链表 的形式
  • 实现线程安全的方式(重要):
    • 在 JDK1.7 的时候,ConcurrentHashMap 对整个桶数组进行了分割分段(Segment,分段锁) ,Segment 继承自 ReentrantLock。每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。最大并发度是 Segment 的个数,默认是 16。
    • 到了 JDK1.8 的时候,ConcurrentHashMap 已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。最大并发度是 Node 数组的大小,并发度更大。
    • Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。

JDK1.7 的 ConcurrentHashMap

在这里插入图片描述

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。Segment 数组中的每个元素包含一个 HashEntry 数组,每个 HashEntry 数组属于链表结构。

首先将数据分为一段一段(这个“段”就是 Segment)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。

一个 ConcurrentHashMap 里包含一个 Segment 数组,Segment 的个数一旦初始化就不能改变Segment 数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。

JDK1.8 的 ConcurrentHashMap

Java8 ConcurrentHashMap 存储结构

数据结构跟 HashMap 1.8 的结构类似,数组+链表/红黑二叉树。ConcurrentHashMap 取消了 Segment 分段锁,采用 Node + CAS + synchronized 来保证并发安全。,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ponymate

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值