Java集合部分面试梳理(一)

集合结构图介绍

Java集合部分面试梳理(一)_第1张图片

集合实战图介绍

本图片转载而来,原作者链接:https://blog.csdn.net/qq_36711757/article/details/80464499
Java集合部分面试梳理(一)_第2张图片

本篇介绍内容:

  • 1.ArrayList、LinkedList和Vector的区别?
  • 2.说说 ArrayList,Vector, LinkedList 的存储性能和特性?
  • 3.快速失败 (fail-fast) 和安全失败 (fail-safe) 的区别是什么?
  • 4.HashMap 的数据结构?
  • 5.HashMap 的工作原理是什么?

1、ArrayList、LinkedList和Vector的区别?

1)相同点
  • ArrayList 、LinkedList、Vector均为线型的数据结构
2)底层实现
  • ArrayList和Vector内部采用数组来实现
  • LinkedList内部采用双向链表来实现
3)线程安全
  • ArrayList 、LinkedList是非线程安全
  • Vector是基于synchronized实现的线程安全的ArrayList
  • 单线程下尽量使用ArrayList,Vector因为同步会有性能损耗;即使在多线程环境下,我们可以利用Collections这个类中为我们提供的synchronizedList(List list)方法返回一个线程安全的同步列表对象,从而实现线程安全的问题
4)读写效率
  • ArrayList和Vector查询快,增删慢。由于ArrayList和Vector是基于动态数组的数据结构实现,在尾端操作数据开销是固定的,但是如果不是在尾端,可能会导致数据重新分配,而且结尾会预留一定的容量空间(增删要移动数据,所以慢)
  • LinkedList查询慢,增删快。由于LinkedList是基于链表的数据结构实现,访问集合数据LinkedList要移动指针,从一端向另一端查找,效率很低。但增删操作时对于LinkedList开销是统一的,只要分配一个Entry对象即可!(查询要移动指针,所以慢)
5)空间占用情况
  • ArrayList存储区间连续,占用空间大,空间复杂度大。空间不足时默认增加50%,有三个构造器,默认长度为10,不可设置容量增长参数
  • Vector存储区间连续,占用空间大,空间复杂度大。空间不足时默认增加原来1倍,有四个构造器,默认长度为10,可设置容量增长参数
  • LinkedList存储区间离散,占用空间宽松,空间复杂度小。

2、说说 ArrayList,Vector, LinkedList 的存储性能和特性?

  • ArrayList和Vector底层都是数组方式存储数据,其中数组元素数大于实际存储的数据以便增加和插入元素,而且也都允许直接按序号查询元素,但插入元素要涉及数组元素移动等内存操作,所以查询数据快而插入数据慢
  • Vector方法添加了synchronized修饰,因此Vector是线程安全的容器,但性能上较ArrayList差,因此已经是Java中的遗留容器
  • LinkedList底层使用双向链表实现数据的存储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号查询的线性结构,这种链式存储方式与数组的连续存储方式相比,内存的利用率更高),按序号查询数据需要进行前向或后向遍历,但插入数据时只需要记录本项的前后项即可,所以插入速度较快
  • 由于ArrayList和LinkedList都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类Collections中的synchronizedList方法将其转换成线程安全的容器后再使用

3、快速失败 (fail-fast) 和安全失败 (fail-safe) 的区别是什么?

1)快速失败 (fail-fast)
  • 定义:在迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception异常
  • 原理描述:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历
  • 使用注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug
  • 使用场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)
2)安全失败 (fail-safe)
  • 定义:采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,然后在拷贝的集合上进行遍历
  • 原理描述:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception异常
  • 使用注意:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的
  • 使用场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改
3)快速失败(fail-fast)和安全失败(fail-safe)的比较
  • Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常
  • fail-fast机制,是一种错误检测机制。它只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生。若在多线程环境下使用fail-fast机制的集合,建议使用“java.util.concurrent包下的类”去取代“java.util包下的类”
  • fail-safe机制需要复制集合,产生大量的无效对象,因此开销大,而且无法保证读取的数据是目前原始数据结构中的数据
