Java集合常见问题(已完结)

1、说说有哪些常见集合?

集合相关类和接口都在java.util中,主要分为3种:List(列表)、Map(映射)、Set(集)。

其中Collection是集合ListSet的父接口,它主要有两个子接口:

  • List:存储的元素有序,可重复。
  • Set:存储的元素不无序,不可重复。

Map是另外的接口,是键值对映射结构的集合。

2.4 如何选用集合?

我们主要根据集合的特点来选择合适的集合。

比如: 需要根据键值获取到元素值时就选用 Map 接口下的集合

需要排序时选择 TreeMap 不需要排序时就选择 HashMap

扫描二维码关注公众号,回复: 17033324 查看本文章

需要保证线程安全就选用 ConcurrentHashMap

只需要存放元素值时,就选择实现 Collection 接口的集合

需要保证元素唯一时选择实现 Set 接口的集合比如 TreeSet 或 HashSet

不需要保证元素唯一时选择实现 List 接口的比如 ArrayList 或 LinkedList

需要队列时就选择Queue接口下的集合

单端队列就选择PriorityQueue 双端队列就选ArrayDeque

2.ArrayList和LinkedList有什么区别?

**(1)**数据结构不同

  • ArrayList基于数组实现
  • LinkedList基于双向链表实现

(2) 多数情况下,ArrayList更利于查找,LinkedList更利于增删

  • ArrayList基于数组实现,get(int index)可以直接通过数组下标获取,时间复杂度是O(1);LinkedList基于链表实现,get(int index)需要遍历链表,时间复杂度是O(n);当然,get(E element)这种查找,两种集合都需要遍历,时间复杂度都是O(n)。

  • ArrayList增删如果是数组末尾的位置,直接插入或者删除就可以了,但是如果插入中间的位置,就需要把插入位置后的元素都向前或者向后移动,甚至还有可能触发扩容;双向链表的插入和删除只需要改变前驱节点、后继节点和插入节点的指向就行了,不需要移动元素。

注意,这个地方可能会出陷阱,LinkedList更利于增删更多是体现在平均步长上,不是体现在时间复杂度上,二者增删的时间复杂度都是O(n)

**(3)**是否支持随机访问

  • ArrayList基于数组,所以它可以根据下标查找,支持随机访问,当然,它也实现了RandmoAccess 接口,这个接口只是用来标识是否支持随机访问。
  • LinkedList基于链表,所以它没法根据序号直接获取元素,它没有实现RandmoAccess 接口,标记不支持随机访问。

**(4)**内存占用,ArrayList基于数组,是一块连续的内存空间,LinkedList基于链表,内存空间不连续,它们在空间占用上都有一些额外的消耗:

  • ArrayList是预先定义好的数组,可能会有空的内存空间,存在一定空间浪费
  • LinkedList每个节点,需要存储前驱和后继,所以每个节点会占用更多的空间

3.ArrayList的扩容机制了解吗?

ArrayList是基于数组的集合,数组的容量是在定义的时候确定的,如果数组满了,再插入,就会数组溢出。所以在插入时候,会先检查是否需要扩容,如果当前容量+1超过数组长度,就会进行扩容。

ArrayList的扩容是创建一个1.5倍的新数组,然后把原数组的值拷贝过去。

4.快速失败(fail-fast)和安全失败(fail-safe)了解吗?

快速失败(fail—fast):快速失败是Java集合的一种错误检测机制

  • 在用迭代器遍历一个集合对象时,如果线程A遍历过程中,线程B对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
  • 原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
  • 注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
  • 场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改),比如ArrayList 类。

安全失败(fail—safe)

  • 采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
  • 原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
  • 缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
  • 场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改,比如CopyOnWriteArrayList类。
fail-fast 和 fail-safe 迭代器的区别是什么?
  • fail-fast 直接在容器上进行,在遍历过程中,一旦发现容器中的数据被修改,就会立刻抛出 ConcurrentModificationException 异常从而导致遍历失败。常见的使用 fail-fast 方式的容器有 HashMap 和 ArrayList 等。
  • fail-safe 这种遍历基于容器的一个克隆。因此对容器中的内容修改不影响遍历。常见的使用 fail-safe 方式遍历的容器有 ConcurrentHashMap 和 CopyOnWriteArrayList。

5、HashMap的底层数据结构是什么?

HashMap 的 7 种遍历方式与性能分析!(强烈推荐) (qq.com)

JDK 7 中,HashMap 由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。

在 JDK 8 中,HashMap 由“数组+链表+红黑树”组成。链表过长,会严重影响 HashMap 的性能,而红黑树搜索的时间复杂度是 O(logn),而链表是糟糕的 O(n)。因此,JDK 8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:

  • 当链表超过 8 且数据总量超过 64 时会转红黑树。
  • 将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。

