java基础-集合

120 阅读6分钟

前言

13题。

说一说集合类的体系结构?

常见的集合主要有两大类,分别是单列集合双列集合

  1. 单列集合的顶级接口是Collection,它下面有两个主要的子接口分别是ListSetList的特点是元素有序的,可以重复的;Set的特点是元素无序的,不可重复的。List下我们常用的类有ArrayListLinkedList等,Set下我们常用的类有HashSetLinkedHashSetTreeSet等。
  2. 双列集合的顶级接口是Map,它的特点是键值对形式存在,且键不能重复。Map接口下我们常用的类有:HashMapLinkedHashMapTreeMap等。

聊聊集合类的底层数据结构?

集合主要分为单列集合双列集合

  1. 双列集合都是Map的实现类,主要有HashMapLinkedHashMapTreeMapHashMap: 数组+链表+红黑树;LinkedHashMap:继承自HashMap,在HashMap的基础上增加了一条双向链表;TreeMap:底层是红黑树。
  2. 单列集合主要是ListSetListArrayListLinkedListArrayList的底层是数组,查询快,增删慢;LinkedList的底层是双向链表,查询慢,增删快;SetHashSetLinkedHashSetTreeSet,它的实现原理和对应的Map是一样的,底层都是用的对应Mapkey实现。

ArrayList和LinkedList的区别?

ArrayListLinkedList都是单列集合,都是有序,可重复的。不同点有:

  • ArrayList的底层是动态数组,而LinkedList底层是双向链表。
  • ArrayList查询快,增删慢,适合查询场景;LinkedList查询慢,增删快,适合频繁修改的场景。
  • LinkedListArrayList更占内存,这是因为它的每个节点除了存储数据,还存储了前后节点的引用。

HashMap和HashTable的区别?

  • 调用put方法时,HashTable有锁保证线程安全,但效率较低;HashMap没有,但效率高。
  • HashMap较为常用,HashTable不太常用,且多线程场景一般会使用ConcurrentHashMap(采用了乐观锁和悲观锁)。

HashMap的底层原理?

HashMap的底层数据结构是哈希表,哈希表在JDK1.8之前是数组+链表实现,在JDK1.8之后是数组+链表+红黑树实现的。

下面以map中存储对象的流程来说一下它的实现原理:

  1. 当创建一个HashMap时,JDK就会在内存中创建一个长度为16的数组;

  2. 当调用put方法向HashMap中保存一个元素时,它会先调用keyhashCode方法计算出keyhash值;

  3. 然后用得到的hash值对数组长度取余,找出当前对象的元素在数组中的位置;

  4. 接着它会判断该位置上是否有元素,如果没有,就会将此元素直接存储到当前位置上;

  5. 如果算出的位置上有元素或者链表,它会再调用keyequals方法跟存在元素的key做比较;

  6. 如果有一个比较得到的结果为true,则会进行值的覆盖,如果都为false,则会将元素追加在链表的末尾。

为了降低Hash冲突和链表长度,HashMap还做了一些优化:

  • 当元素的数量超过数组大小与负载因子的乘积时,就会执行扩容,扩容为原来的2倍,并将原来数组中的键重新进行hash运算,然后分配到新数组中;
  • 当链表的长度>8,并且数组长度>=64时,链表会转换为红黑树,当红黑树结点数小于6时将再次转回为链表。

HashMap是怎么解决哈希冲突的?

解决hash冲突最常用的方式有链表法和开放地址法,HashMap采用了链表法。

当哈希冲突出现后,HashMap会在发生冲突的位置上创建一个链表来保存元素。在JDK1.8之后,又对此做出了改进,那就是当链表的长度>8,并且数组长度>=64的时候,链表就会转换为红黑树,使得效率更高。而当红黑树节点个数小于6时,红黑树又转换回到链表。

HashMap的扩容机制是怎样的?

HashMap中的元素个数超过数组长度乘以负载因子时,会重新分配一个更大的数组,将原来的元素重新计算哈希值并插入到新的数组中。

JDK1.8中,底层是调用resize方法实现扩容的。它的默认做法是:当元素个数超过数组长度的0.75倍时触发扩容,每次扩容的时候,都是扩容为原来的2倍, 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。

为何HashMap的数组长度一定是2的次幂?

  • 提高HashMap存取数据的效率;
  • 尽可能地减少hash值的碰撞;
  • 提高计算索引时和扩容时重新计算索引的效率。

说一下HashSet的实现原理?

HashSet是基于HashMap实现的。HashSet的值存放于HashMapkey上,因此HashSet的实现比较简单。HashSet的操作也基本上都是直接调用底层HashMap的相关方法来完成。由于HashMap的键是不能重复的,所以HashSet不允许重复的值。

HashSet如何检查重复?

HashSet底层不会产生链表

通过哈希表来实现,具体做法是:

  1. 在向HashSet中保存元素的时候,会先计算该元素的哈希值,确定他在数组中的位置;

  2. 如果该位置上没有元素,则保存成功,如果有元素,则调用equals方法去跟存在的每个值进行比较;

  3. 比较结果有一个相等,则直接丢弃;不相等,就存到HashSet中。

如何删除正在循环遍历的集合方法?

不能删除正在循环遍历的集合,要先将要删除的存到大集合中,循环遍历后删除大集合,有以下几种方法:迭代器(iterator),stream流(removeIf)--底层是迭代器。

集合中都有哪些类是线程安全的?

集合中线程安全的类,我知道的有:

  1. 双列集合:HashtableConcurrentHashMap
  2. 单列集合:VectorStack

此外还可以通过Collections.synchronized* 工厂方法创建线程安全的集合,例如Collections.synchronizedList(new ArrayList<>())

ArrayList和普通数组有什么区别?

  1. 类型限制:数组可以存储基本类型,而ArrayList存储的是对象引用,需要自动装箱和拆箱,对基本类型操作时会稍有性能损失。
  2. 内存分配:数组创建时分配的内存是连续的,而ArrayList虽然内部使用数组,但在扩容时可能会导致数据迁移,产生一定的性能开销。
  3. 使用场景:如果数据量固定且不大,且对性能要求极高,可以选择数组。反之,如果数据量动态变化,或需要频繁的增删操作,ArrayList会是更好的选择。

List的判空判null,掌握几种方法,直接说代码实现?

  1. 直接判空判null
  2. 使用CollectionUtils.isEmpty()Apache Commons Collections
  3. 使用Optional(alldata项目源码中还真有使用)
// 1、直接判空判null
if (list == null || list.isEmpty()) {
}
// 2、Apache Commons Collections
if (CollectionUtils.isEmpty(list)) {
}
// 3、jdk8以上
if (Optional.ofNullable(list).orElse(Collections.emptyList()).isEmpty()) {
}