Java后端开发面经-Java集合

Java集合

RandomAccess接口

1. 概念


问:RandomAccess接口是什么?
答:RandomAccess接口是一个标记接口,如果List集合实现该接口,就可以支持快速随机访问。


2. ArrayList、LinkedList和RandomAccess接口


问:为什么ArrayList实现了RandomAccess接口,而LinkedList没有?
答:通过查看Collections类的二分查找方法(binarySearch)可知,实现RandomAccess接口的List集合采用一般的for循环遍历,而未实现RandomAccess接口的List集合采用迭代器遍历。 由于ArrayList采用for循环遍历的速度比迭代器遍历快,因此ArrayList实现了RandomAccess接口;而LinkedList采用迭代器遍历的速度比for循环遍历快,因此LinkedList未实现RandomAccess接口。


ArrayList

1. ArrayList的扩容机制


问:Arraylist的扩容机制是什么?
答:

  1. 新创建一个ArrayList对象且未添加任何元素时,ArrayList对象是一个默认的空数组,长度为0。当添加第一个元素(调用add方法)时,ArrayList对象内部的Object数组被开辟,默认长度为10;
  2. 添加元素时,首先判断是否大于默认容量10,如果小于默认容量,则直接添加;如果大于默认容量,则需要进行扩容(调用grow方法):
    1. 将ArrayList对象的容量扩充为原来的1.5倍,并与最小所需容量进行比较,取其中较大者作为新容量;
    2. 如果新容量未超出ArrayList定义的最大容量(MAX_ARRAY_SIZE = Integer最大值 - 8),则新容量即扩充容量;否则,进一步对最小所需容量和MAX_ARRAY_SIZE进行比较。若最小所需容量未超出MAX_ARRAY_SIZE,则扩充容量为MAX_ARRAY_SIZE,否则为Integer最大值。此外,若最小所需容量大于Integer最大值,将导致内存溢出错误;
    3. 创建一个新的Object数组,数组长度为扩充容量,并将原有数组中的数据复制到新数组中,返回一个全新的副本。

2. Array和ArrayList的比较


问:Array和ArrayList有什么区别?
答:

  1. Array可以容纳基本数据类型和引用数据类型的数据,但ArrayList只能容纳引用数据类型的数据;
  2. Array的大小是固定的,ArrayList的大小是动态分配的。


问:什么时候更适合用Array?
答:

  1. 当数组的大小已经确定,大部分情况是存储和遍历数据时,更适合用Array;
  2. 当遍历基本数据类型的数据时,更适合用Array。尽管Collections类使用自动装箱和拆箱来减轻编码任务,但ArrayList遍历的效率仍然较低;
  3. 对于多维数组,使用Array比ArrayList更容易。

HashMap

1. HashMap的实现原理


问:HashMap的实现原理/底层数据结构是什么,根据JDK1.7和JDK1.8分别论述?
答:

  1. 在JDK1.7中,HashMap的底层数据结构为数组 + 链表,采用链地址法(拉链法)解决散列冲突(哈希碰撞)。当出现散列冲突时,元素将以头插法的方式插入对应数组位置下的链表中,在多线程环境下扩容时头插法可能会导致环形链表死循环问题。
  2. 在JDK1.8中,HashMap的底层数据结构为数组 + 链表 / 红黑树,采用链地址法(拉链法)解决散列冲突(哈希碰撞)。当出现散列冲突时,元素将以尾插法的形式插入对应数组位置下的链表中。若链表的长度大于默认阈值8且数组的长度大于64,链表将会被转换为红黑树,以提高搜索和插入元素的效率。红黑树的本质是一颗自平衡的二叉查找树,查找的时间复杂度为 O ( l o g n ) O(logn) O(logn)

2. HashMap的put、get、resize方法


问:论述HashMap的put方法的执行过程?
答:

  1. 检查HashMap内部的索引数组是否为空,若为空,则调用resize方法对索引数组进行初始化;
  2. 计算Key的哈希值并得到Key在索引数组中的存储位置,若该位置上没有元素,则直接插入键值对;若该位置上已有元素,说明发生了散列冲突(哈希碰撞)。
  3. 发生散列冲突(哈希碰撞)时,将判断该位置上的元素是否为红黑树的根结点,如果是,则将键值对插入到红黑树中;否则说明该位置上是一个链表,采用尾插法将键值对插入到链表中。插入后,若链表的长度大于默认阈值8且索引数组的长度大于64,链表将会被转换为红黑树。在插入过程中,将不断根据Key和哈希值判断待插入的Key是否已存在,若已存在则以新值覆盖旧值,终止插入过程并返回旧值。
  4. 插入键值对后判断是否需要扩容,如果需要,则调用resize方法进行扩容。


