面试-集合框架

74 阅读9分钟

1.说说有哪些常见的集合?

集合分为两大类,分别是Collection接口和Map接口,Collection分为List和set,Queue,List有LinkedList,ArrayList,vector;Set有HashSet和TreeSet,而Map有HashMap,TreeMap和HashTable,List存储的元素有序,可重复,而set存储的元素无序不可重复的,去重可以使用Set中的TreeSet,Map是键值对映射的集合;

2.ArrayList和LinkedList的区别?

ArrayList它是基于数组实现的,而LinkedList它是基于双向链表实现的;

多数情况下ArrayList用于查询,LinkedList用于增删;

当随机访问List时候,ArrayList比LinkedList的效率要高,因为LinkedList是链表的数据存储方式,它是从前往后依次去查找,而ArrayList可以直接根据下标去查询,就会比较快一点;

当数据进行添加或删除时,LinkedList就会比ArrayList的效率高,因为ArrayList是数组,是一块连续的内存空间,如果增删是数组末尾的位置,直接插入或删除就可以了,但是如果插入中间的位置,会对操作之后所有数据的下标索引造成影响,需要进行数据的移动,甚至还有可能触发扩容;而LinkedList只需要改变前后节点的指向就行了,不需要移动元素;如果我们讨论的是获取第一个元素,或最后一个元素,ArrayList和LinkedList在性能上是没有区别的,因为LinkedList中有两个属性分别记录了当前链表中的头结点和尾结点,并不需要遍历链表;

3.ArrayList扩容机制

ArrayList是基于数组的集合,数组的容量是在定义的时候确定的,如果数组满了,再插入,就会数组溢出. 所以在插入的时候,会先检查是否需要扩容,如果当前容量+1超过数组长度,就会进行扩容,ArrayList的扩容是创建一个1.5倍的新数组,然后把原数组中的元素拷贝一份到新的数组中;这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。

4.ArrayList如何制定底层数组大小的?

1.如果使用无参构造函数初始化ArrayList后,他当时的数组容量是0,是因为刚开始的时候给底层的Object数组也就是elementData 赋值了一个默认的空数组

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

只有在我们真正添加操作(add)的时候,才会给数组分配一个默认的初始容量

 private static final int DEFAULT_CAPACITY = 10;

2.如果是使用有参构造,则是根据用户指定的大小开辟数组空间

5.ArrayList是怎么序列化的,为什么用transient修饰数组?

ArrayList通过readObject和writeObject两个方法自定义序列化和反序列化,实际直接使用两个流ObjectOutputStream和ObjectInputStream来进行序列化和反序列化; 使用transient修饰的变量不能被序列化

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

快速失败是Java集合的一种错误检测机制

在使用迭代器遍历一个集合对象时,如果线程A遍历过程中,线程B对集合对象的内容进行了修改(增删改),就会抛出异常

原理: 因为在遍历过程中它会使用一个modCount变量,集合如果在遍历期间内容发生变化,就会改变modCount的值,每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount的值,是的话就遍历,不是则抛出异常,终止遍历

场景:不能在多线程下发生并发修改,比如ArrayList类

安全失败机制,它是在遍历的时候先复制原有的集合内容,在拷贝的集合上进行遍历,而不是在遍历的时候直接去集合内容上访问的

原理: 因为它是在拷贝的集合上进行遍历,所以在遍历过程中对原集合修改并不能被迭代器检测到,所以就不会触发异常 场景:可以在多线程下并发使用修改,比如CopyOnWriteArrayList类

7.有哪几种实现ArrayList线程安全的方法?

1.使用Vector代替ArrayList(不推荐,因为它是一个历史遗留类)

2.使用Conllections.synchronizedList包装ArrayList,然后操作包装后的list

3.使用CopyOnWriteArrayList代替ArrayList

4.在使用ArrayList时,应用程序通过同步机制去控制ArrayList的读写