4)ArrayList和CopyOnWriteArrayList的区别的区别
  • 和ArrayList继承于AbstractList不同,CopyOnWriteArrayList没有继承于AbstractList,它仅仅只是实现了List接口
  • ArrayList的iterator()函数返回的Iterator是在AbstractList中实现的;而CopyOnWriteArrayList是自己实现Iterator
  • ArrayList的Iterator实现类中调用next()时,会“调用checkForComodification()比较‘expectedModCount’和‘modCount’的大小”;但是,CopyOnWriteArrayList的Iterator实现类中,没有所谓的checkForComodification(),更不会抛出ConcurrentModificationException异常!
5)更多知识点参考
  • https://www.cnblogs.com/songanwei/p/9387745.html
  • https://www.cnblogs.com/shamo89/p/6685216.html

4、HashMap 的数据结构?

1)没有哈希冲突
  • 为数组,支持动态扩容
2)有哈希冲突时
  • 当冲突长度小于8或数组长度小于64(MIN_TREEIFY_CAPACITY默认值为64)时,为数组+链表(Node)
  • 当冲突长度大于8时,为数组+红黑树/链表(TreeNode),其中红黑树用于快速查找,链表用于遍历
3)红黑树
  • HashMap中的TreeNode是红黑树的实现
  • TreeNode实现的几个方法(左旋转、右旋转和插入)
4)JDK7与JDK8中应用的区别
  • JDK7中HashMap采用的是位桶+链表的方式,即我们常说的散列链表的方式。(HashMap底层维护一个数组table, 数组中的每一项是一个key,value形式的Entry。HashMap的结构都是这么简单,基于一个数组以及多个链表的实现,hash值冲突的时候,就将对应节点以链表的形式存储)
  • JDK8中采用的是位桶+链表/红黑树的方式,也是非线程安全的。(JDK8中,当同一个hash值的节点数大于等于8时,将不再以单链表的形式存储了,会被调整成一颗红黑树)
5)更多知识点参考
  • https://www.cnblogs.com/wang-meng/p/7545725.html
  • https://blog.csdn.net/zhangcanyan/article/details/79347477

5、HashMap 的工作原理是什么?

1)工作原理
  • 通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高查询数据的速度,但是遍历的时候还是使用链表来实现的
2)使用场景
  • 是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的,HashMap存储着Entry(hash, key, value, next)对象
3)容量(Capacity)、负载因子(Load factor) 和扩容阈值(threshold)介绍
  • Capacity就是buckets的数目,Load factor就是buckets填满程度的最大比例。如果对迭代性能要求很高的话不要把capacity设置过大,也不要把load factor设置过小。当bucket填充的数目(即hashmap中元素的个数)大于capacity*load factor时就需要调整buckets的数目为当前的2倍,一般扩容的时候会考虑到
  • 容量(capacity):HashMap中数组的长度
  • 加载因子(Load factor):HashMap在其容量自动增加前可达到多满的一种尺度,默认加载因子 = 0.75
  • 扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表
4)get和put的原理以及equals()和hashCode()的都有什么作用
  • put函数的实现
    1.对key的hashCode()做hash,然后再计算index;
    2.如果没碰撞直接放到bucket里;
    3.如果碰撞了,以链表的形式存在buckets后;
    4.如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
    5.如果节点已经存在就替换old value(保证key的唯一性)
    6.如果bucket满了(超过load factor*current capacity),就要resize
  • get函数的实现
    1.bucket里的第一个节点,直接命中;
    2.如果有冲突,则通过key.equals(k)去查找对应的entry
    若为树,则在树中通过key.equals(k)查找,O(logn);
    若为链表,则在链表中通过key.equals(k)查找,O(n)
  • equals()和hashCode()的实现
    通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点
5)更多知识点参考
  • https://www.jianshu.com/p/75901a34ae2b
  • https://blog.csdn.net/weixin_40197494/article/details/84110807

你可能感兴趣的:(面试锦集,集合)