Java集合面试题总结
Posted z啵唧啵唧
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java集合面试题总结相关的知识,希望对你有一定的参考价值。
文章目录
- 集合类常见面试题总结
- 1、Java中常见的集合
- 2、容器中那些那些是线程安全的,那些不是线程安全的
- 3、Map接口的实现类
- 4、Map的put过程(源码分析)
- 5、得到一个线程安全的Map的方法
- 6、HashMap的特点
- 7、Jdk7和jdk8中HashMap的区别
- 8、HashMap的扩容机制
- 9、HashMap中循环链表的产生
- 10、如何将HashMap实现线程安全呢?
- 11、HashMap和ConcurrentHashMap的区别
- 12、LinkedHashMap
- 13、TreeMap
- 14、ArrayList和LinkedList
- 15、那些List是线程安全的
- 16、HahsSet底层接结构
- 17、TreeSet和HashSet的区别
集合类常见面试题总结
1、Java中常见的集合
- Java中常见的集合主要是由Collection和Map这两个接口派生出来的,其中Conllection又派生出三个子接口分别是Set、List、Queue。Java中所有的集合都是Set、List、Queue、Map这四个接口下面的实现类,这四个接口将集合分成了四大类。
- Set表示无序、元素不可重复的集合
- List表示有序,元素可以重复的集合
- Queue表示先进先出的队列
- Map表示具有映射关系的(key-value)集合
2、容器中那些那些是线程安全的,那些不是线程安全的
线程不安全
- java.util下面的集合类大部分都不是线程安全我们常用的HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap这些都不是线程安全的,但是他们性能更好。
线程安全
- Vector和Hashtable,这些集合是线程安全的,也比较古老,他们虽然线程安全但是他们的效率很低,即便想要使用线程安全的集合类,也建议采用集合的工具类中的方法,将集合包装成为线程安全的类,不建议采用古老的线程安全的集合。
- 从java5开始,Java.util.concurrent包下就支持了大量的支持高并发的集合类,他们既有不错的性能,又能够实现线程安全。
3、Map接口的实现类
- Map接口有很多实现类,其中比较常见的有HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap。
- 在不需要排序的场景下,我们优先使用HashMap,因为它的性能最好,如果要保证线程安全,可以使用ConcurrentHashMap,它的性能远好于HashTable,因为他在put的时候采用的是分段锁/CAS,而不是像HashTable那样无论是get/put都做了同步处理
- 对于需要排序的场景我们可以使用LinkedHashMap,LinkedHashMap插入的元素输出顺序和插入顺序一致,但是HashMap的插入顺序和输出顺序不一定一致,如果需要让插入的顺序按照(Key)大小顺序排列输出,我们可以采用TreeMap(),注意TreeMap()传入的类型一定是可以排序的类型(实现了Comparator,或者Comparable接口的类型)
4、Map的put过程(源码分析)
以HashMap为例
-
1、首次扩容:先判断数组是否为空,如果数组为空则对数组进行第一次扩容
-
2、索引计算,通过Hash算法,计算键值再数组当中的位置
-
3、插入数据:
- 如果当前位置元素为空,则直接插入元素
- 如果当前元素非空,表示key已经存在,将新添加进来的value值覆盖到原先的value上
- 如果当前元素非空,且key不存在,则将数据添加到链表末端
- 如果链表长度达到了8,则将链表转换,判断数组的长度是否大于64。大于的话就将链表转换成为红黑树
- 再次扩容,如果数组中元素的个数,超过了threshold,则再次进行扩容操作
-
HashMap的put<K key,V value> 方法详解
-
起始插入第一个元素,判断数组是否为空,如果为空,先对数组进行扩容,jdk默认HashMap初始化大小为16,也就是扩容成16大小,然后根据键值Key通过hash值算法计算出hash值找到在数组中的索引i
-
扩容的方法resize()
-
数组在索引i处是否为空,如果为空直接插入
-
如果不为空,如果不为空说明i位置处有元素,会发生hash碰撞,形成链表
-
p.hash是原本tab[i]处的hash,hash是我传入和hash,如果他俩相等,并且tab[i]处的key和我传入的key相等的,则直接覆盖掉原先位置处的元素,(因为只要key相等,通过同样的hash计算法一定会找到同样的i位置)
-
key不存在,判断是否是tab[i]是否是treeNoe类型,如果是直接将其插入到红黑树中(此时链表转换成为红黑树条件为:链表长度>,并且数组长度大于64)
-
如果tab[i]不是树,开始遍历链表,当链表长度大于8,调用treeifBin方法,预备将链表转换成为红黑树,如果在8之前找到了key存在的位置,也就直接插进去了。
-
treeifBin方法
-
当元素添加进去之后,最后还要判断一下,当这个元素添加进去之后,判断一下size的值是否大于扩容阈值,进行扩容。
-
put方法流程图
-
5、得到一个线程安全的Map的方法
- 使用HashTable代替HashMap进行使用,但是HashTable性能太差了不建议使用。
- 使用Collections集合工具类这个包下的SynchronizedMap来将我们线程不安全的map进行包装,将其编程线程安全的map
- 用java.util.concurrent包下的map,比如ConcurrentHashMap
6、HashMap的特点
- 非线程安全、性能好、效率高
- HashMap的key和value都可以为空,但是只能有一个key为null
7、Jdk7和jdk8中HashMap的区别
- 在jdk7中HashMap是基于数组+链表实现的,但是在jdk8中HashMap是基于数组+链表+红黑树进行实现的
- 在jdk7中的HashMap没有树化的操作,这样就会导致,当发生hash冲突的时候很严重的时候,在这个桶上形成的链表会越来越长,这样在查询的时候效率就会越来越低,其时间复杂度为O(n)
- 所谓hash冲突就是两个对象计算出来的hash值相同,但是我们需要注意的是,处于hashMap中同一个链表上的元素,只是他们的数组下标i(i=(n-1)&hash),n是数组的长度),这个i是相同的,但是他们的hash值不一定相同。详情解释看大佬博客:(187条消息) HashMap的同一链表中对象的hashcode真的一样吗?_Royal_lr的博客-CSDN博客_hashmap同一个链表里的hash值
- jdk8中,当调用put方法的时候,如果发现链表的长度大于8的时候,就会调用treeifBin方法,然后进入treeifBin方法后,先判断数组的长度,若数组的长度为0,或者数组的长度大于等于最小树化阈值(64)就会将链表转化为红黑树,当这个链表的长度小于6的时候就会将红黑树用转变为链表进行存储元素,这样就可以大大提高查询的效率,链表为O(n),而红黑树是O(logn),大大提高查询效率。
8、HashMap的扩容机制
- HashMap再没有指定其大小初始化的时候,他的数组容量和扩容阈值都为0,当添加第一个元素的时候,容量设置为HashMap的默认初始化容量:16,然后设置他的扩容阈值:0.75*16=12(0.75)是负载因子
- 当不是第一次添加元素的时候,他就会判断当前数组的长度和扩容阈值的大小,如果大于了这个扩容阈值就调用扩容方法,将新的数组容量变为老的数组容量的二倍,且新的扩容阈值变为老的扩容阈值的2倍。
9、HashMap中循环链表的产生
- 产生循环链表主要是因为HashMap它是非线程安全的,产生循环链表就是因为HashMap再多线程的情况下,当重新调整HashMap的大小的时候,会存在条件竞争,因为当两个线程都发现HashMap需要调整大小的时候,他们会同时试着调整HashMap的大小,再调整大小的过程中,存储在链表中的元素,次序会反过来,因为移动到新的桶的位置的时候,HashMap并不会将元素放在链表的尾部,而是放在了链表的头部,所以为了避免尾部遍历。
- 这也是HashMap非线程安全的原因,HashMap在高并发的put操作时,可能就会形成循环链表,从而引起死循环。
10、如何将HashMap实现线程安全呢?
- 使用HashTable来替代HashMap(不推荐)
- java.util.collection下面的synchronizedMap(),将我们的HashMap进行包装使之成为线程安全的map,但是synchronizedMap()底层还是使用了synchronized关键字实现的线程同步,也会导致效率大大降低。
- 使用java.util.concurrent包下面的这个ConcurrentHashMap,它不仅性能好,而且也能实现线程安全,ConcurrentHashMap的实现细节是比较复杂的,他不是简单的使用了synchronized全局锁来锁住自己,而是采用了减少锁粒度的方法,尽量减少因为竞争锁而导致的阻塞和冲的,来实现线程安全。
11、HashMap和ConcurrentHashMap的区别
- HashMap是非线程安全的,这意味着不应该在多线程中对HashMap进行操作,否则容易产生数据不一致的问题,还可能因为并发导致链表成环发生死循环的情况和数据覆盖的情况。
- Conllections工具类可以将一个Map转换成为线程安全的实现,其实也就是通过一个包装类,然后把所有功能都委托给传入的Map,而包装类是基于synchronized关键字来保证线程安全俺的,底层是互斥锁,性能和吞吐量较低
- ConcurrentHashMap的实现细节远远没有这么简单,他虽然是线程安全的,但是他的性能也是很可观的,他没有使用一个全局锁来锁住自己,而是采用了减少锁粒度的方法,尽量减少因为竞争锁而导致的阻塞与冲突,而且ConcurrentHashMap的检索操作是不需要锁的。
12、LinkedHashMap
- 他是继承于HashMap,他在HashMap的基础上,通过维护了一条双向链表,解决了HashMap不能随时保持遍历顺序和插入顺序一致的问题。在实现上LenkedHashMap很多的方法都直接继承于HashMap,仅为维护双向链表重写了部分方法。
13、TreeMap
- TreeMap基于红黑树实现,映射根据其键的自然顺序排序,或者根据创建映射时提供的Comparator进行排序,具体取决于使用的构造方法。TreeMap的基本操作,containsKey、get、put、remove,他的时间复杂度是(logN)。
- TreeMap包含几个重要的成员变量,root、size、comparator。其中root是红黑树的根节点。他是Entry类型,Entyr是红黑树的节点,它包含了红黑树的6个基本组成:key、value、left、right、parent和color。Entry节点根据key进行排序,包含的内容是value。Entry中key比较大小是根据比较器comparator来进行判断的。size是红黑树的节点个数。
14、ArrayList和LinkedList
ArrayList
- ArrayList的底层是用数组来实现的,默认第一次插入元素时创建一个大小为10的数组,超出限制时会增加50%的容量,并且数据以System.arraycopy()复制到新的数组。
- ArrayList按照下标访问元素的性能很高,这是数组的基本优势,直接在数组的末尾添加元素的性能很高,但是如果按照下标添加元素,删除元素,要使用System。arraycopy()来移动部分受影响的元素,性能就变差了,这是基本劣势。
LinkedList
两者的区别
- ArrayList的底层是一个数组,LinkedList底层是一个双向的链表
- 对于随机访问ArrayList要优于LinkedList,ArrayList可以根据下标以O(1)的时间复杂度对元素进行访问,而LinkedList的每一个元素都依靠的是地址指针将每个元素连接起来,查找某个元素需要从链表的头部一致遍历到查询位置才能访问到,他的时间复杂度是O(n)。
- 对于删除、插入元素操作,LinkedList优于ArrayList,因为当元素添加到LinkedList任意位置的时候,不需要像ArrayList那样要将元素整体进行一个大的移动,LinkedList只需要改变引用的指向即可。
- LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储元素,还存储了两个引用,一个指向前驱节点、一个指向后继节点。
15、那些List是线程安全的
- Vector
- Conllections.SynchronizedList
- CopyOnWriteArrayLsit
16、HahsSet底层接结构
- HashSet是基于HashMap实现的,默认构造函数是一个初始容量为16,负载因子为0.75的HashMap。它封装了一个HashMap对象来存储所有的集合元素,所有放入HashSet集合中的元素实际上是由HashMap的Key来保存的,而HashMap的Value存储的是一个PRESENT,他是一个静态的保护对象。
17、TreeSet和HashSet的区别
二者共同点
- 首先TreeSet和HashSet中的元素都是不能够重复的,并且都不是线程安全的
二者区别
- HashSet中的元素可以是null,但是TreeSet的元素不能够是null
- HashSet不能保证元素的排序顺序,而TreeSet支持自然排序、定制排序两种方式
- HashSet底层是采用哈希表来实现的,而TreeSet底层是红黑树
以上是关于Java集合面试题总结的主要内容,如果未能解决你的问题,请参考以下文章