Java面试干货之集合(面试系列)

347 阅读8分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Java面试干货之集合(面试系列)

见贤思齐焉,见不贤而内自省矣。

话不多说,上货!

Collection

Collection接口是面试中基础部分问的最多的问题,头两次没问,这次必问,20分爱要不要~

快速失败机制

首先,Java.Util包下所有的数据结构都是快速失败机制(fast-fail),这种机制保证了在并发条件下,集合在遍历过程中,除了迭代器本身的remove()方法外,如果对集合进行增加或者删除操作时,会抛出ConcurrentModificationException异常,顾名思义,这个异常叫做并发修改异常。

原理是迭代器在遍历中会使用一个modCount变量来记录当前集合的状态,如果其他线程修改了这个集合,则会同时修改这个modCount变量,这时候迭代器在遍历时,便会判断此modCount值是否等于所期望的modCount值,如果不等于,便会抛出异常。

安全失败机制

同样,有了快速失败机制,也有对应的安全失败机制。简单说一下安全失败机制,安全失败(safe-fail):当使用迭代器遍历时,如果数组长度发生变化,不会抛出异常,采用安全失败机制的集合容器,使用迭代器进行遍历时不是直接在集合内容上访问的,而是将原有集合内容进行拷贝,在拷贝的集合上进行遍历。

那你说说Java中你都使用过哪些集合?

List

  • ArrayList

    • 底层是数组,增删慢,查询快,可以通过数组下标以O(1)的时间复杂度查询,但如果不通过下标,也需要O(n)的时间复杂度。
    • 连续的内存空间。
    • 1.5倍扩容,如果不确定存储元素的数据量,则需要考虑数组扩容所带来的性能损耗。
  • LinkedList

    • 底层是双向链表,查询需要通过遍历,所以时间复杂度为O(n),增加和删除元素则是O(1)的时间复杂度,只需要进行链表指针的增加和删除操作。
    • 值得注意的是,链表不需要扩容,不需要连续的内存空间。
  • Vector

    • 跟ArrayList一样,底层是数组,连续的内存空间存储,线程安全,但同样是快速失败机制。
    • 线程安全的代价是在内部加入了大量的sychronized关键字,导致性能低下,这也是此数据结构被弃用的原因。
    • 跟ArrayList不同的是,两倍扩容。
  • Stack

    • 顺带提一下Stack类,此类继承Vector,因为其父类含有大量的sychronized关键字而被弃用。
    • 弃用原因同样是效率低。

Set

  • HashSet

    • 无序的,不重复,采用HashMap的key来存储元素,通过hash()来定位所需要存储数组位置的下标。
    • 通过hash()可以保证插入的元素不会重复。
  • LinkedHashSet

    • 有序,不重复,一样采用HashMap的key来保存元素。
    • 内部维护了一个LinkedList来记录元素插入是的顺序,因此插入效率是要低于HashSet的。
  • TreeSet

    • 有序,不重复。
    • 采用红黑树结构,通过红黑树来保证有序。

这里赘述一下红黑树,红黑树的优点在于查询效率高,之所以高是因为它区别于完全平衡二叉树和完全不平衡二叉树之间,完全不平衡二叉树会存在子树高度差过大的情况,导致查询效率有可能很低,而完全平衡二叉树需要进行树节点的转换,转换的效率也是一个值得考虑的问题,那么红黑树就介于两者之间,实现了树的高度差最多一倍的差距,这完全就是中庸之道。

Deque

双端队列,有两类返回值的操作方法,如遇到队列为空的情况,一类是返回异常,一类是返回值。

  • LinkedList

    • 双向链表,在实现List接口的同时,也实现了Deque接口。
    • 可以存储null值,可以作为FIFO的队列使用。
  • ArrayDeque

    • 可扩容的数组。
    • 可以作为FIFO的队列,也可以作为FILO栈的实现。
  • PriorityQueue

    • 优先队列。
    • 可以根据自定义的排序器进行排序。
    • 优先队列在算法中使用的频率不低。

小伙子还说的挺全,还可以,那就再说说Map吧,使用频率应该也不低吧...

Map

如果说Collection还有那么一丝丝侥幸没被面试官选中的话,那么Map一定是那种没有侥幸,一定被选中的部分。