8.说一下CopyOnWriteArrayList?

CopyOnWriteArrayList线程安全版本的ArrayList,它采用了一种读写分离的并发策略,允许并发读,是无锁的,性能较高; 写操作:比如向容器中添加一个元素,则首相会将当前容器复制一份,然后在新的副本上执行写操作,结束之后再将原容器的引用指向新容器

9.什么是红黑树?为什么不用二叉树/平衡树呢?

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

1.每个节点要么是红色,要么是黑色

2.根节点永远都是黑色的

3.所有的叶子节点都是黑色的(null节点)

4.每个红色节点的两个子节点一定都是黑色

5.从任一节点到其子树中每个叶子节点的路劲都包含相容数量的黑色节点

之所以不用二叉树:是因为红黑树是一种平衡的二叉树,插入,删除,查询的最坏时间复杂度都为O(logn),避免了二叉树最坏情况下的O(n)时间复杂度 而之所以不用平衡二叉树:是因为平衡二叉树是比红黑树更严格的平衡树,为了保持平衡,需要旋转的次数更多,也就是说平衡二叉树保持平衡的效率更低,插入,删除的效率要比红黑树低

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

红黑树保持平衡的方式有两种:旋转和染色; 旋转分为左旋和右旋

11.讲一下HashMap的put流程?

(1)首相进行哈希值的扰动,获取一个新的哈希值

(2)判断tab数组是否为null或者长度为0,如果是则进行扩容

(3)根据哈希值计算下标,如果该下标的位置没有数据,那么就直接把数据存进去,有则覆盖

(4)判断tab[i]是否是树节点TreeNode,是则在红黑树中直接插入,不是则插入链表中

(5)如果链表中插入节点的时候,链表长度大于8,数组长度大于或者等于 64 的情况下,链表转红黑树,否则则扩容

(6)最后所有元素处理完成后,判断是否超过阈值(threshold),超过则扩容

注:函数扰动:通过将高位向右移动的方法,再将高位和低位进行异或操作 扰动的目的: 为了使计算出的数组下标更加均匀,减少冲突发生的可能性

12.HashMap是怎么查找元素的?

首先会根据key计算hashCode,然后得出其数组下标(位置),然后看第一个节点是否和key匹配,是则直接返回,不是则看第一个节点是否为树节点,是则查找红黑树,不是则遍历链表查询

13.HashMap的哈希/扰动函数是怎么设计的?

HashMap的哈希函数是先拿到key的hashcode,是一个32位的int类型的数值,然后让hashcode的高16位和低16位进行异或操作,这么设计的目的是为了降低哈希碰撞的概率

14.解决hash冲突有那些方法呢?

1.链地址法:在冲突的位置拉一个链表,把冲突的元素放进去;

2.开放定址法:从冲突的位置再接着往下找,给冲突元素找个空位;

找空闲位置的方法:

(1)线行探查法: 从冲突的位置开始,依次判断下一个位置是否空闲,直到找到空闲的位置

(2)平方探查法: 从冲突的位置开始,第一次增加1的平方个位置...直到找到空闲位置

3.再哈希法:换种哈希函数,重新计算冲突元素的地址

4.建立公共溢出区:再建一个数组,把冲突的元素放进去

15.HashMap是线程安全的吗?多线程下会有什么问题?

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

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

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

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

16.有什么办法能解决HashMap线程不安全的问题呢?

1.使用HashTable,它是直接在操作方法上加synchronized关键字,锁住整个table数组,粒度比较大

2.使用Collections.synchronizedMap,它是使用Collection集合工具的内部类,通过传入Map封装出一个 SynchronizedMap对象,内部定义了一个对象锁,方法内通过对象锁实现;

3.使用ConcurrentHashMap,它在jdk1.7中使用分段锁实现,在jdk1.8中使用CAS+synchronized实现

17.说说HashMap扩容机制?

18.说一下ConcurrentHashMap的实现?