java 集合面试问题
目录
Collection 子接口之 Map
HashMap:
1.数组大小为什么一定要是2的幂:
key值(hash桶)的落脚点为key的hash值与数组长度作取余操作,比如一共有16个hash桶,为了把所有元素均匀放入这16个hash桶中,即把任意int值转换为0-15之间,我们采用的操作是取余(%);
但是取余操作有两个缺点:1. 负数求余要先转换成正数,因为负数求余还是负数;
2. 求余操作速度慢。
为了解决这两个问题,使用的方式是把key.hashcode % array.length转换成
key.hashcode & (array.length - 1),这样可以提高效率,两者的结果是一样的.
原理说明:
2^4-1用二进制形式表达出来是1111(01111),他与key.hashcode相与的结果就是我们需要的地址坐标了,高位则可以直接舍弃。而位运算在计算机中能提升效率。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KEQUPfvY-1661072359628)(C:\Users\ASUS\AppData\Roaming\marktext\images\2022-07-25-15-16-03-image.png)]
2.JDK7的hashmap有什么缺点:
潜在安全问题
可能会被大量hashcode相同的key攻击,退化成一个链表,拖慢系统运行效率,因为链表查询效率太低了。
出现死锁
因为JKD7采用的是"头插法",而且hashmap是没有同步锁的,所以当外部没有使用同步操作限制读取时,在一定的机缘巧合下,hashmap发生了扩容重组(rehash),本来的结构和新诞生的结构可能会发生死锁。
正常的rehash过程:(注意是头插法)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K67saafD-1661072359629)(C:\Users\ASUS\AppData\Roaming\marktext\images\2022-07-25-15-51-09-image.png)]
而在并发情况下的rehash过程:
首先是transfer函数的源码:
do {
Entry<K,V> next = e.next; // <--假设线程一执行到这里就被调度挂起了
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
假设有线程一和线程二,分别使用红色和蓝色来标记;
(1)当线程一在第一行Entry<K,V> next = e.next被挂起,此时e指向key(3),next指向key(7);
而线程二此时正常运行:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y7ZzL6Cl-1661072359629)(C:\Users\ASUS\AppData\Roaming\marktext\images\2022-07-25-16-03-36-image.png)]
当线程二完成rehash以后,线程一指向的就是rehash以后的链表,可以看到3和7的位置已经交换了。
(2)线程一被调度回来继续运行:
- 先是执行 newTalbe[i] = e;
- 然后是e = next,导致了e指向了key(7),
- 而下一次循环的next = e.next导致了next指向了key(3)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KgZr9C3Z-1661072359630)(C:\Users\ASUS\AppData\Roaming\marktext\images\2022-07-25-16-07-25-image.png)]
(3)目前还是一切正常,继续:
线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-obEEBUts-1661072359631)(C:\Users\ASUS\AppData\Roaming\marktext\images\2022-07-25-16-08-22-image.png)]
(4)出现循环
e.next = newTable[i] 导致 key(3).next 指向了 key(7)
注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qy5z2h6O-1661072359631)(C:\Users\ASUS\AppData\Roaming\marktext\images\2022-07-25-16-10-02-image.png)]
所以在并发环境下应该使用ConcurrentHashMap.
原帖:https://coolshell.cn/articles/9606.html
3.在JDK1.8中为什么红黑树的阈值是8:
HashMap桶中添加元素时,若链表个数超过8,链表会转换成红黑树。 那么,为什么HasMap红黑树的阈值为什么是8呢?
hashcode碰撞次数的泊松分布有关,主要是为了寻找一种时间和空间的平衡。在负载因子0.75(HashMap默认)的情况下,单个hash槽内元素个数为8的概率小于百万分之一,将7作为一个分水岭,等于7时不做转换,大于等于8才转红黑树,小于等于6才转链表。链表中元素个数为8时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了8,是根据概率统计而选择的。(即链表长度在大于8以后再出现hash碰撞的可能性几乎为0)
4.为什么默认的加载因子等于0.75:
加载因子表示Hash表中元素的填满程度,加载因子=填入表中的元素个数/散列表的长度
加载因子越大,填满的元素越多,空间利用率越高,但发生冲突的机会变大了;
加载因子越小,填满的元素越少,发生冲突的机会减小,但是空间浪费更多,而且还会提高扩容rehash操作的次数;
因此在“冲突的机会”和“空间利用率”之间,寻找一种平衡与折衷。
在hashmap的源码中有注释说到:在理想情况下,使用随机哈希码,在加载因子(扩容阈值)为0.75的情况下,节点出现在频率在hash桶中遵循参数平均为0.5的泊松分布。在这样的泊松分布条件下,当一个桶中的链表长度达到8个元素时,概率为0.00000006,几乎是一个不可能事件。
5.rehash()方法解读:
6.成员方法的解读:
put方法:
- 第一步当然是先计算key的hash值(有过处理的 (h = key.hashCode()) ^ (h >>> 16))
- 第二步调用putval方法,然后判断是否容器中全部为空,如果是的话,就把容器的容量扩容。
- 第三步,把最大容量和hash值求&值(i = (n - 1) & hash),判断这个数组下标是否有数据,如果没有就把它放进去。还要判断key的equals方法,看是否需要覆盖。
- 第四步,如果有,说明发生了碰撞,那么继续遍历判断链表的长度是否大于8,如果大于8,就继续把当前链表变成红黑树结构。
- 第五步,如果没有到8,那么就直接把数据存在链表的尾部
- 第六步,最后将容器的容量+1。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pmyojuRV-1661072359632)(C:\Users\ASUS\AppData\Roaming\marktext\images\2022-07-28-16-31-52-image.png)]
get方法:
提供给用户使用的是getNode(key)方法
- 第一步,看下整个容器是否为空。
- 第二步,如果不为空,再比较hash值的同时需要比较key的值是否相同e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))
- 然后返回
contains方法:
HashMap没有提供判断元素是否存在的方法,只提供了判断Key是否存在及Value是否存在的方法,分别是containsKey(Object key)、containsValue(Object value)。
containsKey(Object key)方法很简单,只是判断getNode (key)的结果是否为null,是则返回false,否返回true
containsValue(Object value)方法判断一个value是否存在比判断key是否存在还要简单,就是遍历所有元素判断是否有相等的值。这里分为两种情况处理,value为null何不为null的情况,但内容差不多,只是判断相等的方式不同。这个判断是否存在必须遍历所有元素,是一个双重循环的过程,因此是比较耗时的操作。
remove方法:
HashMap中“删除”相关的操作,有remove(Object key)和clear()两个方法。
其中向用户开放的remove方法调用的是removeNode方法,removeNode (key)的返回结果应该是被移除的元素,如果不存在这个元素则返回为null。
而clear()方法更简单,删除HashMap中所有的元素,这里就不用一个个删除节点了,而是直接将table数组内容都置空,这样所有的链表都已经无法访问,Java的垃圾回收机制会去处理这些链表。table数组置空后修改size为0。
7.红黑树
上文一直有提到红黑树,那么我们再来说一说红黑树,红黑树是一种自平衡的二叉查找树,是一种高效的查找树。红黑树具有良好的效率,它可在 O(logN)
时间内完成查找、增加、删除等操作。他具有以下特点:
-
节点不是红色就是黑色
-
根节点是黑色
-
空节点为叶子,叶子也是黑色
-
红节点的两个孩子必为黑色(如果全是黑节点,则树必须是满的,要符合第五点)
-
从任意节点到每个叶子,经过的黑色节点数相等
-
其中条件四和五最为关键,他们两个限制了任意节点到其每个叶子节点路径最长不会超过最短路径的2倍(即最长的情况是黑色节点数等于红色节点数)
当树出现不平衡的情况的时候,有两种操作:调色和调位置;
能调色的时候优先调色,根据父、叔节点的颜色进行调整;再根据祖先和根节点的情况继续调整,若调整完还是不满足4、5这两个条件的话,则进行旋转操作调位置;
左旋右旋操作:左旋是将某个节点旋转为其右孩子的左孩子,而右旋是节点旋转为其左孩子的右孩子,实际情况如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5YgY8SJt-1661072359632)(C:\Users\ASUS\AppData\Roaming\marktext\images\2022-07-28-15-01-44-image.png)]
而旋转的具体步骤(以右旋为例)如下:
-
M的左子树的引用指向子树2
-
E的右子树的引用指向M,完成旋转
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jDK6HxUo-1661072359633)(C:\Users\ASUS\AppData\Roaming\marktext\images\2022-07-28-15-07-17-image.png)]
红黑树的操作:
插入: 红黑树的插入过程和二叉查找树插入过程基本类似,不同的地方在于,红黑树插入新节点后,需要进行调整,以满足红黑树的性质。插入的节点应该为红色,因为插入红色不会导致整条路径上的黑色节点数量发生变化,仅可能出现两个连续的红色节点的情况,这种情况通过变色和旋转进行调整即可。(插入时比较需要注意的是叔叔节点的颜色,如果叔叔节点和父亲节点都是红色,那直接变色即可,如果不是则要旋转操作)
情况一为插入根节点,情况二为插入叶子节点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6c2Kwq1T-1661072359634)(C:\Users\ASUS\AppData\Roaming\marktext\images\2022-07-28-15-55-43-image.png)]
删除: 欲删除节点分为三大类:欲删除节点为叶子节点、欲删除节点仅仅有一个子节点和欲删除有两个子节点。而欲删除节点有两种可能的颜色,也须要分别对待。
LinkedHashMap
概述:
LinkedHashMap是HashMap的子类,它的大部分实现与HashMap相同,两者最大的区别在于,HashMap的对哈希表进行迭代时是无序的,而 LinkedHashMap对哈希表迭代是有序的,LinkedHashMap默认的规则是,迭代输出的结果保持和插入key-value pair的顺序一致(当然具体迭代规则可以修改)。LinkedHashMap除了像HashMap一样用数组、单链表和红黑树来组织数据外,还额外维护了一个 双向链表,每次向linkedHashMap插入键值对,除了将其插入到哈希表的对应位置之外,还要将其插入到双向循环链表的尾部。
数据结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l3IW9agf-1661072359635)(C:\Users\ASUS\AppData\Roaming\marktext\images\2022-07-28-17-09-01-image.png)]
LinkedHashMap与HashMap的不同点?
结构上没有本质的区别,LinkedHashMap记录了键值对的插入顺序,可以按照插入顺序来遍历键值对,而HashMap没有记录插入顺序。
LinkedHashMap是如何记录的插入顺序的呢?
LinkedHashMap继承了HashMap,本身具有HashMap的全部特性,LinkedHashMap内部的Entry类继承了HashMap的Node类,而这个Entry类比Node类多了两个指针,这两个指针分别指向Entry的前驱节点和后继节点,靠这两个指针就可以维护一个键值对的双向链表,从而记录插入顺序。
LinkedHashMap中accessOrder的作用?
accessOrder是LinkedHashMap中的一个布尔型成员变量。false代表着插入顺序,true代表着访问顺序。也就是accessOrder为true时,最新访问的键值对会被放到双向链表的末尾。
ConcurrentHashMap
概述:
ConcurrentHashMap是conccurrent家族中的一个类,由于它可以高效地支持并发操作,以及被广泛使用。与同是线程安全的老大哥HashTable相比,它已经更胜一筹,因此它的锁更加细化,而不是像HashTable一样为几乎每个方法都添加了synchronized锁,这样的锁无疑会影响到性能。1.7和1.8实现线程安全的思想也已经完全变了其中抛弃了原有的Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想,但是为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。
实现原理:
JKD1.7版本的实现:JDK1.7 中的 ConcurrentHashMap 是由 Segment
数组结构和 HashEntry
数组结构组成,即 ConcurrentHashMap 把哈希桶数组切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成。
如下图所示,首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gzGBiVM1-1661072359635)(C:\Users\ASUS\AppData\Roaming\marktext\images\2022-07-28-23-42-31-image.png)]
Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下:
Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。Segment 默认为 16,也就是并发度为 16。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zpzQlkzF-1661072359636)(C:\Users\ASUS\AppData\Roaming\marktext\images\2022-07-28-23-46-06-image.png)]
存放元素的 HashEntry,也是一个静态内部类,主要的组成如下:
用 volatile 修饰了 HashEntry 的数据 value 和 下一个节点 next,保证了多线程环境下数据获取时的可见性!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kSOjRvMq-1661072359637)(C:\Users\ASUS\AppData\Roaming\marktext\images\2022-07-28-23-46-55-image.png)]
JDK1.8版本的实现:
在数据结构上, JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构;
在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized
实现更加细粒度的锁。将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jUJlyaht-1661072359637)(C:\Users\ASUS\AppData\Roaming\marktext\images\2022-07-28-23-56-13-image.png)]
JDK1.8 中为什么使用内置锁 synchronized替换 可重入锁 ReentrantLock?
-
在 JDK1.6 中,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态,会从无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁一步步转换。
-
减少内存开销 。假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。
ConcurrentHashMap的put和get操作对比
ConcurrentHashMap 的 get 方法是否要加锁,为什么?
get 方法不需要加锁。因为 Node 的元素 value 和指针 next 是用 volatile 修饰的,在多线程环境下线程A修改节点的 value 或者新增节点的时候是对线程B可见的。
这也是它比其他并发集合比如 Hashtable、用 Collections.synchronizedMap()包装的 HashMap 效率高的原因之一。
get 方法不需要加锁与 volatile 修饰的哈希桶数组有关吗?
没有关系。哈希桶数组table
用 volatile 修饰主要是保证在数组扩容的时候保证可见性。
ConcurrentHashMap 不支持 key 或者 value 为 null 的原因?
假设 ConcurrentHashMap 允许存放值为 null 的 value,这时有A、B两个线程,线程A调用ConcurrentHashMap.get(key)
方法,返回为 null ,我们不知道这个 null 是没有映射的 null ,还是存的值就是 null 。
假设此时,返回为 null 的真实情况是没有找到对应的 key。那么,我们可以用 ConcurrentHashMap.containsKey(key)
来验证我们的假设是否成立,我们期望的结果是返回 false 。
但是在我们调用 ConcurrentHashMap.get(key)方法之后,containsKey方法之前,线程B执行了ConcurrentHashMap.put(key, null)的操作。那么我们调用containsKey方法返回的就是 true 了,这就与我们的假设的真实情况不符合了,这就有了二义性。
JDK1.7 与 JDK1.8 中ConcurrentHashMap 的区别?
-
数据结构:取消了 Segment 分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
-
保证线程安全机制:JDK1.7 采用 Segment 的分段锁机制实现线程安全,其中 Segment 继承自 ReentrantLock 。JDK1.8 采用CAS+synchronized保证线程安全。
-
锁的粒度:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8 调整为对每个数组元素加锁(Node)。
-
链表转化为红黑树:定位节点的 hash 算法简化会带来弊端,hash 冲突加剧,因此在链表节点数量大于 8(且数据总量大于等于 64)时,会将链表转化为红黑树进行存储。
-
查询时间复杂度:从 JDK1.7的遍历链表O(n), JDK1.8 变成遍历红黑树O(logN)。
HashTable
概述:hashtable的实现是数组+链表,没有使用红黑树; 也是一个散列表,它存储的内容是键值对(key-value)映射。通过拉链法实现的哈希表。Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
HashMap和HashTable的区别
key和value的取值范围不同
HashTable中不允许null值(key和value都不可以)。
HashMap中允许null值(key和value都可以)。但是key=null的键只允许存在一个,value=null可以存在多个。
HashTable中的put()方法,首先通过if判断value是否为null,若为空,则直接抛出空指针异常。
key也不允许为null(因为调用key.hashCode()方法时,若key = null,也会造成空指针异常)。
而在HashMap中,允许value=null,也允许key = null。HashMap.hash()方法中,当key=null时,hash()方法返回的值是0。 对于HashMap如果使用get方法返回null,并不能表明HashMap不存在这个key,有可能是键对应的值为null。
线程安全
HashTable中的大多数方法上都加了synchronized关键字进行修饰,用于保证线程安全,但效率较低。(相当于表锁,粒度大)
而HashMap中的方法上,没有synchronized关键字,不能保证线程安全,但效率较高。
而concurrenthashmap的只对链表头位置上锁,粒度更小,效率更高。
效率与同步
HashMap是不同步的、效率高的,HashTable是同步的、效率低的。
虽然HashMap不是线程安全的,但是它的效率会比HashTable要好很多。这样设计是合理的。在我们的日常使用当中,大部分时间是单线程操作的。HashMap把这部分操作解放出来了。
当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比HashTable要高很多。
选择与使用
如果不需要线程安全,那么使用HashMap,如果需要线程安全,那么使用ConcurrentHashMap,ConcurrentHashMap不但是线程安全的,效率也比HashTable要高,HashTable已经几乎被淘汰了。
Collection 子接口之 Set
HashSet
概述:实现Set接口,由哈希表(实际是一个hashmap对象)支持,它不保证set的迭代顺序;特别是它不保证该顺序恒久不变。此类允许下使用null元素。Set集合无索引,不可以重复,无序(存储不一致)它的继承系统中有toString方法,故不需要书写toString()方法。
原理:
-
我们使用Set集合都是需要去掉重复元素的, 如果在存储的时候逐个equals()比较, 效率较低,哈希算法提高了去重复的效率, 降低了使用equals()方法的次数
-
当HashSet调用add()方法存储对象的时候, 先调用对象的hashCode()方法得到一个哈希值, 然后在集合中查找是否有哈希值相同的对象
-
如果没有哈希值相同的对象就直接存入集合
-
如果有哈希值相同的对象, 就和哈希值相同的对象逐个进行equals()比较,比较结果为false就存入, true则不存
将自定义类的对象存入HashSet去重复
-
类中必须重写hashCode()和equals()方法
-
hashCode(): 属性相同的对象返回值必须相同, 属性不同的返回值尽量不同(提高效率)
-
equals(): 属性相同返回true, 属性不同返回false,返回false的时候存储。
LinkedHashSet
概述:
LinkedHashSet继承的是HashSet,自身的构造器基本上都是调用的父类构造器的三个参数的构造器,该构造器使用的是LinkedHashMap存储元素。
LinkedHashSet是有序的,维护了元素的插入顺序;
LinkedHashSet是不支持按访问顺序对元素排序的,只能按插入顺序排序。
TreeSet
概述:
TreeSet是一个有序集合,它扩展了AbstractSet类并实现了NavigableSet接口。底层使用红黑树来储存对象。
以下是此实现最重要方面的快速摘要:
- 它存储唯一的元素
- 它不保留元素的插入顺序
- 它按升序对元素进行排序
- 它不是线程安全的
Collection 子接口之 List
ArrayList
概述:
ArrayList是一个其容量能够动态增长的动态数组。但是他又和数组不一样,下面会分析对比。它继承了AbstractList,实现了List、RandomAccess, Cloneable, java.io.Serializable。
RandomAccess接口,被List实现之后,为List提供了随机访问功能,也就是通过下标获取元素对象的功能。
实现了Cloneable, java.io.Serializable意味着可以被克隆和序列化。
继承关系:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-18UyNbAj-1661072359638)(C:\Users\ASUS\AppData\Roaming\marktext\images\2022-07-30-00-10-06-image.png)]
为什么要先继承AbstractList,而让AbstractList先实现List?而不是让ArrayList直接实现List?
这里是有一个思想,接口中全都是抽象的方法,而抽象类中可以有抽象方法,还可以有具体的实现方法,正是利用了这一点,让AbstractList是实现接口中一些通用的方法,而具体的类,如ArrayList就继承这个AbstractList类,拿到一些通用的方法,然后自己在实现一些自己特有的方法,这样一来,让代码更简洁,就继承结构最底层的类中通用的方法都抽取出来,先一起实现了,减少重复代码。所以一般看到一个类上面还有一个抽象类,应该就是这个作用。
RandomAccess接口
RandomAccess接口是一个标志接口(Marker),里面是空的
ArrayList集合实现这个接口,就能支持快速随机访问
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
实现RandomAccess接口的List集合采用一般的for循环遍历,而未实现这接口则采用迭代器。
ArrayList用for循环遍历比iterator迭代器遍历快,LinkedList用iterator迭代器遍历比for循环遍历快,
所以说,当我们在做项目时,应该考虑到List集合的不同子类采用不同的遍历方式,能够提高性能!
然而有人发出疑问了,那怎么判断出接收的List子类是ArrayList还是LinkedList呢?
这时就需要用instanceof来判断List集合子类是否实现RandomAccess接口!
核心方法grow()
arrayList核心的方法,能扩展数组大小的真正秘密。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; //将扩充前的elementData大小给oldCapacity
int newCapacity = oldCapacity + (oldCapacity >> 1);//newCapacity就是1.5倍的oldCapacity
if (newCapacity - minCapacity < 0)//这句话就是适应于elementData就空数组的时候,length=0,那么oldCapacity=0,newCapacity=0,所以这个判断成立,在这里就是真正的初始化elementData的大小了,就是为10.前面的工作都是准备工作。
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)//如果newCapacity超过了最大的容量限制,就调用hugeCapacity,也就是将能给的最大值给newCapacity
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//新的容量大小已经确定好了,就copy数组,改变容量大小咯。
elementData = Arrays.copyOf(elementData, newCapacity);
}
//用于赋最大值的方法
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//如果minCapacity都大于MAX_ARRAY_SIZE,那么就Integer.MAX_VALUE返回,反之将MAX_ARRAY_SIZE返回。因为maxCapacity是三倍的minCapacity,可能扩充的太大了,就用minCapacity来判断了。
//Integer.MAX_VALUE:2147483647 MAX_ARRAY_SIZE:2147483639 也就是说最大也就能给到第一个数值。还是超过了这个限制,就要溢出了。相当于arraylist给了两层防护。
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
总结扩容过程:
- 1.判断是否需要扩容,如果需要,计算需要扩容的最小容量
- 2.如果确定扩容,就执行grow(int minCapacity),minCapacity为最少需要的容量
- 3.第一次扩容是的容量大小是原来的1.5倍
- 4.如果第一次 扩容后容量还是小于minCapacity,那就扩容为minCapacity
- 5.最后,如果minCapacity大于最大容量,则就扩容为最大容量
ArrayList的优缺点:
- ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快
- ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已
- 删除元素时,涉及到元素复制,如果要复制的元素很多,那么就会比较耗费性能
- 插入元素时,涉及到元素复制,如果要复制的元素很多,那么就会比较耗费性能
Vector
概述:
Vector
是 List
的古老实现类,底层使用Object[ ]
存储,线程安全的
底层是数组,所以大多数是相同了 只是在方法上加了synchronized锁
vector与arraylist的另一个区别在grow()函数,如果定义了增长因子就每次扩容增长因子,不然就是扩容2倍.
LinkedList
概述:
LinkedList是一种常用的数据容器,与ArrayList相比,LinkedList的增删操作效率更高,而查改操作效率较低。
- LinkedList是一个双向链表
- 也就是说list中的每个元素,在存储自身值之外,还额外存储了其前一个和后一个元素的地址,所以 也就可以很方便地根据当前元素获取到其前后的元素
- 链表的尾部元素的后一个节点是链表的头节点;而链表的头结点前一个节点则是则是链表的尾节点
ArrayList 和 LinkedList 的区别:
- 是否保证线程安全:
ArrayList
和LinkedList
都是不同步的,也就是不保证线程安全; - 底层数据结构:
Arraylist
底层使用的是Object
数组;LinkedList
底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) - 插入和删除是否受元素位置的影响: ①
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, E element)
) 时间复杂度近似为o(n))
因为需要先移动到指定位置再插入。 - 是否支持快速随机访问:
LinkedList
不支持高效的随机元素访问,而ArrayList
支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)
方法)。 - 内存空间占用:
ArrayList
的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而LinkedList
的空间花费则体现在它的每一个元素都需要消耗比ArrayList
更多的空间(因为要存放直接后继和直接前驱以及数据)。
lement))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② **
LinkedList采用链表存储,所以对于
add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置
i插入和删除元素的话(
(add(int index, E element)) 时间复杂度近似为
o(n))`因为需要先移动到指定位置再插入。** - 是否支持快速随机访问:
LinkedList
不支持高效的随机元素访问,而ArrayList
支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)
方法)。 - 内存空间占用:
ArrayList
的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而LinkedList
的空间花费则体现在它的每一个元素都需要消耗比ArrayList
更多的空间(因为要存放直接后继和直接前驱以及数据)。