equals() & hashCode()

  • 原则:如果两个对象的equals,那他们的hash值一定相同。
  • equals是Object类的公共方法,所有的类都是继承的Object类,当然也都继承了它的equals方法。
  • equals的方法用来判断两个对象是否相等,在我们重写equals方法之前,它的作用等价于==,用来比较两个对象的地址是否相等,而覆写equals更多追求的是让其判断两个对象的内容或者说是值是否相等。
  • hashCode方法同样也是Object类的公共方法,用来生成一个对象的hash值。
  • 在原则的情况下,如果覆写hashCode方法不重写equals会造成在hashMap中,无法分辨出同一个entry中链表上不同的key对象。(HashMap查询时,先用hashCode找出entry,再通过equals查找链表上的对应的对象。)
  • 同样,在只写equals而不重写hashCode时,两个对象equals,但hash值不同,则根本无法准确定位到entry,从而无法get到要查询的对象。

HashMap

  • HashMap也是快速失败机制,线程不安全。

  • 1.7中数组加链表的结构,在1.8版本中引入红黑树,在满足树形化的条件下,链表转换为红黑树,增加查询效率。

  • 扩容

    • 新建一个数组,长度是原来的两倍,两倍是因为要保证数组长度为2的整数次幂。
    • rehash(),重新hash()得到该元素重新插入数组的下标,公式为index = hash(Key) & (length - 1)。
    • 数组长度减一(length-1)之后得到的二进制位全是1,然后再与hash(Key)进行逻辑与操作,更容易实现hash后数组的均匀分布。
    • 1.7中是头插法,之所以使用头插法,是因为后插入的数据被认为更有可能被查询,所以效率更高,但头插法会使rehash()过程中容易出现死循环的可能。
    • 1.8改为尾插法,解决了扩容过程中可能存在死循环的可能,但并发条件下仍有可能存在put的值被覆盖的问题。
  • 参数

    • 默认初始化容量:16,2的整数次幂,前面说过,为了实现数组的均匀分布,因为2的整数次幂减一二进制位全是1。
    • 默认负载因子:0.75,当数组使用超过16*0.75 = 12时,数组扩容。
    • 树形化阈值(1.8以后):8,当链表长度大于8时,转换成红黑树。
    • 树形化最小容量:64,当数组长度大于64时,才允许转换成红黑树。

ConcurrentHashMap

  • 安全失败机制(Java.Util.Concurrent包下都是安全失败机制)
  • 在1.7版本中使用segment(分段锁)来控制并发,每一个segment控制对应多个Entry,每一个segment可以单独看做一个HashMap,每次put操作会对相应的segment加锁,不会影响其他segment。
  • segment继承于ReentrantLock,理论上来说,ConcurrentHashMap支持当前segment数组容量的线程的并发,这个应该比较好理解。每一个segment都是一个锁,有多少把锁,就支持多少线程并发操作。
  • 用volatile关键字修饰了entry,保证了线程间的可见性,所以get操作不会受到锁的影响。
  • 但问题是,1.7没有引入红黑树,如果链表很长,查询效率也不高。
  • 所以在1.8中就引入了红黑树,跟HashMap一样。同样的,采用了CAS+Synchronized来保证线程安全。
  • 具体做法就是在put的时候,会首先尝试CAS写入,成功则已,不成功则自旋,判断是否需要扩容,是否需要转换成红黑树,如果都不是,则使用Synchronized锁。

小伙可以,那还有没有其他方法来保证线程安全呢?

HashTable

  • 继承自Dictionary,效率低,但线程安全。
  • 效率低的根本原因是对数据的操作都会上锁,包括读。
  • 不允许键或值为空,因为安全失败机制,没办法判断是不存在,还是为null。
  • 初始容量为11,扩容为2倍原数组大小+1。

还有吗?

SynchronizedMap

  • SychronizedMap内部维护了一个普通的Map对象以及排斥锁mutex。
  • 当SynchronizedMap被创建出来时,所有对它的操作都是上了锁的。
  • 同样,效率很低。

一点体会

这段时间太多的东一个西一个的想法冒出来,想要学习,想要提升,但都没有去做。

但身边总是有人让你产生一种想法是,你好像也可以。

那么,如果你真这么想。

去做吧。

见贤思齐焉,见不贤而内自省也。

与诸君共勉。

后记

太久没有写文章了,可能有些退步了,后续会继续为大家更新面试干货系列,后续大家如果有任何问题或者建议,欢迎在评论区指出。

同时也希望大家可以关注我的公众号【类似程序员】,最新的文章会先在公众号上发布,也会定期与大家分享我的一些想法与学习感悟,谢谢各位。