6、为什么链表改为红黑树的阈值是 8?

理想情况下使用随机的哈希码,容器中节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的计算公式计算出了桶中元素个数和概率的对照表,可以看到链表中元素个数为 8 时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了 8,是根据概率统计而选择的。

红黑树节点的大小大概是普通节点大小的两倍,所以转红黑树,牺牲了空间换时间,更多的是一种兜底的策略,保证极端情况下的查找效率。

阈值为什么要选8呢?和统计学有关。理想情况下,使用随机哈希码,链表里的节点符合泊松分布,出现节点个数的概率是递减的,节点个数为8的情况,发生概率仅为0.00000006

至于红黑树转回链表的阈值为什么是6,而不是8?是因为如果这个阈值也设置成8,假如发生碰撞,节点增减刚好在8附近,会发生链表和红黑树的不断转换,导致资源浪费。

7、为什么在解决 hash 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。

当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。

因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。

8、为什么 hash 值要与length-1相与?

  • 把 hash 值对数组长度取模运算,模运算的消耗很大,没有位运算快。
  • 当 length 总是 2 的n次方时,h& (length-1) 运算等价于对length取模,也就是 h%length,但是 & 比 % 具有更高的效率。

9、HashMap 中 key 的存储索引是怎么计算的?

首先根据key的值计算出hashcode的值,然后根据hashcode计算出hash值,最后通过hash&(length-1)计算得到存储的位置

10、HashMap默认加载因子是多少?为什么是 0.75,不是 0.6 或者 0.8 ?

作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。

为什么HashMap的加载因子一定是0.75?而不是0.8,0.6?

11、解决hash冲突的办法有哪些?HashMap用的哪种?

解决Hash冲突方法有:

  • 开放定址法:也称为再散列法,基本思想就是,如果p=H(key)出现冲突时,则以p为基础,再次hash,p1=H(p),如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地址pi。因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素,而且因为存在再次hash,所以只能在删除的节点上做标记,而不能真正删除节点。
  • 再哈希法:双重散列,多重散列,提供多个不同的hash函数,当R1=H1(key1)发生冲突时,再计算R2=H2(key1),直到没有冲突为止。这样做虽然不易产生堆集,但增加了计算的时间。
  • 链地址法:拉链法,将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。
  • 建立公共溢出区:将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。

HashMap中采用的是链地址法 。

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

2 的 N 次幂有助于减少碰撞的几率。如果 length 为2的幂次方,则 length-1 转化为二进制必定是11111……的形式,在与h的二进制与操作效率会非常的快,而且空间不浪费。我们来举个例子,看下图:

当 length =15时,6 和 7 的结果一样,这样表示他们在 table 存储的位置是相同的,也就是产生了碰撞,6、7就会在一个位置形成链表,4和5的结果也是一样,这样就会导致查询速度降低。

如果我们进一步分析,还会发现空间浪费非常大,以 length=15 为例,在 1、3、5、7、9、11、13、15 这八处没有存放数据。因为hash值在与14(即 1110)进行&运算时,得到的结果最后一位永远都是0,即 0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的。

12、一般用什么作为HashMap的key?

一般用Integer、String 这种不可变类当作 HashMap 的 key,String 最为常见。

  • 因为字符串是不可变的,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。
  • 因为获取对象的时候要用到 equals() 和 hashCode() 方法,那么键对象正确的重写这两个方法是非常重要的。Integer、String 这些类已经很规范的重写了 hashCode() 以及 equals() 方法。

13、HashMap为什么线程不安全?

  • JDK 7 时多线程下扩容会造成死循环。
  • 多线程的put可能导致元素的丢失。
  • put和get并发时,可能导致get为null。

14、HashMap 的put方法流程?

以JDK 8为例,简要流程如下:

1、首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;

2、如果数组是空的,则调用 resize 进行初始化;

3、如果没有哈希冲突直接放在对应的数组下标里;

4、如果冲突了,且 key 已经存在,就覆盖掉 value;

5、如果冲突后,发现该节点是红黑树,就将这个节点挂在树上;

6、如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value

15.arrayList 和 linkedList 的区别? 

  • 1.ArrayList 是实现了基于数组的,存储空间是连续的。LinkedList 基于链表的,存储空间是不连续的。(LinkedList 是双向链表)

  • 2.对于随机访问 get 和 set ,ArrayList 觉得优于 LinkedList,因为 LinkedList 要移动指针。

  • 3.对于新增和删除操作 add 和 remove ,LinedList 比较占优势,因为 ArrayList 要移动数据。

  • 4.同样的数据量 LinkedList 所占用空间可能会更小,因为 ArrayList 需要预留空间便于后续数据增加,而 LinkedList 增加数据只需要增加一个节点

