Java复习篇2-集合

双向链表 双向循环链表

在这里插入图片描述
在这里插入图片描述

List,Set,Map三者的区别

  • List : 存储的元素是有序的、可重复的。对付顺序的好帮⼿
  • Set : 存储的元素是⽆序的、不可重复的。注重独⼀⽆⼆的性质
  • Map : 使⽤键值对(kye-value)存储,Key 是⽆序的、不可重复的,value 是⽆序的、可重复的,每个键最多映射到⼀个值。⽤ Key 来搜索的专家

Arraylist 与 LinkedList 区别

  1. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
  2. 底层数据结构: Arraylist 底层使⽤的是 Object 数组; LinkedList 底层使⽤的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。)
  3. 插⼊和删除是否受元素位置的影响: ① ArrayList 采⽤数组存储,所以插⼊和删除元素的时间复杂度受元素位置的影响。 ⽐如:执⾏ add(E e) ⽅法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i插⼊和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进⾏上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执⾏向后位/向前移⼀位的操作。 ②LinkedList 采⽤链表存储,所以对于 add(E e) ⽅法的插⼊,删除元素时间复杂度不受元素位置的影响近似 O(1),如果是要在指定位置 i 插⼊和删除元素的话( (add(int index, Eelement) ) 时间复杂度近似为 o(n)) 因为需要先移动到指定位置再插⼊。
  4. 是否⽀持快速随机访问LinkedList 不⽀持⾼效的随机元素访问,⽽ ArrayList ⽀持。快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) ⽅法)。
  5. 内存空间占⽤: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留⼀定的容量空间,⽽ LinkedList 的空间花费则体现在它的每⼀个元素都需要消耗⽐ ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

随机访问RandomAccess接口

RandomAccess 接⼝中什么都没有定义。RandomAccess 接⼝只是⼀个标识,标识实现这个接⼝的类具有随机访问功能。在 binarySearch() ⽅法中,它要判断传⼊的 list 是否 RamdomAccess 的实例,如果是,调⽤ indexedBinarySearch() ⽅法,如果不是,那么调⽤ iteratorBinarySearch() ⽅法。

  • RandomAccess 接⼝只是标识,并不是说实现了 RandomAccess 接⼝才具有快速随机访问功能的!

ArrayList 与 Vector 区别

  • ArrayList 是 List 的主要实现类,底层使⽤ Object[ ] 存储,适⽤于频繁的查找⼯作,线程不安全
  • Vector 是 List 的古⽼实现类,底层使⽤ Object[ ] 存储,线程安全的。

ArrayList扩容机制

HashMap 和 Hashtable区别

  1. 线程是否安全HashMap 是⾮线程安全的, HashTable 是线程安全的,因为 HashTable 内部的⽅法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使⽤ConcurrentHashMap 吧!);
  2. 效率: 因为线程安全的问题, HashMap 要⽐ HashTable 效率⾼⼀点。另外, HashTable基本被淘汰,不要在代码中使⽤它;
  3. 对 Null key 和 Null value 的⽀持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有⼀个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出NullPointerException 。
  4. 初始容量⼤⼩和每次扩充容量⼤⼩的不同 : ① 创建时如果不指定容量初始值, Hashtable默认的初始⼤⼩为 11,之后每次扩充,容量变为原来的 2n+1。 HashMap 默认的初始化⼤⼩为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么Hashtable 会直接使⽤你给定的⼤⼩,⽽ HashMap 会将其扩充为 2 的幂次⽅⼤⼩( HashMap 中的 tableSizeFor() ⽅法保证,下⾯给出了源代码)。也就是说 HashMap 总是使⽤ 2 的幂作为哈希表的⼤⼩,后⾯会介绍到为什么是 2 的幂次⽅。
  5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了⼤的变化,当链表⻓度⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。Hashtable 没有这样的机制。

HashMap 和 HashSet区别

HashSet 底层就是基于 HashMap 实现的。( HashSet 的源码⾮常⾮常少,因为除了 clone() 、 writeObject() 、 readObject() 是 HashSet
⾃⼰不得不实现之外,其他⽅法都是直接调⽤ HashMap 中的⽅法。
在这里插入图片描述

HashMap的底层实现

JDK1.8 之前 HashMap 底层是 数组和链表 结合在⼀起使⽤也就是 链表散列。HashMap 通过key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这⾥的 n 指的是数组的⻓度),如果当前位置存在元素的话,就判断该元素与要存⼊的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。 JDK1.8 之后在解决哈希冲突时有了较⼤的变化,当链表⻓度⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。

  • 扰动函数:就是 HashMap 的 hash ⽅法。使⽤ hash ⽅法也就是扰动函数是为了防⽌⼀些实现⽐较差的 hashCode() ⽅法 换句话说使⽤扰动函数之后可以减少碰撞。
  • 拉链法:将链表和数组相结合。也就是说创建⼀个链表数组,数组中每⼀格就是⼀个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

HashMap 的长度为什么是2的幂次方

为了能让 HashMap 存取⾼效,尽量减少碰撞,也就是要尽量把数据分配均匀。Hash 值的范围值-2147483648到2147483647,前后加起来⼤概40亿的映射空间,只要哈希函数映射得比较均匀松散,⼀般应⽤是很难出现碰撞的。但问题是⼀个40亿⻓度的数组,内存是放不下的。所以这个散列值是不能直接拿来⽤的。⽤之前还要先做对数组的长度取模运算,得到的余数才能⽤来要存放的位置也就是对应的数组下标。取余(%)操作中如果除数是2
的幂次则等价于与其除数减⼀的与(&)操作(也就是说hash%length==hash&(length-1)的前提是 length 是2的 n 次⽅;)。 并且 采⽤⼆进制位操作 &,相对于%能够提⾼运算效率
所以数组下标的计算⽅法是 (n - 1) &hash 。(n代表数组⻓度)。这也就解释了 HashMap 的⻓度为什么是2的幂次⽅。

