这几道Java集合框架面试题在面试中几乎必问

本文是“最最最多见Java面试题总结”系列第三周的文章。 主要内容:php

  1. Arraylist 与 LinkedList 异同
  2. ArrayList 与 Vector 区别
  3. HashMap的底层实现
  4. HashMap 和 Hashtable 的区别
  5. HashMap 的长度为何是2的幂次方
  6. HashSet 和 HashMap 区别
  7. ConcurrentHashMap 和 Hashtable 的区别
  8. ConcurrentHashMap线程安全的具体实现方式/底层具体实现
  9. 集合框架底层数据结构总结

本文会同步更新在我开源的Java学习指南仓库 Java-Guide (一份涵盖大部分Java程序员所须要掌握的核心知识,正在一步一步慢慢完善,期待您的参与)中,地址:github.com/Snailclimb/…,欢迎star、issue、pr。html

Arraylist 与 LinkedList 异同

  • 1. 是否保证线程安全: ArrayList 和 LinkedList 都是不一样步的,也就是不保证线程安全;
  • 2. 底层数据结构: Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向循环链表数据结构;
  • 3. 插入和删除是否受元素位置的影响:ArrayList 采用数组存储,因此插入和删除元素的时间复杂度受元素位置的影响。 好比:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种状况时间复杂度就是O(1)。可是若是要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。由于在进行上述操做的时候集合中第 i 和第 i 个元素以后的(n-i)个元素都要执行向后位/向前移一位的操做。 ② LinkedList 采用链表存储,因此插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。
  • 4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而ArrayList 实现了RandmoAccess 接口,因此有随机访问功能。快速随机访问就是经过元素的序号快速获取元素对象(对应于get(int index)方法)。
  • 5. 内存空间占用: ArrayList的空 间浪费主要体如今在list列表的结尾会预留必定的容量空间,而LinkedList的空间花费则体如今它的每个元素都须要消耗比ArrayList更多的空间(由于要存放直接后继和直接前驱以及数据)。

补充:数据结构基础之双向链表

双向链表也叫双链表,是链表的一种,它的每一个数据结点中都有两个指针,分别指向直接后继和直接前驱。因此,从双向链表中的任意一个结点开始,均可以很方便地访问它的前驱结点和后继结点。通常咱们都构造双向循环链表,以下图所示,同时下图也是LinkedList 底层使用的是双向循环链表数据结构。java

ArrayList 与 Vector 区别

Vector类的全部方法都是同步的。能够由两个线程安全地访问一个Vector对象、可是一个线程访问Vector的话代码要在同步操做上耗费大量的时间。git

Arraylist不是同步的,因此在不须要保证线程安全时时建议使用Arraylist。程序员

HashMap的底层实现

JDK1.8以前

JDK1.8 以前 HashMap 由 数组+链表 组成的(“链表散列” 即数组和链表的结合体),数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(HashMap 采用 “拉链法也就是链地址法” 解决冲突),若是定位到的数组位置不含链表(当前 entry 的 next 指向 null ),那么对于查找,添加等操做很快,仅需一次寻址便可;若是定位到的数组包含链表,对于添加操做,其时间复杂度依然为 O(1),由于最新的 Entry 会插入链表头部,急须要简单改变引用链便可,而对于查找操做来说,此时就须要遍历链表,而后经过 key 对象的 equals 方法逐一比对查找.github

所谓 “拉链法” 就是将链表和数组相结合。也就是说建立一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中便可。面试

jdk1.8以前的内部结构

JDK1.8以后

相比于以前的版本, JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减小搜索时间。算法

JDK1.8以后的HashMap底层数据结构

TreeMap、TreeSet以及JDK1.8以后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,由于二叉查找树在某些状况下会退化成一个线性结构。数组

推荐阅读:安全