16、Collection 和 Collections 有什么区别?

  • Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。
  • Collections 是一个包装类,包含了很多静态方法、不能被实例化,而是作为工具类使用,比如提供的排序方法:Collections.sort(list);提供的反转方法:Collections.reverse(list)。

17、HashSet 中,equals 与 hashCode 之间的关系?

equals 和 hashCode 这两个方法都是从 object 类中继承过来的,equals 主要用于判断对象的内存地址引用是否是同一个地址;hashCode 根据定义的哈希规则将对象的内存地址转换为一个哈希码。HashSet 中存储的元素是不能重复的,主要通过 hashCode 与 equals 两个方法来判断存储的对象是否相同:

  • 如果两个对象的 hashCode 值不同,说明两个对象不相同。
  • 如果两个对象的 hashCode 值相同,接着会调用对象的 equals 方法,如果 equlas 方法的返回结果为 true,那么说明两个对象相同,否则不相同

18、HashMap 和 Hashtable 有什么区别?

  • 线程是否安全:
  • HashMap 是非线程安全的, Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程 安全的话就使用 ConcurrentHashMap 吧!,Hashtable已经过时了);
  • 效率:
  • 因为线程安全的问题, 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 在解决哈希冲突时有了较大的变化, 当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑 树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不 是转换为红黑树),以减少搜索时间。 Hashtable 没有这样的机制。。

19、ArrayList、Vector 和 LinkedList 有什么共同点与区别?

20、简述 Java 的 Set

Set 即集合,该数据结构不允许元素重复且无序。Java 对 Set 有三种实现方式:

HashSet 通过 HashMap 实现,HashMap 的 Key 即 HashSet 存储的元素,Value 系统自定义一个名为 PRESENT 的 Object 类型常量。判断元素是否相同时,先比较 hashCode,相同后再利用 equals 比较,查询 O(1)

LinkedHashSet 继承自 HashSet,通过 LinkedHashMap 实现,使用双向链表维护元素插入顺序。

TreeSet 通过 TreeMap 实现的,底层数据结构是红黑树,添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序。查询 O(logn)

21、简述 Java 的 HashMap

JDK8 之前底层实现是数组 + 链表,JDK8 改为数组 + 链表/红黑树。主要成员变量包括存储数据的 table 数组、元素数量 size、加载因子 loadFactor。HashMap 中数据以键值对的形式存在,键对应的 hash 值用来计算数组下标,如果两个元素 key 的 hash 值一样,就会发生哈希冲突,被放到同一个链表上。

table 数组记录 HashMap 的数据,每个下标对应一条链表,所有哈希冲突的数据都会被存放到同一条链表,Node/Entry 节点包含四个成员变量:key、value、next 指针和 hash 值。在 JDK8 后链表超过 8 会转化为红黑树。

若当前数据/总数据容量>负载因子,Hashmap 将执行扩容操作。默认初始化容量为 16,扩容容量必须是 2 的幂次方、最大容量为 1<< 30 、默认加载因子为 0.75。

22、为何 HashMap 线程不安全

HashMap不是线程安全的,可能会发生这些问题:

  • 多线程下扩容死循环。JDK1.7 中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8 使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。

  • 多线程的 put 可能导致元素的丢失。多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在 JDK 1.7 和 JDK 1.8 中都存在。

  • put 和 get 并发时,可能导致 get 为 null。线程 1 执行 put 时,因为元素个数超出 threshold 而导致 rehash,线程 2 此时执行 get,有可能导致这个问题。这个问题在 JDK 1.7 和 JDK 1.8 中都存在。

23、简述 Java 的 TreeMap

TreeMap 是底层利用红黑树实现的 Map 结构,底层实现是一棵平衡的排序二叉树,由于红黑树的插入、删除、遍历时间复杂度都为 O(logN),所以性能上低于哈希表。但是哈希表无法提供键值对的有序输出,红黑树可以按照键的值的大小有序输出。

24、ArrayList、Vector 和 LinkedList 有什么共同点与区别?

  • ArrayList、Vector 和 LinkedList 都是可伸缩的数组,即可以动态改变长度的数组。
  • ArrayList 和 Vector 都是基于存储元素的 Object[] array 来实现的,它们会在内存中开辟一块连续的空间来存储,支持下标、索引访问。但在涉及插入元素时可能需要移动容器中的元素,插入效率较低。当存储元素超过容器的初始化容量大小,ArrayList 与 Vector 均会进行扩容。
  • Vector 是线程安全的,其大部分方法是直接或间接同步的。ArrayList 不是线程安全的,其方法不具有同步性质。LinkedList 也不是线程安全的。
  • LinkedList 采用双向列表实现,对数据索引需要从头开始遍历,因此随机访问效率较低,但在插入元素的时候不需要对数据进行移动,插入效率较高。

