集合
2021-05-29 12:33:12 1 举报
AI智能生成
java集合
作者其他创作
大纲/内容
Collection
List<br><br>有序,可重复
<b>ArrayList</b>
ArrayList就是数组列表,主要用来装载数据,当我们装载的是基本类型的数据int,long,boolean,short,byte…的时候我们只能存储他们对应的包装类<br><br><b>arraylist可以存null吗? ArrayList存储的类型是object,null属于object类型。</b><br>
和LinkedList相比,它的查找和访问元素的速度较快,但新增,删除的速度较慢。
特点:ArrayList底层是用数组实现的存储,查询效率高,增删效率低,线程不安全。使用频率很高
初始化大小
ArrayList可以通过构造方法在初始化的时候指定底层数组的大小,不会初始化数组大小(就是指定为8,但size为0),只有add数据后,才会真正分配<br><br>通过无参构造方法的方式ArrayList()初始化,则赋值底层数Object[] elementData为一个默认空数组Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}所以数组容量为0,只有真正对数据进行添加add时,才分配默认DEFAULT_CAPACITY = 10的初始容量。
扩容
扩容1.5倍,通过数组扩容的方式,比如我们现在有一个长度为10的数组,现在我们要新增一个元素,发现已经满了,重新定义一个长度为10+10/2的数组也就是新增一个长度为15的数组,然后把原数组的数据,原封不动的复制到新数组中,这个时候再把指向原数的地址换到新数组,ArrayList就这样完成了一次改头换面。
新增
在这之前他会有一步校验长度的判断ensureCapacityInternal,就是说如果长度不够,是需要扩容的
在扩容的时候,老版本的jdk和8以后的版本是有区别的,8之后的效率更高了,采用了位运算,右移一位,其实就是除以2这个操作。
在校验之后就是数组的copy,System.arraycopy()
删除
删除其实跟新增是一样的,不过叫是叫删除,但是在代码里面我们发现,他还是在copy一个数组。
线程不安全
Vector 已废弃
Collections.synchronizedList
juc 包下的CopyOnWriteArrayList
ArrayList不适合做队列,数组适合用来做队列
遍历
论遍历ArrayList要比LinkedList快得多,ArrayList遍历最大的优势在于内存的连续性,CPU的内部缓存结构会缓存连续的内存片段,可以大幅降低读取内存的性能开销。
两种数组复制方法的区别在于:<br>arraycopy()需要目标数组,将原数组拷贝到你自己定义的数组里,而且可以选择拷贝的起点和长度以及放入新数组中的位置<br>copyOf()是系统自动在内部新建一个数组,调用arraycopy()将original内容复制到copy中去,并且长度为newLength。返回copy; 即将原数组拷贝到一个长度为newLength的新数组中,并返回该数组。<br>总结<br>Array.copyOf()可以看作是受限的System.arraycopy(),它主要是用来将原数组全部拷贝到一个新长度的数组,适用于数组扩容。
<b>LinkedList</b>
底层是双向链表,只能从头开始逐个遍历
<b>Vector</b>
底层也是用数组来实现的,基本每个方法都加Synchronized,已经废弃了
<b>List 的实现方式有 LinkedList 和 ArrayList 两种,那面试时最常问的就是这两个数据结构如何选择。</b>
一是考虑数据结构是否能完成需要的功能;<br>如果都能完成,二是考虑哪种更高效。
<b>Vector 和 ArrayList 的区别是什么</b>
线程安全
默认情况下它是扩容两倍。
Set<br><br>无序,不重复
<b>HashSet</b>
采用 Hashmap 的 key 来储存元素,主要特点是无序的,基本操作都是 O(1) 的时间复杂度,很快。
<b>LinkedHashSet</b>
HashSet + LinkedList 的结构,特点就是既拥有了 O(1) 的时间复杂度,又能够保留插入的顺序。
<b>SortedSet</b>
TreeSet
采用红黑树结构,特点是可以有序,可以用自然排序或者自定义比较器来排序;缺点就是查询速度没有 HashSet 快。
Queue<br><br>队列,先进先出
<b>LinkedList</b>
实现「普通队列 - 先进先出」的语义
<b>Deque</b>
<b>ArrayDeque</b>
实现「普通队列 - 先进先出」和栈 的语义
<b>PriorityQueue(特殊,heap)</b>
实现「优先队列」的语义
<b>基本API</b>
<br>
<b>如何选择用 LinkedList 还是 ArrayDeque 呢?</b>
总结来说就是推荐使用 ArrayDeque,因为效率高,而 LinkedList 还会有其他的额外开销(overhead)。
<b>那 ArrayDeque 和 LinkedList 的区别有哪些呢?</b>
ArrayDeque 是一个可扩容的数组,LinkedList 是链表结构;<br>ArrayDeque 里不可以存 null 值,但是 LinkedList 可以;<br>ArrayDeque 在操作头尾端的增删操作时更高效,但是 LinkedList 只有在当要移除中间某个元素且已经找到了这个元素后的移除才是 O(1) 的;<br>ArrayDeque 在内存使用方面更高效。<br>所以,只要不是必须要存 null 值,就选择 ArrayDeque 吧!
Map
HashMap<br><br>与 HashSet 对应,也是无序的,O(1)。<br>
<b>由数组和链表组合构成的数据结构,允许使用 null 值和 null 键</b>
我们都知道数组长度是有限的,在有限的长度里面我们使用哈希,哈希本身就存在概率性,就是”帅丙“和”丙帅“我们都去hash有一定的概率会一样,就像上面的情况我再次哈希”丙帅“极端情况也会hash到一个值上,那就形成了链表。
每一个节点都会保存自身的hash、key、value、以及下个节点
新的Entry节点在插入链表的时候,插入方式
jdk 8之前是<b>头插法</b>
<b>同一位置上新元素总会被放在链表的头部位置,会改变链表的上的顺序。</b><br>在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。<br><b>会造成无限循环的问题</b>
jdk8之后是<b>尾插法</b>
<b>在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。</b>
<b>扩容机制</b><br>数组容量是有限的,数据多次插入的,<br>到达一定的数量就会进行扩容,也就是resize。<br>
因素
Capacity:HashMap当前长度。<br><br>LoadFactor:负载因子,默认值0.75f。
扩容
分为两步<br><br>扩容:创建一个新的Entry空数组,长度是原数组的2倍。<br><br>ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组
<b>为什么要重新哈希?<br><br></b>因为长度扩大以后,Hash的规则也随之改变。<br><br>Hash的公式---> index = HashCode(Key) & (Length - 1)<br>
<b>初始化大小为16</b>
<b>为了位运算的方便,位与运算比算数计算的效率高了很多</b>
<b>实现均匀分布。</b><br><br>只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。<br>
<b>链表有红黑树</b>
二叉树
1.左子树上所有结点的值均小于或等于它的根结点的值。<br>2.右子树上所有结点的值均大于或等于它的根结点的值。<br>3.左、右子树也分别为二叉排序树。
<b>当二叉树多次插入新的节点,导致的不平衡,红黑树应用而生。</b>
1.节点是红色或黑色。<br><br>2.根节点是黑色。<br><br>3.每个叶子节点都是黑色的空节点(NIL节点)。<br><br>4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)<br><br>5.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
<b>如何保证红黑树始终是红黑树</b>
变色
旋转
在极限状态下,HashMap在同一索引中放入第十一个元素时才会转变为红黑二叉树<br><br>那么由此我们会想为什么会这样呢?<b>Java中明明预设了阈值"8"</b>,怎么会到第十一才转为红黑二叉树呢?<br><br>其实在HashMap中存在扩容机制,<br>它一开始默认是长度为16的数组,<br>当某一个索引中的元素超过到8时,它优先会选择将长度扩容2倍(即长度为32)的数组,<br>它会认为是不是由于数组的长度不够才导致一个索引中元素过多;<br>但当它发现长度为32时,某一个索引中的元素还是超过9时,<br>它还是会优先选择将长度再次扩容到2倍(即长度为64)的数组,<br>在长度为64的数组中,它发现某一个索引中的元素还是超过10时,它就会对该索引所在的链表转化为红黑二叉树.<br>所以在第十一个元素时,就会对该索引所在的链表转化为红黑二叉树.
<b>为啥我们重写equals方法的时候<br>需要重写hashCode方法呢?</b><br>
<b>在java中,所有的对象都是继承于Object类。Object类中有两个方法equals、hashCode,这两个方法都是用来比较两个对象是否相等的</b>
<b>在未重写equals方法我们是继承了object的equals方法,那里的 equals是比较两个对象的内存地址,显然我们new了2个对象内存地址肯定不一样<br><br>对于值对象,==比较的是两个对象的值<br><br>对于引用对象,比较的是两个对象的地址</b>
大家是否还记得我说的HashMap是通过key的hashCode去寻找index的,那index一样就形成链表了,也就是说”帅丙“和”丙帅“的index都可能是2,在一个链表上的。<br><br>我们去get的时候,他就是根据key去hash然后计算出index,找到了2,那我怎么找到具体的”帅丙“还是”丙帅“呢?<br><br>equals!是的,所以如果我们对equals方法进行了重写,建议一定要对hashCode方法重写,以保证相同的对象返回相同的hash值,不同的对象返回不同的hash值。
线程不安全,线程安全需使用
HashTable
适合在多线程的情况下使用,但是效率可不太乐观。<br>对数据操作的时候都会上锁,所以效率比较低下。<br>
CurrentHashMap
Collections.synchronizedMap(Map)
两个构造器<br>如果你传入了mutex参数,则将对象排斥锁赋值为传入的对象。<br><br>如果没有,则将对象排斥锁赋值为this,即调用synchronizedMap的对象<br>
<b>Hashtable 跟HashMap不一样</b>
<b>Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null。</b><br><br>Hashtable在我们put 空值的时候会直接抛空指针异常,但是HashMap却做了特殊处理。<br>
<b>Hashtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据。</b>
<b>实现方式不同:Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类。</b>
<b>初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。</b>
<b>扩容机制不同:当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。</b>
<b>迭代器不同:HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的。</b>
总结:当其他线程改变了HashMap 的结构,如:增加、删除元素,将会抛出ConcurrentModificationException 异常,而 Hashtable 则不会。
<b>快速失败</b>(fail—fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,<br>如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),<br>则会抛出Concurrent Modification Exception。<br>
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。<br><br>集合在被遍历期间如果内容发生变化,就会改变modCount的值。<br><br>每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。<br><br>Tip:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。<br><br>因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
LinkedHashMap<br>这是一个「HashMap + 双向链表」的结构,落脚点是 HashMap,所以既拥有 HashMap 的所有特性还能有顺序。<br>
TreeMap<br>是有序的,本质是用二叉搜索树来实现的。<br>
ConcurrentHashMap
<b>是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。</b>
HashEntry跟HashMap差不多的,但是不同点是,<br><b>使用volatile去修饰了他的数据Value还有下一个节点next。</b><br>
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)<br><br>禁止进行指令重排序。(实现有序性)<br><br>volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性
<b>并发度高的原因</b>
<b>jdk1.7</b><br><br>ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。<br><br>每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。<br>
j<b>dk1.8<br><br></b>其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。<br><br>跟HashMap很像,也把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性,并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)<b><br></b><br>
<b>jdk1.8 put操作</b>
ConcurrentHashMap在进行put操作的还是比较复杂的,大致可以分为以下步骤:<br><br>根据 key 计算出 hashcode 。<br><br>判断是否需要进行初始化。<br><br>即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。<br><br>如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。<br><br>如果都不满足,则利用 synchronized 锁写入数据。<br><br>如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
<b>CAS</b>
CAS 是乐观锁的一种实现方式,是一种轻量级锁,<br>JUC 中很多工具类的实现就是基于 CAS 的。<br><br>线程在读取数据时不进行加锁,在准备写回数据时,<br>比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。<br>
<b>ABA 问题</b>
一个线程把值改回了B,又来了一个线程把值又改回了A,<br>对于这个时候判断的线程,就发现他的值还是A,<br>所以他就不知道这个值到底有没有被人改过<br>
(1) 版本号去保证就好了,就比如说,我在修改前去查询他原来的值的时候再带一个版本号,<br>每次判断就连值和版本号一起判断,判断成功就给版本号加1。<br><br>(2) 时间戳也可以,查询的时候把时间戳一起查出来,对的上才修改并且更新值的时候一起修改更新时间
<b>synchronized性能可不咋地,为啥jdk1.8升级之后反而多了synchronized?</b>
synchronized之前一直都是重量级的锁,但是后来官方是对他进行过升级的,<br>他现在采用的是锁升级的方式去做的。<br><br>针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,<br>就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,<br>如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁<br>
<b>Jdk1.8 get 操作</b>
根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。<br><br>如果是红黑树那就按照树的方式获取值。<br><br>就不满足那就按照链表的方式遍历获取值。
学习目标
明确每个接口和类的对应关系;<br>对每个接口和类,熟悉常用的 API;<br>对不同的场景,能够选择合适的数据结构并分析优缺点;<br>学习源码的设计,面试要会答啊。
0 条评论
下一页