HashMap 和 Hashtable 的区别

  1. 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都通过 synchronized 修饰。(若是你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
  2. 效率: 由于线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
  3. 对Null key 和Null value的支持: HashMap 中,null 能够做为键,这样的键只有一个,能够有一个或多个键所对应的值为 null。。可是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
  4. 初始容量大小和每次扩充容量大小的不一样 : ①建立时若是不指定容量初始值,Hashtable 默认的初始大小为11,以后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。以后每次扩充,容量变为原来的2倍。②建立时若是给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 老是使用2的幂做为哈希表的大小,后面会介绍到为何是2的幂次方。
  5. 底层数据结构: JDK1.8 之后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减小搜索时间。Hashtable 没有这样的机制。

HashMap 的长度为何是2的幂次方

为了能让 HashMap 存取高效,尽可能较少碰撞,也就是要尽可能把数据分配均匀,每一个链表/红黑树长度大体相同。这个实现就是把数据存到哪一个链表/红黑树中的算法。

这个算法应该如何设计呢?

咱们首先可能会想到采用%取余的操做来实现。可是,重点来了:“取余(%)操做中若是除数是2的幂次则等价于与其除数减一的与(&)操做(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 而且 采用二进制位操做 &,相对于%可以提升运算效率,这就解释了 HashMap 的长度为何是2的幂次方。

HashSet 和 HashMap 区别

HashSet 和 HashMap 区别

ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMap 和 Hashtable 的区别主要体如今实现线程安全的方式上不一样。

  • 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构同样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 以前的 HashMap 的底层数据结构相似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
  • 实现线程安全的方式(重要):在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不一样数据段的数据,就不会存在锁竞争,提升并发访问率。(默认分配16个Segment,比Hashtable效率提升16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操做。(JDK1.6之后 对 synchronized锁作了不少优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,可是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率很是低下。当一个线程访问同步方法时,其余线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另外一个线程不能使用 put 添加元素,也不能使用 get,竞争会愈来愈激烈效率越低。

二者的对比图:

图片来源:www.cnblogs.com/chengxiao/p…

HashTable:

JDK1.7的ConcurrentHashMap:

JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):

ConcurrentHashMap线程安全的具体实现方式/底层具体实现

JDK1.7(上面有示意图)

首先将数据分为一段一段的存储,而后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其余段的数据也能被其余线程访问。

ConcurrentHashMap 是由 Segment 数组结构和 HahEntry 数组结构组成

Segment 实现了 ReentrantLock,因此 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。

static class Segment<K,V> extends ReentrantLock implements Serializable {
}
复制代码

一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap相似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每一个 HashEntry 是一个链表结构的元素,每一个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先得到对应的 Segment的锁。

JDK1.8 (上面有示意图)

ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构相似,数组+链表/红黑二叉树。

synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提高N倍。

集合框架底层数据结构总结

Collection

1. List

  • Arraylist: Object数组
  • Vector: Object数组
  • LinkedList: 双向循环链表

2. Set

  • HashSet(无序,惟一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素
  • LinkedHashSet: LinkedHashSet 继承与 HashSet,而且其内部是经过 LinkedHashMap 来实现的。有点相似于咱们以前说的LinkedHashMap 其内部是基于 Hashmap 实现同样,不过仍是有一点点区别的。
  • TreeSet(有序,惟一): 红黑树(自平衡的排序二叉树。)

Map

  • HashMap: JDK1.8以前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减小搜索时间
  • LinkedHashMap: LinkedHashMap 继承自 HashMap,因此它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增长了一条双向链表,使得上面的结构能够保持键值对的插入顺序。同时经过对链表进行相应的操做,实现了访问顺序相关逻辑。详细能够查看:《LinkedHashMap 源码详细分析(JDK1.8)》
  • HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
  • TreeMap: 红黑树(自平衡的排序二叉树)

推荐阅读:

你若怒放,清风自来。 欢迎关注个人微信公众号:“Java面试通关手册”,一个有温度的微信公众号。公众号有大量资料,回复关键字“1”你可能看到想要的东西哦!

相关文章
相关标签/搜索