Java集合 面试题

一、集合概念

1. 集合概念

  1. 集合是存储数据的容器
  2. 集合与数组的区别
    • 集合的长度是不固定的,数组是固定的
    • 集合只能存储引用类型的变量,数组能存放基本数据类型和引用类型
    • 一个集合里面可以存放不同类型的数据,数组只能存放同一种

2.常见集合类

  1. collection类:包括set、list、queue、stack

queue是一种先进先出(FIFO)队列,元素通过add方法插入队尾,通过poll方法从队首取出
stack是一种后进先出(LOFO)线性表,翻译为栈,元素通过push方法压入栈顶,通过pop方法从栈顶弹出,Java中的stack继承自vector,是线程安全的类

  1. map类

二、集合实现类

1. list类

list是一个有序可重复的容器,允许多个null值,元素都有索引

常用实现类:

1. arraylist:

  • 底层是数组,它实现了randomAccess接口,查找效率高,但是插入和删除元素的时候,需要做一次复制操作,效率就比较低

但是并不是每一次插入和删除都会涉及到数组元素的移动或复制:

  • 当插入时,当前数组空间不足,就会创建一个更大的数组,这时候需要将原数组的元素复制到新数组
  • 当删除时,当前数组剩余空间过大,就会进行缩容,创建一个更小的数组,也会涉及到元素的复制
  • 当进行数组尾部的插入和删除时,是不涉及到其他数组元素的移动的(当然前提是没有以上两种需要创建新数组的情况),因为ArrayList会维护一个指向数组尾部的指针,对于尾部的增删操作,只需要更新指针即可
  • 对于何时扩容和缩容,ArrayList有一个私有变量capacityIncrement,默认情况下,这个变量值为0,表示空间不足需要扩容时,新数组长度是旧数组长度的1.5倍,当这个值为正数时,每次扩容增加这个值数量的容量;而对于缩容,有一个条件:
    当前数组元素数量 <= 数组大小的一半 && (当前数组大小 > 初始容量)
    满足这个条件时缩容
  • 线程不安全
  • 如果要实现线程安全,可以使用controllers 下的synchronizedList()来实现,Collections.synchronizedList(list),或使用CopyOnWriteArrayList
  • 扩容:150%

2. vector:

  • 底层是数组
  • 比ArrayList多了一个线程安全,但不推荐使用,原因是全部方法通过synchronized加锁,性能较低
  • 扩容:100%

3. linkedlist:

  • 底层是数组 + 双向链表,因此正常情况下增删操作性能比ArrayList高

特殊情况包括:

  1. 当进行的是数组尾部的增删操作时,ArrayList性能比LinkedList高,因为LinkedList需要从头移动指针到尾部,而ArrayList不需要
  2. 当数组长度非常长时,ArrayList在增删时,移动元素的成本可能比LinkedList从头遍历元素到插入或删除位置的成本更小
  • 线程不安全
  • LinkedList继承与list和queue类,也可以作为一个先进先出队列使用

4. list类与数组的转换:

  • list – 数组:toArray()
  • 数组 – list:asList()

2.set类

set是一个无序不可重复的容器,只能有一个null值,经常在去重的时候使用

常用实现类:

1. hashset:

  • 无序唯一
  • 底层是HashMap

2. linkedhashset:

  • 继承于hashset
  • 底层是LinkedHashMap

3. treeset:

  • 有序唯一
  • 底层是红黑树

3.map类

map是一个键值对集合、存储值、值之间的映射,它的key是无序唯一的,只能有一个null值,value无序不要求唯一

常用实现类:

1. HashMap:

  • JDK1.8之前是数组 + 链表; JDK1.8是数组 + 链表 + 红黑树
  • 每次扩容为原来的200%

2. treemap:

  • 底层是红黑树

3. hashtable:

  • 数组 + 链表
  • 比HashMap多了一个线程安全,通过synchronized加锁

4. linkedHashMap:

  • 在HashMap的基础上加一条双向链表,提高插入效率

5. concurrentHashMap:

  • JDK1.8之前是segment + hashentry; JDK1.8是数组 + 链表 + 红黑树
  • 线程安全(JDK1.7之前分段锁,之后使用synchronized 和 CAS来控制并发)
  • 不允许null值

三、重要机制和原理

1. 快速失败机制:fail-fast

多线程访问一个集合时,如果一个线程正在访问的集合,被另一个线程修改了集合的结构,就会产生fail-fast,这是一种错误检测机制

2. 只读集合

Collection下有一个unmodifiableCollection方法可以用来创建一个只读的集合

3. 迭代器

  1. lterator接口提供了遍历所有Collection的接口(不包括map)
  2. iterator只能单向遍历
  3. 通过Collection. iterator()方法获取迭代器实例
  4. iterator . remove () 方法可以边遍历,边移除元素
  5. 如果需要双向遍历,则可以使用其子接口,listIterator