25、 HashMap 和 HashSet 区别

HashSet 底层就是基于 HashMap 实现的。

实现接口不同

HashMap 实现Map接口

HashSet 实现Set接口

存储不同

HashMap 存储键值对

HashSet 存储对象

添加方式不同

HashMap 调用put()添加元素

HashSet 调用add()添加元素

计算HashCode不同

HashMap 使用键(Key)计算 hashcode

HashSet 使用成员对象来计算 hashcode 值,对于两个不同的对象来说 hashcode 可能相同,所以还要用 equals() 方法用来判断对象的相等性

26.jdk1.8对HashMap主要做了哪些优化呢?为什么?

jdk1.8 的HashMap主要有五点优化:

  1. 数据结构:数组 + 链表改成了数组 + 链表或红黑树

    原因:发生 hash 冲突,元素会存入链表,链表过长转为红黑树,将时间复杂度由O(n)降为O(logn)

  2. 链表插入方式:链表的插入方式从头插法改成了尾插法

    简单说就是插入时,如果数组位置上已经有元素,1.7 将新元素放到数组中,原始节点作为新节点的后继节点,1.8 遍历链表,将元素放置到链表的最后。

    原因:因为 1.7 头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环。

  3. 扩容rehash:扩容的时候 1.7 需要对原数组中的元素进行重新 hash 定位在新数组的位置,1.8 采用更简单的判断逻辑,不需要重新通过哈希函数计算位置,新的位置不变或索引 + 新增容量大小。

    原因:提高扩容的效率,更快地扩容。

  4. 扩容时机:在插入时,1.7 先判断是否需要扩容,再插入,1.8 先进行插入,插入完成再判断是否需要扩容;

  5. 散列函数:1.7 做了四次移位和四次异或,jdk1.8只做一次。

    原因:做 4 次的话,边际效用也不大,改为一次,提升效率

27、你还知道哪些哈希函数的构造方法呢?

HashMap里哈希构造函数的方法叫:

  • 除留取余法:H(key)=key%p(p<=N),关键字除以一个不大于哈希表长度的正整数p,所得余数为地址,当然HashMap里进行了优化改造,效率更高,散列也更均衡。

除此之外,还有这几种常见的哈希函数构造方法:

  • 直接定址法

    直接根据key来映射到对应的数组位置,例如1232放到下标1232的位置。

  • 数字分析法

    key的某些数字(例如十位和百位)作为映射的位置

  • 平方取中法

    key平方的中间几位作为映射的位置

  • 折叠法

    key分割成位数相同的几段,然后把它们的叠加和作为映射的位置

28.你对红黑树了解多少?为什么不用二叉树/平衡树呢?

红黑树本质上是一种二叉查找树,为了保持平衡,它又在二叉查找树的基础上增加了一些规则:

  1. 每个节点要么是红色,要么是黑色;
  2. 根节点永远是黑色的;
  3. 所有的叶子节点都是是黑色的(注意这里说叶子节点其实是图中的 NULL 节点);
  4. 每个红色节点的两个子节点一定都是黑色;
  5. 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点;

之所以不用二叉树:

红黑树是一种平衡的二叉树,插入、删除、查找的最坏时间复杂度都为 O(logn),避免了二叉树最坏情况下的O(n)时间复杂度。

之所以不用平衡二叉树:

平衡二叉树是比红黑树更严格的平衡树,为了保持保持平衡,需要旋转的次数更多,也就是说平衡二叉树保持平衡的效率更低,所以平衡二叉树插入和删除的效率比红黑树要低

红黑树怎么保持平衡的知道吗?

红黑树有两种方式保持平衡:旋转染色

  • 旋转:旋转分为两种,左旋和右旋

 29.HashMap 内部节点是有序的吗?

HashMap是无序的,根据 hash 值随机插入。如果想使用有序的Map,可以使用LinkedHashMap 或者 TreeMap。

30、HashMap不是线程安全的,可能会发生这些问题:

  • 多线程下扩容死循环。JDK1.7 中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8 使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。

  • 多线程的 put 可能导致元素的丢失。多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在 JDK 1.7 和 JDK 1.8 中都存在。

  • put 和 get 并发时,可能导致 get 为 null。线程 1 执行 put 时,因为元素个数超出 threshold 而导致 rehash,线程 2 此时执行 get,有可能导致这个问题。这个问题在 JDK 1.7 和 JDK 1.8 中都存在。

猜你喜欢

转载自blog.csdn.net/pachupingminku/article/details/132692497