莱西网站建设,033340网站建设与管理,网站解析出问题 邮件收不到了,怎样做寻亲网站志愿者文章目录 前言源码分析ArrayList基本属性初始化新增元素删除元素遍历元素 LinkedList实现类基本属性节点查询新增元素删除元素遍历元素 分析测试 前言
在面试的时候#xff0c;经常会被问到几个问题#xff1a; ArrayList和LinkedList的区别#xff0c;相信大部分朋友都能回… 文章目录 前言源码分析ArrayList基本属性初始化新增元素删除元素遍历元素 LinkedList实现类基本属性节点查询新增元素删除元素遍历元素 分析测试 前言
在面试的时候经常会被问到几个问题 ArrayList和LinkedList的区别相信大部分朋友都能回答上 ArrayList是基于数组实现LinkedList是基于链表实现当随机访问List时ArrayList比LinkedList的效率更高等等 当被问到ArrayList和LinkedList的使用场景是什么时大部分朋友的答案可能是 ArrayList和LinkedList在新增、删除元素时LinkedList的效率要高于 ArrayList而在遍历的时候ArrayList的效率要高于LinkedList
那这个回答是否准确呢今天我们就来研究研究 从源码角度解析ArrayList.subList的几个坑 我们先来简单介绍下ArrayList和LinkedList的原理实现
源码分析
ArrayList
实现类
public class ArrayListE extends AbstractListEimplements ListE, RandomAccess, Cloneable, java.io.SerializableArrayList实现了List接口继承了AbstractList抽象类底层是数组实现的并且实现了自增扩容数组大小。
ArrayList还实现了Cloneable接口和Serializable接口所以他可以实现克隆和序列化。
ArrayList还实现了RandomAccess接口这个接口是一个标志接口他标志着“只要实现该接口的List类都能实现快速随机访问”。
基本属性
ArrayList属性主要由数组长度size、对象数组elementData、初始化容量default_capacity等组成 其中初始化容量默认大小为10。
//默认初始化容量
private static final int DEFAULT_CAPACITY 10;
//对象数组
transient Object[] elementData;
//数组长度
private int size;从ArrayList属性来看elementData被关键字transient修饰了transient关键字修饰该字段则表示该属性不会被序列化。
但ArrayList其实是实现了序列化接口这是为什么呢
由于ArrayList的数组是基于动态扩增的所以并不是所有被分配的内存空间都存储了数据。 如果采用外部序列化法实现数组的序列化会序列化整个数组ArrayList为了避免这些没有存储数据的内存空间被序列化内部提供了两个私有方法writeObject以及readObject来自我完成序列化与反序列化从而在序列化与反序列化数组时节省了空间和时间。 因此使用transient修饰数组是防止对象数组被其他外部方法序列化。 ArrayList自定义序列化方法如下
初始化
有三种初始化办法无参数直接初始化、指定大小初始化、指定初始数据初始化源码如下
当ArrayList新增元素时如果所存储的元素已经超过其已有大小它会计算元素大小后再进行动态扩容数组的扩容会导致整个数组进行一次内存复制。
因此我们在初始化ArrayList时可以通过第一个构造函数合理指定数组初始大小这样有助于减少数组的扩容次数从而提高系统性能。
注意点
ArrayList 无参构造器初始化时默认大小是空数组并不是大家常说的 1010 是在第一次 add 的时候扩容的数组值。
新增元素
ArrayList新增元素的方法有两种一种是直接将元素加到数组的末尾另外一种是添加元素到任意位置。
public boolean add(E e) {ensureCapacityInternal(size 1); // Increments modCount!!elementData[size] e;return true;}
public void add(int index, E element) {rangeCheckForAdd(index);
ensureCapacityInternal(size 1); // Increments modCount!!System.arraycopy(elementData, index, elementData, index 1,size - index);elementData[index] element;size;}两个方法的相同之处是在添加元素之前都会先确认容量大小如果容量够大就不用进行扩容如果容量不够大就会按照原来数组的1.5倍大小进行扩容在扩容之后需要将数组复制到新分配的内存地址。 下面是具体的源码
这两个方法也有不同之处添加元素到任意位置会导致在该位置后的所有元素都需要重新排列而将元素添加到数组的末尾在没有发生扩容的前提下是不会有元素复制排序过程的。
所以ArrayList在大量新增元素的场景下效率不一定就很慢的
如果我们在初始化时就比较清楚存储数据的大小就可以在ArrayList初始化时指定数组容量大小并且在添加元素时只在数组末尾添加元素那么ArrayList在大量新增元素的场景下性能并不会变差反而比其他List集合的性能要好。
删除元素
ArrayList 删除元素有很多种方式比如根据数组索引删除、根据值删除或批量删除等等原理和思路都差不多。 ArrayList在每一次有效的删除元素操作之后都要进行数组的重组并且删除的元素位置越靠前数组重组的开销就越大。 我们选取根据值删除方式来进行源码说明
遍历元素
由于ArrayList是基于数组实现的所以在获取元素的时候是非常快捷的。
public E get(int index) {rangeCheck(index);
return elementData(index);}
E elementData(int index) {return (E) elementData[index];}LinkedList
LinkedList是基于双向链表数据结构实现的。 这个双向链表结构链表中的每个节点都可以向前或者向后追溯有几个概念如下
链表每个节点我们叫做 NodeNode 有 prev 属性代表前一个节点的位置next 属性代表后一个节点的位置first 是双向链表的头节点它的前一个节点是 null。last 是双向链表的尾节点它的后一个节点是 null 当链表中没有数据时first 和 last 是同一个节点前后指向都是 null因为是个双向链表只要机器内存足够强大是没有大小限制的。
Node结构中包含了3个部分元素内容item、前指针prev以及后指针next代码如下。
private static class NodeE {E item;// 节点值NodeE next; // 指向的下一个节点NodeE prev; // 指向的前一个节点
// 初始化参数顺序分别是前一个节点、本身节点值、后一个节点Node(NodeE prev, E element, NodeE next) {this.item element;this.next next;this.prev prev;}
}LinkedList就是由Node结构对象连接而成的一个双向链表。
实现类
LinkedList类实现了List接口、Deque接口同时继承了AbstractSequentialList抽象类LinkedList既实现了List类型又有Queue类型的特点LinkedList也实现了Cloneable和Serializable接口同ArrayList一样可以实现克隆和序列化。 由于LinkedList存储数据的内存地址是不连续的而是通过指针来定位不连续地址因此LinkedList不支持随机快速访问LinkedList也就不能实现RandomAccess接口。
public class LinkedListextends AbstractSequentialListimplements List, Deque, Cloneable, java.io.Serializable基本属性
transient int size 0;
transient Node first;
transient Node last;我们可以看到这三个属性都被transient修饰了原因很简单我们在序列化的时候不会只对头尾进行序列化所以LinkedList也是自行实现readObject和writeObject进行序列化与反序列化。 下面是LinkedList自定义序列化的方法。
节点查询
链表查询某一个节点是比较慢的需要挨个循环查找才行我们看看 LinkedList 的源码是如何寻找节点的
LinkedList 并没有采用从头循环到尾的做法而是采取了简单二分法首先看看 index 是在链表的前半部分还是后半部分。 如果是前半部分就从头开始寻找反之亦然。通过这种方式使循环的次数至少降低了一半提高了查找的性能。
新增元素
LinkedList添加元素的实现很简洁但添加的方式却有很多种。 默认的add (Ee)方法是将添加的元素加到队尾首先是将last元素置换到临时变量中生成一个新的Node节点对象然后将last引用指向新节点对象之前的last对象的前指针指向新节点对象。
LinkedList也有添加元素到任意位置的方法如果我们是将元素添加到任意两个元素的中间位置添加元素操作只会改变前后元素的前后指针指针将会指向添加的新元素所以相比ArrayList的添加操作来说LinkedList的性能优势明显。
删除元素
在LinkedList删除元素的操作中我们首先要通过循环找到要删除的元素如果要删除的位置处于List的前半段就从前往后找若其位置处于后半段就从后往前找。 这样做的话无论要删除较为靠前或较为靠后的元素都是非常高效的但如果List拥有大量元素移除的元素又在List的中间段那效率相对来说会很低。
遍历元素
LinkedList的获取元素操作实现跟LinkedList的删除元素操作基本类似通过分前后半段来循环查找到对应的元素但是通过这种方式来查询元素是非常低效的特别是在for循环遍历的情况下每一次循环都会去遍历半个List。 所以在LinkedList循环遍历时我们可以使用iterator方式迭代循环直接拿到我们的元素而不需要通过循环查找List。 分析测试 新增元素操作性能测试 测试用例源代码
ArrayListpaste.ubuntu.com/p/gktBvjgMG…
LinkedListpaste.ubuntu.com/p/3jQrY2XMP…分析测试 测试结果
操作花费时间从集合头部位置添加元素ArrayList550从集合头部位置添加元素LinkedList34从集合中间位置位置添加元素ArrayList32从集合中间位置位置添加元素LinkedList58746从集合尾部位置添加元素ArrayList29从集合尾部位置添加元素LinkedList31 通过这组测试我们可以知道LinkedList添加元素的效率未必要高于ArrayList。
从集合头部位置添加元素
由于ArrayList是数组实现的在添加元素到数组头部的时候需要对头部以后的数据进行复制重排所以效率很低 LinkedList是基于链表实现在添加元素的时候首先会通过循环查找到添加元素的位置如果要添加的位置处于List的前半段就从前往后找若其位置处于后半段就从后往前找因此LinkedList添加元素到头部是非常高效的。
从集合中间位置位置添加元素
ArrayList在添加元素到数组中间时同样有部分数据需要复制重排效率也不是很高 LinkedList将元素添加到中间位置是添加元素最低效率的因为靠近中间位置在添加元素之前的循环查找是遍历元素最多的操作。
从集合尾部位置添加元素
而在添加元素到尾部的操作中在没有扩容的情况下ArrayList的效率要高于LinkedList。 这是因为ArrayList在添加元素到尾部的时候不需要复制重排数据效率非常高。 LinkedList虽然也不用循环查找元素但LinkedList中多了new对象以及变换指针指向对象的过程所以效率要低于ArrayList。
注意这是排除动态扩容数组容量的情况下进行的测试如果有动态扩容的情况ArrayList的效率也会降低。
删除元素操作性能测试 ArrayList和LinkedList删除元素操作测试的结果和添加元素操作测试的结果很接近 结论 如果需要在List的头部进行大量的插入、删除操作那么直接选择LinkedList。否则ArrayList即可。 遍历元素操作性能测试 测试用例源代码 测试结果 操作花费时间for循环ArrayList3for循环LinkedList17557迭代器循环ArrayList4迭代器循环LinkedList4 我们可以看到LinkedList的for循环性能是最差的而ArrayList的for循环性能是最好的。 这是因为LinkedList基于链表实现的在使用for循环的时候每一次for循环都会去遍历半个List所以严重影响了遍历的效率ArrayList则是基于数组实现的并且实现了RandomAccess接口标志意味着ArrayList可以实现快速随机访问所以for循环效率非常高。 LinkedList的迭代循环遍历和ArrayList的迭代循环遍历性能相当也不会太差所以在遍历LinkedList时我们要切忌使用for循环遍历。