4. HashMap的实现机制

  1. HashMap是基于hash算法实现的
    • 在put一个元素时,需要使用key的hashcode计算出它在数组中的下标
    • 如果出现hash值相同的情况,则通过equals比较key是否相同,如果相同则用新值替换旧值,如果不同,则新值放入链表中
    • 在get一个元素时,通过hash值,找到数组的下标,再判断key值是否相同就可以获得value

Java8及以后put的过程:

  1. 根据HashCode计算key的hash值
  2. 初始化hashMap到初始容量
  3. 根据hash值计算在数组中的下标
  4. 找到下标,检测下标上是否元素,如果没有,则直接添加key
  5. 如果有元素,检查这个元素是否与插入的key相同, 如果相同,则新值替换旧值
  6. 如果不同,判断当前节点是否是链表节点,如果是,遍历链表,如果有相同的值,则替换,没有则插入,插入时需要记录链表长度,插入后需要判断是否要转红黑树并判断是否需要扩容
  7. 如果当前节点不是链表,那么就是红黑树,遍历红黑树,如果有相同值,则替换,没有则插入,插入完成后需要判断是否需要扩容

get的过程与put过程类似,只不过不需要初始化hashMap,在找到下标(索引位置)、或者遍历链表、红黑树的时候找到相同的返回对应值,没有返回null,不需要判断扩容和是否转红黑树

  1. JDK1.8中,HashMap底层的实现
    • 在数组长度大于64且链表长度大于8时,链表才转换成红黑树(数组长度小于64时,优先扩容)
    • 扩容机制:
      • 在HashMap初始化或者键值对数量大于阈值时调用resize()
      • 每次扩展都是扩展为原来长度的2倍
      • 扩容之后的长度都是2的次幂,如果指定扩容的长度不是2的次幂,那么其长度会自动扩容为比指定长度大的最小一个次幂数,比如,指定长度为7,实际扩容长度为8
      • 扩容阈值:0.75(时间与空间成本上的折中)

Java8及以后扩容过程

  1. 创建一个新数组,通常大小是原数组的两倍
  2. 重新计算原数组上每一个元素的hash值,确定原数组元素在新数组中的下标位置,具体计算公式为:
    (新数组长度 - 1) & hash(这个公式在Java8之前就开始使用)
  3. 如果发送碰撞,则根据节点类型(链表或红黑树)将碰撞的元素插入对应结构中
  4. 将扩容阈值扩大一倍
  5. 将此hashMap的table数组引用指向新数组地址
  1. hash冲突
    • 碰撞,不同元素通过hashcode()计算出来相同的hash值
    • 解决:hash()中做扰动,JDK1.7进行了9次扰动,1.8中值进行了两次(1次位运算,1次异或运算);引入红黑树,降低遍历的时间复杂度

5. CopyOnWriteArrayList的实现机制

  1. 实现机制:CopyOnWriteArrayList翻译为“写时复制”,在进行写入操作的时候,会复制这个数组,在新数组上进行修改,而读操作仍然在原始数组上,这样可以保证写时的线程安全
  2. 优点:通过不加锁的方式,实现了写操作的线程安全
  3. 缺点:由于是不加锁的方式,如果在写操作的同时,有另一个线程读取了原始数组的数据,那么它得到的将不是写入完成后的最新数据,这会造成数据的不一致;另外,由于写入时需要复制数组,在数据量大的时候,可能会影响性能
  4. 总结:CopyOnWriteArrayList是一个线程安全的List实现类,但是它是通过不加锁的方式实现的同步,在对数据实时性要求高的场景下并不适用,而且在写入时会涉及到数组复制,比较适合读多写少的场景

在实时性要求高的场景下,可以考虑使用Collections提供的synchronizedList方法生成一个对类中方法加锁的List

6. ConcurrentHashMap的实现机制

  1. JDK1.8之前:
    采用了分段式的方式实现线程安全,底层是数组,将一个数组分割成多个小段,加锁的时候锁住一个个小段,因此每个小段之间的操作是线程安全的,线程安全的实现方式可以总结为:分段 + 互斥锁

  2. JDK1.8及之后:
    互斥锁的开销是比较大比较影响性能的,所以在JDK1.8以后对ConcurrentHashMap进行了比较大的改动,主要包括以下两点:
    ①:底层数据结构修改为数组 + 链表 / 红黑树
    ②:分段的概念没有摒弃,只是加锁的方式从互斥锁修改为了CAS乐观锁

举个例子:
对于ConcurrentHashMap上的一个节点,有key1,某一时刻有AB两个线程尝试对其进行修改
线程A首先进入key1所在的分段,通过CAS操作,修改key1的值
同时,线程B进入,尝试进行CAS操作,但是会CAS失败,只能等待重试或者放弃,通过互斥的方式达到了线程安全

7.comparable和comparator

  • comparable是lang包下,使用compareTo(object obj)实现排序
  • Comparator是util包下,使用compare(object obj1 , object obj2)实现排序
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值