Java面试题-集合

Collection包含哪些类

Collection分为Set, List, Queue

  • Set包括SortedSet(TreeSet), HashSet, LinkedHashSet
  • List包括ArrayList, Vector, LinkedList
  • Queue包括LinkedList, PriorityQueue

Set:

  • TreeSet:基于红黑树实现,支持有序性操作,查找的时间复杂度是O(logn)
  • HashSet:基于Hash表实现,支持快速查找,但是不支持有序操作。
  • LinkedHashSet:具有HashSet的查找效率,且内部使用双向链表维护元素的插入顺序。

List

  • ArrayList:基于动态数组实现,支持随机访问。
  • Vector:和ArraList相似,但它是线程安全的。
  • LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地插入和删除元素。还可以用作栈、队列、双向队列。

Queue

  • LinkedList:可以用它来实现双向队列。
  • PriorityQueue:基于堆结构实现,可以用它来实现优先队列。

Map包含哪些类

Map包括SortedMap(TreeMap), HashTable, HashMap, LinkedHashMap

  • TreeMap:基于红黑树实现。
  • HashMap:基于哈希表实现。
  • HashTable:和HashMap相似,但线程安全。
  • LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少(LRU)顺序。

ArrayList

1. 概览

ArrayList基于动态数组实现,支持快速随机访问。RandomAccess接口标识着该类支持快速随机访问。
数组默认大小为10。

2. 扩容

添加元素时使用ensureCapacity()方法来保证容量是否足够,如果不够时,需要使用grow()方法进行扩容,扩容大小为旧容量1.5倍。
扩容操作需要调用Arrays.copyOf()把原数组整个复制到新数组中,代价很高。

3. 删除元素

需要调用System.arraycopy()index+1后面的元素都复制到index位置上,该操作时间复杂度为O(n),删除代价很高。

4. Fail-Fast

modeCount用来记录ArrayList结构发生变化的次数。
在程序进行序列化或者迭代等操作时,需要比较操作前后modeCount是否改变,如果改变需要抛出ConcurrentModificationException

序列化

保存元素的数组elementData使用transient修饰,该关键字声明数组默认不会被序列化。
ArrayList实现了writeObject()readObject()来控制只序列化数组中有元素填充那部分内容。

5. Vector

ArrayList类似,但是使用了synchronized进行同步。
每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。
可以使用Collections.synchronizedList()得到一个线程安全的ArrayList。也可以使用concurrent并发包下的CopyOnWriteArrayList类。

CopyOnWriteArrayList

读写分离:写操作在一个复制的数组上进行,读操作还是在原数组中进行,读写分离,互不影响。
写操作需要加锁,防止并发写入时导致写入数据丢失。写操作结束之后需要把原数组指向新的复制数组。

适用场景:在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。不适合内存敏感以及对实时性要求很高的场景。

缺陷

  • 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
  • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。

LinkedList

1. 概览

基于双向链表实现,只支持顺序查找。

2. 与ArrayList比较

  • ArrayList基于动态数组实现,支持随机访问,但是增加和删除元素的效率低;
  • LinkedList基于双向链表实现,不支持随机访问,增加和删除元素的效率高。

Set和List的区别

  • List允许重复元素;Set不允许重复元素;
  • List允许多个null元素;Set底层是MapHashSet允许一个null元素,TreeSet不允许null元素;
  • List是一个有序的容器,保持了每个元素的插入元素;Set是一个无序的容器,TreeSet通过Comparator或者Comparable维护了一个排序顺序,LinkedHashSet通过双向链表维护了插入顺序。

HashMap

1. 存储结构

数组+链表+红黑树
内部包含了一个Entry[]数组,数组的每一个位置被当成一个桶,一个桶中放一个链表,节点组成为int hashCode, K key, V value, Entry next
HashMap一般用拉链法解决地址冲突,JDK7之前是头插法,容易导致链表环化,JDK8之后是尾插法。另一个改变是当一个链表长度超过8后,会转换为红黑树,提高查找效率。

