• Java集合容器面试题(2022最新版)


    Java集合容器面试题(2022最新版)

    常用的集合类有哪些?

    Map接口和Collection接口是所有集合框架的父接口:
    Collection接口的子接口包括:Set接口和List接口
    Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
    Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
    List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
    

    List,Set,Map三者的区别?List、Set、Map 是否继承自 Collection 接口?List、Map、Set 三个接口存取元素时,各有什么特点?

    Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。
    Collection集合主要有List和Set两大接口
    List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
    Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。
    Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。
    Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap
    

    集合框架底层数据结构

    Collection
    List
    Arraylist: Object数组
    Vector: Object数组
    LinkedList: 双向循环链表
    
    
    Set
    HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
    LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
    TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)
    Map
    
    HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
    LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
    HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
    TreeMap: 红黑树(自平衡的排序二叉树)
    
    

    哪些集合类是线程安全

    vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
    statck:堆栈类,先进后出。
    hashtable:就比hashmap多了个线程安全。
    enumeration:枚举,相当于迭代器。
    

    Java集合的快速失败机制 “fail-fast”?

    是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
    
    例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
    
    原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
    
    解决办法:
    
    1.在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
    2.使用CopyOnWriteArrayList来替换ArrayList
    

    怎么确保一个集合不能被修改?

    可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。
    

    Iterator 和 ListIterator 有什么区别?

    Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
    Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
    ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
    

    遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?

    遍历方式有以下几种:
    for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
    迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
    
    foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。
    
    最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。
    
    如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。
    如果没有实现该接口,表示不支持 Random Access,如LinkedList。
    推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。
     
    

    ArrayList的扩容机制

    arrayList扩容机制:
    如果是调用无参构造数组长度是0,使用有参的构造,回使用指定的容量作为数组长度,如果使用集合构造,使用集合的大小作为初始容量。
    add的扩容规则,首次扩容是10,再次扩容是上次的1.5倍
    addAll没有元素时,扩容是选择10和实际元素个数作为最大容量。有元素的时候,回在原容量的1.5倍和实际元素个数选最大值。
    

    说一下 ArrayList 的优缺点

    ArrayList的优点如下:
    
    ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
    ArrayList 在顺序添加一个元素的时候非常方便。
    ArrayList 的缺点如下:
    
    删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
    插入元素的时候,也需要做一次元素复制操作,缺点同上。
    ArrayList 比较适合顺序添加、随机访问的场景。
    
    

    ArrayList 和 LinkedList 的区别是什么?

    数据结构实现:ArrayList 是动态数组的数据结构实现,需要连续的内存,可以配合cpu缓存的局部性原理,提升相邻元素的查询效率,而 LinkedList 是双向链表的数据结构实现,不需要连续内存。
    随机访问效率:ArrayList实现了RandomAccess允许下标随机访问,ArrayList 比 LinkedList 在随机访问的时候效率要高,LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
    增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
    内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
    线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
    综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。
    

    List 和 Set 的区别

    List , Set 都是继承自Collection 接口
    List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
    Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。
    
    另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。
    Set和List对比
    Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
    List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变
    
    

    说一下 HashSet 的实现原理?

     HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。
    

    HashSet如何检查重复?HashSet是如何保证数据不可重复的?

    向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。
    HashSet 中的add ()方法会使用HashMap 的put()方法。
    HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。
    

    BlockingQueue是什么?

    Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。
    

    在 Queue 中 poll()和 remove()有什么区别?

    相同点:都是返回第一个元素,并在队列中删除返回的对象。
    不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。
    

    说一下 HashMap 的实现原理?

    HashMap是基于哈希表的Map接口的非同步实现,
    hashMap允许使用键值对为null,HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
    HashMap 基于 Hash 算法实现的,
    当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
    存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
    获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
    理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
    

    HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现

    	1.8之前将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
    	 jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
    

    为何要用红黑树,为何不一上来就树化,树化阈值为何是8,何时会树化,何时会退化为链表

    红黑树用来避免dos攻击的,防止链表超长造成性能下降,树化应当是偶然情况。
    因为树节点所占用的空间是普通节点的两倍,所以只有当节点足够多的时候,才会使用树节点。也就是说,最开始使用链表的时候,链表是比较短的,空间占用也是比较少的,查询性能都差不多,但是当链表越来越长,链表查询越来越慢,为了保证查询效率,这时候才会舍弃链表而使用红黑树,以空间换时间。
    树化有两个条件,链表长度超过树化的阈值,数组容量大于等于64
    退化情况,扩容的时候如果拆分树时候,树元素个数小于等于6会退化为链表
    在remove树节点的时候, 如果删除树的根节点,左孩子,右孩子,左孙子,有变成null,也会退化为链表。
    

    HashMap为什么会进行二次hash?

     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
     
     如果按照正常的hash/数组长度,数据挂在同一个数组里的数据会很多,通过二次hash会把数据更均匀的分布在数组的各个位置。
     code^(code>>>16)
    

    HashMap容量为何是2的n次幂

    计算索引如果是2的n次幂,可以使用位与运算代替取模,效率更高,扩容是Hash&oldCap==0的元素留在原来位置,否则新位置=旧位置+oldCap
    

    当容量不是二的n次幂行不行

    如果不是二的n次幂,可以选择一个比较大的质数作为数组容量,如果想数据分布比较好可以选择质数作为数组容量,还不用二次hash,如果追求性能使用2的n次幂。
    hashtable都不是2的n次幂,是上一次容量翻倍然后+1,.net有个类和hashmap比较像,使用的是数组容量翻倍然然后找一个比它大的最近质数作为容量。
    

    HashMap的put方法的具体流程?

    当我们put的时候,首先计算 key的hash值,这里调用了 hash方法,hash方法实际是让key.hashCode()与key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。
    ①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
    ②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
    ③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
    ④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
    ⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
    ⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
    
    

    hashMap的put1.7和1.8有什么不一样

    1.7头插法,1.8尾插法
    1.7大于阈值且当没有空位则触发扩容,1.8大于阈值就扩容
    1.7进行扩容后会导致死循环,1.8则进行优化
    

    加载因子为什么是0.75呢

    如果大于这个值,空间节省了,链表比较长
    如果小于这个值,冲突减少了,扩容频繁,空间占用多
    

    多线程下操作HashMap会有什么问题

    1.7会发生死链现象
    1.7和1.8都会有数据错乱现象
    

    hashmap的key可以为null吗,作为key的对象有什么要求

    key可以为null,key需要重写hashcode和eq方法,为了均匀分布。
    这个key的对象一定是一个不可变的对象,为了避免将来找不到
    

    HashMap的扩容操作是怎么实现的?

    ①.在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;
    
    ②.每次扩展的时候,都是扩展2倍;
    
    ③.扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
    
    在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
     
    

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

    主要是高低位与运算,也就是二次hash
    
    

    HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

    hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;
    

    HashMap 与 HashTable 有什么区别?

    线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
    效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
    对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。
    **初始容量大小和每次扩充容量大小的不同 **: ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
    底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
    推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。
    
    

    HashMap 和 ConcurrentHashMap 的区别

    ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
    HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。
    

    ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?

    JDK1.7
    
    首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
    
    在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下:
    
    一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。
    
    该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;
    Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。
    JDK1.8
    
    在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
    
    如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;
    如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount
    
    
  • 相关阅读:
    添加外部文件夹代码文件到项目中使用
    在.NET Core console application中使用User Secrets(用户机密)
    RBAC用户特别授权的思考
    Service Fabric部署笔记
    ubuntu12.04 rvm 搭建 Ruby on Rails
    thinkphp 查询当天 ,本周,本月,本季度,本年度,全部, 数据方法
    php 计算该用户IP在网页停留的时间
    ubuntu下安装sublime text2
    快速找到php方法-------php 反射。。
    QQ快速登录
  • 原文地址:https://www.cnblogs.com/q1359720840/p/16254666.html
Copyright © 2020-2023  润新知