问:论述HashMap的get方法的执行过程?
答:

  1. 检查HashMap内部的索引数组是否为空,若为空,直接返回null;
  2. 计算Key的哈希值并得到Key在索引数组中的存储位置,将Key与该位置上的第一个元素进行比较,若相同则直接返回相应的值;若不相同,判断该元素是否为红黑树的根结点。如果是,则调用红黑树的查找方法获得相应的值;否则说明该位置上是一个链表,通过遍历链表查找相应的元素并返回值,若未找到则返回null。


问:HashMap的get方法能否判断某个元素是否在map中?
答:不能。由于HashMap允许Key或Value的值为null,因此无法确定HashMap的get方法返回null时是HashMap不包含Key还是Key对应的Value的值为null。



问:论述HashMap的hash函数(扰动函数)的计算过程?
答:调用Key的类型自带的hash函数计算Key的哈希值(32位整型),再将此哈希值的高16位和低16位进行异或运算,得到最终的哈希值。扰动函数的设计思路是:使哈希值的分布尽可能均匀松散从而减少散列冲突(哈希碰撞),且算法需要尽可能高效,因此在函数中采用了异或和位运算。



问:HashMap在什么情况下进行扩容?
答:

  1. 第一次插入键值对时进行扩容,此时HashMap内部的索引数组为空;
  2. 链表转换为红黑树,且索引数组的长度小于64时进行扩容;
  3. HashMap中存储的键值对的数量大于扩容阈值时进行扩容。


问:论述HashMap的resize方法的执行过程(扩容机制)?
答:

  1. 若HashMap内部的索引数组非空,则对数组进行两倍扩容。若旧有数组的长度未达到最大值(数组长度的上限为 1 < < 30 1<<30 1<<30),则数组长度和扩容阈值均扩大到之前的两倍);若数组长度已达到最大值,则将扩容阈值设置为Integer最大值,直接返回而不继续进行扩容。若HashMap内部的索引数组为空,则设置默认数组长度为16,扩容阈值为12,负载因子为0.75(扩容阈值=数组长度 * 负载因子);
  2. 确定新的数组长度和扩容阈值后,通过rehash操作构建新的索引数组。具体而言,若旧有数组为空,则直接返回新的数组;若旧有数组非空,则需进行数据迁移。进行数据迁移时,将会遍历旧有数组的每一个槽位:
    1. 若槽位中的是一个普通结点,则该结点在新的数组中的存储位置为: e . h a s h   &   ( n e w C a p − 1 ) e.hash\ \& \ (newCap - 1) e.hash & (newCap1)
    2. 若槽位中的是一个红黑树根结点,则进行红黑树的迁移操作,红黑树根结点在新的数组中的存储位置计算方法同普通结点;
    3. 若槽位中的是一个链表结点,则将链表拆分为高位链表和低位链表,在新的数组中的存储位置分别为:旧有数组中的存储位置和(旧有数组中的存储位置 + 旧有数组的长度);
    4. 完成数据迁移后返回新的数组。


问:HashMap的容量为什么总是2的幂次方?
答:如此设计的目的是快速计算出Key在索引数组中的存储位置并且尽可能均匀分布,且扩容时仅需使容量左移一位。在源码中,计算Key的存储位置的语句为: ( n − 1 )   &   h a s h (n-1)\ \& \ hash (n1) & hash ,可知:当 n n n为2的幂次方时, n − 1 n-1 n1的二进制数全部为1。该语句相当于通过哈希值对 n n n进行取模,但位运算的效率要远高于直接取模。若输入的哈希值均匀分布,则计算出的存储位置也是均匀分布的。


3. HashMap和Hashtable的比较


问:HashMap和Hashtable的区别是什么?
答:

  1. 继承的类不同:HashMap继承于AbstractMap类,该类一个实现了Map接口的抽象类;而Hashtable继承于Dictionary类,该类是一个表示存储键值对的抽象类,没有实现任何接口,目前已过时;
  2. Key / Value允许的值不同:HashMap的Key和Value都可以为null,而Hashtable的Key和Value都不可以为null,否则会抛出NullPointerException(空指针异常);
  3. hash函数不同:添加元素时,HashMap调用的是自定义的hash函数,而Hashtable调用的是Key的类型自带的hash函数;
  4. 线程是否安全不同:HashMap不是线程安全的,除非调用Collections.synchronizedMap方法使HashMap实现线程同步;而Hashtable是线程安全的;
  5. 支持的遍历方式不同:HashMap只支持Iterator(迭代器)遍历;而Hashtable支持Iterator(迭代器)和Enumeration(枚举器)两种方式遍历。

ConcurrentHashMap

1. ConcurrentHashMap的实现原理