HashMap 多线程操作导致死循环问题

主要原因:并发下的Rehash 会造成元素之间会形成⼀个循环链表。jdk 1.8 后解决了这个问题,但还是不建议在多线程下使⽤ HashMap,因为多线程下使⽤ HashMap 还是会存在其他问题⽐如数据丢失。并发环境下推荐使⽤ ConcurrentHashMap 。

  • Rehash

ConcurrentHashMap 和 Hashtable 的区别

主要体现在实现线程安全的⽅式上不同。

  1. 底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采⽤ 分段的数组+链表 实现,JDK1.8采⽤的数据结构跟 HashMap1.8 的结构⼀样,数组+链表/红黑二叉树。 Hashtable 和JDK1.8 之前的 HashMap 的底层数据结构类似都是采⽤ 数组+链表 的形式,数组是HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的
  2. 实现线程安全的⽅式(重要): ① 在 JDK1.7 的时候, ConcurrentHashMap (分段锁)对整个桶数组进⾏了分割分段 ( Segment )每⼀把锁只锁容器其中⼀部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,ConcurrentHashMap 不在是 Segment 数组 + HashEntry 数组 + 链表,而是直接用 Node 数组+链表+红黑树的数据结构来实现,不过,Node 只能⽤于链表的情况,红⿊树的情况需要使⽤ TreeNode 。当冲突链表达到⼀定⻓度时,链表会转换成红⿊树。并发控制使⽤ synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap ,虽然在 JDK1.8 中还能看到Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable (同⼀把锁) :使⽤ synchronized 来保证线程安全,效率⾮常低下。当⼀个线程访问同步⽅法时,其他线程也访问同步⽅法,可能会进⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不能使⽤ put 添加元素,也不能使⽤ get,竞争会越来越激烈效率越低。
    在这里插入图片描述
    在这里插入图片描述
    更多细节ConcurrentHashMap实现原理及源码分析

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

  • JDK1.7:⾸先将数据分为⼀段⼀段的存储,然后给每⼀段数据配⼀把锁,当⼀个线程占⽤锁访问其中⼀个段数据时,其他段的数据也能被其他线程访问。ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。Segment 实现了 ReentrantLock ,所以 Segment 是⼀种可重⼊锁,扮演锁的⻆⾊。 HashEntry ⽤于存储键值对数据。⼀个 ConcurrentHashMap ⾥包含⼀个 Segment 数组。 Segment 的结构和 HashMap 类似,是⼀种数组和链表结构,⼀个 Segment 包含⼀个 HashEntry 数组,每个 HashEntry 是⼀个链表结构的元素,每个 Segment 守护着⼀个 HashEntry 数组⾥的元素,当对 HashEntry 数组的数据进⾏修改时,必须⾸先获得对应的 Segment 的锁。
  • JDK1.8:ConcurrentHashMap 取消了 Segment 分段锁,采⽤ CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红⿊⼆叉树。Java 8 在链表⻓度超过⼀定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红⿊树(寻址时间复杂度为 O(log(N)))synchronized 只锁定当前链表或红黑二叉树的⾸节点,这样只要 hash 不冲突,就不会产⽣并发,效率⼜提升 N 倍。

HashSet、LinkedHashSet 和 TreeSet 三者的异同

  • HashSet 是 Set 接⼝的主要实现类 , HashSet 的底层是 HashMap ,线程不安全的,可以存储 null 值;
  • LinkedHashSet 是 HashSet 的⼦类,能够按照添加的顺序遍历;
  • TreeSet 底层使用红黑树,能够按照添加元素的顺序进⾏遍历,排序的⽅式有⾃然排序和定制排序。

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

List

  • Arraylist : Object[] 数组
  • Vector : Object[] 数组
  • LinkedList : 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)

Set

  • HashSet (⽆序,唯⼀): 基于 HashMap 实现的,底层采⽤ HashMap 来保存元素
  • LinkedHashSet : LinkedHashSet 是 HashSet 的⼦类,并且其内部是通过 LinkedHashMap来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于 HashMap 实现⼀样,不过还是有⼀点点区别的
  • TreeSet (有序,唯⼀): 红⿊树(⾃平衡的排序⼆叉树)

Map

  • HashMap : JDK1.8 之前 HashMap 由数组+链表组成的,数组是HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较⼤的变化,当链表⻓度⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。
  • LinkedHashMap : LinkedHashMap 继承⾃ HashMap ,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红⿊树组成。另外,LinkedHashMap 在上⾯结构的基础上,增加了⼀条双向链表,使得上⾯的结构可以保持键值对的插⼊顺序。同时通过对链表进⾏相应的操作,实现了访问顺序相关逻辑。详细可以查看:《LinkedHashMap 源码详细分析(JDK1.8)》
  • Hashtable : 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的
  • TreeMap : 红⿊树(⾃平衡的排序⼆叉树)

如何选用集合

主要根据集合的特点来选⽤

  • 需要根据键值获取到元素值时就选⽤ Map 接⼝下的集合,需要排序时选择 TreeMap ,不需要排序时就选择HashMap ,需要保证线程安全就选⽤ConcurrentHashMap 。
  • 只需要存放元素值时,就选择实现 Collection 接⼝的集合,需要保证元素唯⼀时选择实现Set 接⼝的集合⽐如 TreeSet 或 HashSet ,不需要就选择实现 List 接⼝的⽐如 ArrayList 或LinkedList ,然后再根据实现这些接⼝的集合的特点来选⽤。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值