2. HashMap中的put是如何实现的?

  1. 计算关于key的hash值(与Key.hashCode的高16位做异或运算);
  2. 如果散列表为空时,调用resize()初始化散列表;
  3. 如果没有发生碰撞,直接添加元素到散列表中去;
  4. 如果发生了碰撞(hash值相同),进行三种判断:
    4.1 若key地址相同或者equals后内容相同,则替换旧值;
    4.2 如果是红黑树结构,就调用树的插入方法;
    4.3 链表结构,尾插法或者覆盖hash值相同的元素;
  5. 如果桶满了,大于阈值,则resize()进行扩容。

3. HashMap什么时候需要进行扩容?扩容resize()又是如何实现的?

调用场景

  1. 初始化数组table;
  2. 当数组table的size达到阈值时,即++size > loadfactor* capacity时,也是在put函数中。

实现过程
通过判断旧数组的容量是否大于0,来判断数组是否初始化过
否:进行初始化
判断是否调用无参构造器
是:使用默认的大小和阈值;
否:使用构造函数中初始化的容量,这个容量是经过计算后的2的次幂数
是:进行扩容,扩容成两倍(小于最大值的情况下),之后再将元素重新进行运算复制到新的散列表中。

HashMap中的hash函数怎么实现的?还有那些hash函数的实现方式?

keyhashCodehash操作,与高16位做异或运算。

还有平方取中法,除留余数法,伪随机法。

为什么不直接将key作为哈希值,而是与高16位做异或运算?

因为数组位置的确定用的是与运算,仅仅最后四位有效,设计者将key的哈希值与高16位做异或运算,使得在做与运算确定数组的插入位置时,此时的低位实际是高位与低位的结合,增加了随机性,减少了哈希碰撞的次数。

为什么HashMap的默认初始化长度为16?为什么必须是2的幂?

  1. 为了数据的均匀分布,减少哈希碰撞。因为数组位置使用的位运算,若数据不是2的幂则会增加哈希碰撞的次数和浪费数组空间。
  2. 输入数据若不是2的幂,HashMap通过唯一晕眩和或运算扽道德肯定是2的幂,并且时最接近的。

HashMap和HashTable的区别

  • HashTable使用synchronized来进行同步;
  • HashMap可以插入keynullEntry;
  • HashMap的迭代器是fail-fast迭代器;
  • HashMap不能保证随着时间的推移,Map中的元素次序是不变的;
  • HashMap继承自AbstractMap类,HashTable继承自Dictionary

HashMap一般采用什么类型的元素作为key?

选择Integer, String这种不可变的类型,已经规范覆写了hashCode()equals(),天生线程安全。

ConcurrentHashMap

存储结构

ConcurrentHashMapHashMap 实现上类似,主要的差别是 ConcurrentHashMap 采用了分段锁 (Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。
Segment 继承自 ReentrantLock

默认的并发级别为 16,也就是说默认创建 16 个 SegmentSegment的数量一旦初始化就无法更改.每个Segment上只有一个线程可以操作。

JDK8的改动

由于ConcurrentHashMap 本质上还是数组链表的结构,因此我们在链表上遍历数据整体的效率还是偏低。所以JDK8底层改用了数组+链表/红黑树的形式,且换成了Synchronized锁加 CAS 的机制。Synchronized只会锁住当前链表或者红黑树的首结点,只要hash不冲突就不会产生并发,可以大大提升效率。

get操作

ConcurrentHashMapget操作并不直接加锁。使用了volatile关键字去修饰了其中的成员变量value,还有下一个节点next。正因为使用了该关键字保证了内存可见性,因此ConcurrentHashMapget方法是不需要加锁的,这大大提升了效率。

put操作

根据key计算出哈希值,定位到插入位置如果为空,那么尝试CAS自旋插入。否则使用synchronized 锁写入数据。

size操作

每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。

在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。

ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2retries 初始值为 -1,因此尝试次数为 3
如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。

BlockingQueue是什么?

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

你可能感兴趣的:(Java面试题)