问:ConcurrentHashMap的实现原理/底层数据结构是什么,根据JDK1.7和JDK1.8分别论述?
答:

  1. 在JDK1.7中,ConcurrentHashMap采用了分段锁的设计思想,将数据分段存储,每段数据配一把锁,当一个线程占用锁并访问其中一段数据时,其他段的数据也能被其他线程访问,从而实现了真正的并发访问。此版本中ConcurrentHashMap的底层数据结构为Segment数组 + HashEntry数组 + 链表。每个ConcurrentHashMap包含一个Segment数组,其中Segment是一种继承于ReentrantLock的可重入锁,用于保护不同段的数据。每个Segment守护着一个HashEntry数组,HashEntry数组中的元素为链表结构,用于存储键值对。当调用put方法对HashEntry数组进行修改时,必须先获得与其对应的Segment锁;而get方法不需要加锁(除非读到空值时才加锁重读,出现此种情况意味着HashEntry的Key / Value没有映射完成就被其他线程所见),因为get方法不涉及增、删、改操作,不会引发并发故障,并且HashEntry涉及到的共享变量都使用了volatile关键字进行修饰,保证了共享变量对内存的可见性,因此读到的都是最新数据;
  2. 在JDK1.8中,ConcurrentHashMap抛弃了在JDK1.7中的分段锁机制,而是采用synchronized + CAS以实现线程安全。CAS是一种乐观锁,主要负责安全地修改对象的属性或数组上某个位置的值,用于索引数组的存储位置上不存在元素时插入键值对;synchronized关键字主要负责在需要操作某个不为空的位置时进行加锁,用于索引数组的存储位置上已存在元素时插入键值对。在JDK1.7中锁的粒度基于Segment,而在JDK1.8中锁的粒度基于Node结点,锁的粒度更小。此版本中ConcurrentHashMap的底层数据结构为Node数组 + 链表 / 红黑树。

2. ConcurrentHashMap和HashMap、Hashtable的比较


问:ConcurrentHashMap和HashMap的区别是什么?
答:

  1. 底层数据结构中结点的类型不同(JDK1.8之后)。ConcurrentHashMap相比HashMap多了转移结点,主要用于保证扩容时的线程安全;
  2. 线程是否安全不同。ConcurrentHashMap是线程安全的,在多线程环境下,可无需加锁直接使用;HashMap不是线程安全的,除非调用Collections.synchronizedMap方法使HashMap实现线程同步。


问:ConcurrentHashMap和Hashtable的区别是什么?
答:

  1. 底层数据结构不同。在JDK1.7中ConcurrentHashMap的底层数据结构为Segment数据 + HashEntry数组 + 链表,在JDK1.8中ConcurrentHashMap的底层数据结构为Node数组 + 链表 / 红黑树;而Hashtable的底层数据结构为数组 + 链表;
  2. 实现线程安全的方式不同。在JDK1.7中ConcurrentHashMap采用分段锁以实现线程安全,在JDK1.8中ConcurrentHashMap采用synchronized + CAS以实现线程安全;而Hashtable采用synchronized以实现线程安全。Hashtable中的方法通过synchronized关键字加了同步锁,这种加锁方式是针对整张哈希表的,效率十分低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,使得竞争越来越激烈,效率也越来越低。

LinkedHashMap


问:LinkedHashMap的实现原理是什么?
答:LinkedHashMap是基于HashMap实现的。在HashMap的基础上,LinkedHashMap通过维护一条双向链表记录了键值对的插入顺序,因此在使用Iterator(迭代器)遍历时,遍历顺序即键值对的插入顺序。


HashSet

1. HashSet的实现原理


问:HashSet的实现原理是什么?如何保证元素不重复?
答:HashSet的底层为HashMap。HashMap的Key存储HashSet的值,而Value则统一为PRESENT变量,该变量仅作为Key存储时的占位符,并无实际用处。由于HashMap的Key不允许重复,因此HashSet的值作为HashMap的Key时也不允许重复。


Iterator

1. Iterator的定义


问:Iterator是什么?
答:Iterator是Java迭代器的最简单实现,它不是一个集合,而是一种用于访问集合的方法,Iterator接口提供遍历任何Collection的接口。


Fail-fast和Fail-safe机制

1. Fail-fast和Fail-safe机制的定义和区别


问:论述Fail-fast和Fail-safe机制以及它们之间的区别?
答:从字面意思上看,Fail-fast是快速失败,Fail-safe是安全失败,它们都是集合类针对并发读写时的一种应对机制。它们的区别如下:

  1. Fail-fast广泛应用于java.util下的集合类中,其机制为:在使用迭代器进行遍历的过程中,如果集合对象的结构发生了改变(插入或删除元素,不包括修改元素值),将抛出ConcurrentModificationException(并发修改异常)。这是一种不支持并发读写的机制,优点是不会在遍历时额外消耗资源,且保证读到的为最新数据;
  2. Fast-safe广泛应用于java.concurrent下的集合类中,其机制为:在使用迭代器进行遍历时,会创建一个集合的视图,在该视图而非源数据上进行遍历。因此,如果在遍历的过程中集合对象的结构发生了改变,不会抛出异常。这是一种支持并发读写的机制,缺点是每次遍历时都会创建集合的视图,会消耗更多的资源,且有可能读到的不是最新数据。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值