Java集合【7】--List接口超级详细解析
1.List接口的特性
java.util.List
接口继承于 Collection
接口,与Map
最大的不同之处,在于它属于单列集合,相当于一个列表,有以下这些特点:
- 有顺序,按照添加的顺序存储,是一种线性结构。
- 可以根据索引查询元素。
- 元素可以重复。
An ordered collection(also known as a sequence ).The user of this interface has precise control over where in the list each element is inserted.The user can access elements by their integer index(position in the list), and search for elements in the list.
Unlike sets, lists typically allow duplicate elements. More formally,lists typically allow pairs of elements e1 and e2 such that e1.equals(e2), and they typically allow multiple null elements if they allow null elements at all. It is not inconceivable that someone might wish to implement a list that prohibits duplicates, by throwing runtime exceptions when the user attempts to insert them, but we expect this usage to be rare.
下面是List
接口的继承关系:
2.List接口的源码解析
继承于Collection
接口,有顺序,取出的顺序与存入的顺序一致,有索引,可以根据索引获取数据,允许存储重复的元素,可以放入为null的元素。
最常见的三个实现类就是ArrayList
,Vector
,LinkedList
,ArrayList
和Vector
都是内部封装了对数组的操作,唯一不同的是,Vector
是线程安全的,而ArrayList
不是,理论上ArrayList
操作的效率会比Vector
好一些。
里面是接口定义的方法:
比较常用的几个方法无非增删改查:
上面的方法都比较简单,值得一提的是里面出现了ListIterator
,这是一个功能更加强大的迭代器,继承于Iterator
,只能用于List
类型的访问,拓展功能例如:通过调用listIterator()
方法获得一个指向List开头的ListIterator
,也可以调用listIterator(n)
获取一个指定索引为n的元素的ListIterator
,这是一个可以双向移动的迭代器。
操作数组索引的时候需要注意,由于List的实现类底层很多都是数组,所以索引越界会报错IndexOutOfBoundsException
。
3.相关子类介绍
说起List的实现子类,最重要的几个实现类如下:
ArrayList
:底层存储结构是数组结构,增加删除比较慢,查找比较快,是最常用的List集合。线程不安全。LinkedList
:底层是链表结构,增加删除比较快,但是查找比较慢。线程不安全。Vector
:和ArrayList差不多,但是是线程安全的,即同步。
3.1 ArrayList
ArrayList
继承了AbstractList
接口,实现了List
,以及随机访问,可克隆,序列化接口。不是线程安全的,如果需要线程安全,则需要选择其他的类或者使用Collections.synchronizedList(arrayList)
允许存储null元素,也允许相同的元素存在。
其底层实际上是数组实现的,那为什么我们使用的时候只管往里面存东西,不用关心其大小呢?因为ArrayList
封装了这样的功能,容量可以动态变化,不需要使用者关心。
3.1.1 成员变量
transient
表示这个属性不需要自动序列化,因为element存储的不是真的元素的对象,而是指向对象的地址,所以这样的属性序列化是没有太大意义的。对地址序列化之后,反序列化的时候找不到之前的对象,所以需要手动实现对对象的序列化。
那在哪里去实现对西那个的序列化和反序列化的呢?这个需要我们看源码里面的readOject()
和writeOject()
两个方法。其实就除了默认的序列化其他字段,这个elementData
字段,还需要手动序列化和反序列化。
很多人可能会有疑问,为什么这个函数没有看到有调用呢?在哪里调用的呢?
其实就是在对象流中,通过反射的方式进行调用的,这里就不展开了,下次一定!!!有兴趣可以看看。
如果我们创建的时候不指定大小,那么就会初始化一个默认大小为10。
里面定义了两个空数组,EMPTY_ELEMENTDATA
名为空数组,DEFAULTCAPACITY_EMPTY_ELEMENTDATA
名为默认大小空数组,用来区分是空构造函数还是带参数构造函数构造的arrayList,第一次添加元素的时候使用不同的扩容。之所以是一个空数组,不是null,是因为使用的时候我们需要制定参数的类型。
还有一个特殊的成员变量modCount
,这是快速失败机制所需要的,也就是记录修改操作的次数,主要是迭代的时候,防止元素被修改。如果操作前后的修改次数对不上,那么有些操作就是非法的。transient
表示这个属性不需要自动序列化。
序列化id如下:为什么需要这个字段呢?这是因为如果没有显示声明这个字段,那么序列化的时候回自动生成一个序列化的id,这样子的话,假设序列化完成之后,往原来的类里面添加了一个字段,那么这个时候反序列化会失败,因为默认的序列化id已经改变了。假设我们给它指定了序列化id的话,就可以避免这种问题,只是增加的字段反序列化的时候是空的。
3.1.2 构造方法
构造方法有三个,可以指定容量,指定初始的元素集,可以什么都不指定。
3.1.3 常用增删改查方法
添加元素
add()
方法有两个:
add(E e)
:添加一个元素,默认是在末尾添加add(int index, E element)
:在指定位置index添加(插入)一个元素
查询元素
get()
方法相对比较简单,获取之前检查参数是否合法,就可以返回元素了。
更新元素
set()
和之前的get()
有点像,但是必须制定修改的元素下标,检查下标之后,修改,然后返回旧的值。
删除元素
按照元素移除,或者按照下标移除元素
ArrayList
是基于数组动态扩容的,那它什么时候扩容的呢?好像上面的源代码中我们没有看到,其实是有的,所谓扩容嘛,就是容量不够了,那么容量不够的时候只会发生在初始化一个集合的时候或者是增加元素的时候,所以是在add()
方法里面去调用的。
在最小调用的时候容量不满足的时候,会调用grow()
,grow()
是真正扩容的函数,这里不展开了。
3.1.4 小结一下
- ArrayList是基于动态数组实现的,增加元素的时候,可能会触发扩容操作。扩容之后会触发数组的拷贝复制。remove操作也会触发复制,后面的元素统一往前面挪一位,原先最后面的元素会置空,这样可以方便垃圾回收。
- 默认的初始化容量是10,容量不够的时候,扩容时候增加为原先容量的一般,也就是原来的1.5倍。
- 线程不安全,但是元素可以重复,而且可以放null值,这个需要注意一下,每次取出来的时候是需要判断是不是为空。
3.2 LinkedList
LinkedList
底层是以双向链表实现的,这是和ArrayList
最大的不同,除此之外它还实现了Deque
接口,继承AbstractSequentialList
,AbstractSequentialList
继承了AbstractList
。
同时它也是线程不安全的。
双向链表底层的数据结构如下:
3.2.1 成员变量
里面的成员变量主要有三个,之所以记录同时记录头结点和尾节点,主要是为了能够更快的插入以及查找数据。
3.2.2 构造函数
构造函数主要有两个,一个是无参数的够着,一个是需要初始化元素集的函数,初始化的时候其实是调用了批量添加元素的函数addAll()
。
3.2.3 常用函数
添加元素
linkFirst()
在队列头部添加元素linkLast()
在队列尾部添加元素
调用add()
方法,默认是在尾部添加元素。
当然,除了可以在收尾添加元素之外,还可以在中间指定位置添加元素,通过下面这个方法实现。
上面有一个函数是node(index)
, 这个函数是根据index索引获取节点,比较有意思的一点,是当这个索引在前面一半的时候,从前面开始遍历,当这个索引在后面半部分的时候,从后面往前面遍历。判断在哪一部分,使用的是二进制。
在某个下标之前插入元素的函数,首先需要判断索引index的位置是否合法,如果index正好等于大小size的话,那就直接在最后面插入该元素即可,否则,需要调用函数,在某个元素之前插入。
在尾部插入节点linkLast()
:
在某个节点的前面插入,调用linkBefore
查询元素
主要是get(int index)
这个函数:
修改元素
修改元素,主要调用的是set(index,element)
方法,主要是先查找到该节点,然后将其item属性修改。
删除元素
removeFirst()
:移除第一个元素removeLast()
:移除最后一个元素remove(Object object)
:移除objectpop()
:移除首个元素remove()
:移除首个元素remove(int index)
:移除索引为index的元素clear()
:移除所有的元素
删除第一个元素
移除最后一个元素:
移除指定的对象,这里指的是节点里面的item,从源码中可以看出,只会移除第一个满足条件的元素,成功移除则返回true,否则将返回false。
默认移除第一个元素,由于LinkedList是双向链表,所以,pop()和reomve()都是默认删除第一个元素。
移除索引下标为index的元素,思路是先查找到该节点,然后调用unlink(node)
移除节点即可。
移除所有的节点:
3.2.4 小结一下
LinkedList
底层就是双向链表,实现了List
和Queue
接口,允许包含所有的元素,元素可以为null。LinkedList
里面的节点为null怎么保存数据?节点为null确实不能保存数据,但是数据是保存在节点下面的item里面的,所以,item可以为null。
每一个节点都保存了前一个节点的引用和下一个节点的引用,以及当前节点的数据。
由于底层是双向链表以及实现了Queue
接口,所以它也可以当成双向队列来使用。
线程不安全,多线程环境下操作很容易出现底层链表错误操作。
3.3 Vector
Vector
和ArrayList
差不多,区别就是Vector
是线程安全的。继承于AbstractList
,实现了List
, RandomAccess
, Cloneable
这些接口,可以随机访问,也可以克隆。
3.3.1 成员变量
底层是数组,增加元素,数组空间不够的时候,需要扩容。
- elementData:真正保存数据的数组
- elementCount:实际元素个数
- capacityIncrement:容量增加系数
- serialVersionUID:序列化id
3.3.2 构造函数
指定容量和增长系数构造函数
指定初始化容量,增长系数默认为0
什么都不指定,默认给的容量是10:
指定集合初始化:
3.3.3 常用方法
增加
增加元素,默认是在最后添加
那么它是如何确保容量的呢?
可以看到ensureCapacityHelper()
里面判断增加后的元素个数是否大于现在数组的长度,如果不满足,就需要扩容。调用grow()
函数扩容。
在指定的索引index,插入数据,实际上调用的是insertElementAt(element, index)
.
将一个集合所有元素添加进去:
指定index,插入一个集合,和前面不一样的地方在于复制之前,需要计算往后面移动多少位,不是用for循环去插入,而是一次性移动和写入。
删除
删除指定元素
按照索引删除元素:
修改
下面两个set函数都是,修改索引为index的元素,区别就是一个会返回旧的元素,一个不会返回旧的元素。
查询
3.3.4 小结一下
Vector
的思路和ArrayList
基本是相同的,底层是数组保存元素,Vector
默认的容量是10,有一个增量系数,如果指定,那么每次都会增加一个系数的大小,否则就扩大一倍。
扩容的时候,其实就是数组的复制,其实还是比较耗时间的,所以,我们使用的时候应该尽量避免比较消耗时间的扩容操作。
和ArrayList最大的不同,是它是线程安全的,几乎每一个方法都加上了Synchronize
关键字,所以它的效率相对也比较低一点。
3.4 顺便说说AbstractList
AbstractList
是一个抽象类,实现了List
接口,继承了AbstractCollection
类。其内部实现了一些增删改查的操作,
我在想为什么需要AbstractList
实现List
,再让ArrayList
去继承AbstractList
。为啥要这样子干,直接继承List
不行么?
这里主要是因为要遵循一个原则,接口中只能有抽象的方法,但是抽象类除了抽象方法之外,还可以有具体的实现方法。而List
的实现类比较多,比如ArrayList
,LinkedList
,Vector
等等,肯定有共同之处,有通用的方法。
要是它们都直接实现List
接口,那么就会产生一些冗余重复的代码。而要是这些共同之处,通用的方法,被抽象出来实现放在AbstractList
里面,多简洁,香不香?香!!!
这样一来,就相当于加了一层中间层,计算机设计的原理也是动不动就加一个缓冲层。(手动狗头)
下面我们来大概介绍一下AbstractList
:
3.4.1 定义以及成员变量
实现了List
接口,继承了AbstractCollection
类,AbstractList无参数构造protected
修饰。
成员变量:修改次数
里面只有一个抽象方法get()
3.4.2 常用方法
增加
定义了一套add方法实现的标准,一个默认在最后添加,一个是在指定索引位置的,但是考虑到不同的子类实现不一样,这个方法必须实现,要不就会抛出异常。
指定索引的位置,添加集合里面所有的元素
修改
删除
删除指定索引的元素,不做实现,要求子类自实现
// 删除所有的元素
删除指定范围的元素
查询
抽象方法,不做实现。
查询索引位置
截取list
3.4.3 迭代器
我们其实可以看到AbstractList
里面定义了两个迭代器,分别是Itr
和ListItr
,Itr
实现了Iterator
接口,ListItr
实现了ListIterator
,继承了Itr
.
细心点我们就会发现,这里面有几个方法都是操作Iterator
相关的。
为什么需要两个迭代器呢?这个问题一直在我的脑海里,挥之不去...
暂且来看看源码:
下面这段代码让我好疑惑,为什么需要判断上一次访问的index是不是小于下一次执行next返回的index才执行-1
的操作
后来我在ListItr
的代码中找到了答案,因为ListItr
有一个方法是previous()
,会倒回迭代器的上一个元素,如果执行了previous()
,那么上一次返回的元素,和下一次执行next返回的元素就是同一个,所以这个时候是可能出现lastRet= cursor
的。
看看ListItr
源码:
为什么需要两个迭代器呢?
我们发现Itr
其实只有next()
和remove()
两个方法,这是适用于普通的大部分的子类的,而ListItr
则多了一个回溯上一个元素的hasPrevious
以及set()
和add()
方法,属于升级版本。
Itr
属于公共的迭代器,继承它的有ListIterator
和PrimitiveIterator
。
两个迭代器之间的区别:
使用iterator只能向前移动,但是使用ListIterator可以在读取元素时也移动后退字词。
使用ListIterator,您可以在遍历时随时获取索引,而迭代器则无法实现。
使用iterator,您只能检查下一个元素是否可用,但是在listiterator中,您可以检查上一个和下一个元素。
使用listiterator,您可以在遍历的任何时间添加新元素。 使用迭代器是不可能的。
使用listiterator,您可以在遍历时修改元素,而迭代器则不能。
查了一下Stack Overflow,答案链接:https://stackoverflow.com/questions/19799047/what-is-the-need-to-have-listiterator-and-iterator-in-the-list-interface-in
这个有可能是答案:
Since Java 5 it is simply possible to override a method with a more specific return type (called covariant return type). But ListIterator has been introduced with Java 1.2. To avoid casts on usage of iterator() there has to be a new method.
The API could not have been changed from Java 5 on because that would have broken all implementations of List which do not declare iterator() returning ListIterator also if most implementations return a ListIterator instance in real.
A similar dilemma is Enumeration and Iterator. Nowadays Iterator would extend Enumeration and simply add the remove() method. Or better Iterator would have replaced Enumeration and a ModifiableIterator with an additional remove() would have been added.
中文就是:
因为Java 5可以用更特定的返回类型(称为协变返回类型)重写方法。但是ListIterator是在Java 1.2中引入的。为了避免对iterator()的使用进行强制类型转换,必须有一个新方法。
API不可能从Java 5开始改变,因为这将破坏所有没有声明iterator()返回ListIterator的List实现,如果大多数实现在实际中返回ListIterator实例。
类似的困境是枚举和迭代器。现在迭代器将扩展枚举并简单地添加remove()方法。或者更好的迭代器会替换枚举,并且会添加一个额外的remove()来添加一个ModifiableIterator。
个人观点:这是迭代版本的过程中,基于兼容性和可拓展性来做的。也有人说是基于单向链表和双向链表来考虑的,貌似也有一点道理。
3.4.4 小结一下
AbstractList
是实现List
接口的抽象类,AbstractList
抽象类与List
接口的关系有点像AbstractCollection
抽象类与Collection
接口的关系。
拥有一部分抽象方法和一部分实现的方法,属于List
和具体容器实现类比如ArrayList
中间的一层。
4.总结
List
接口,主要是实现了列表的接口标准,常用的三个子类是:
- ArrayList
- 底层是数组,扩容就是申请新的数组空间,复制
- 线程不安全
- 默认初始化容量是10,扩容是变成之前的1.5倍
- 查询比较快
- LinkedList
- 底层是双向链表,可以往前或者往后遍历
- 没有扩容的说法,可以当成双向队列使用
- 增删比较快
- 查找做了优化,index如果在前面一半,从前面开始遍历,index在后面一半,从后往前遍历。
- Vector
- 底层是数组,几乎所有方法都加了Synchronize
- 线程安全
- 有个扩容增长系数,如果不设置,默认是增加原来长度的一倍,设置则增长的大小为增长系数的大小。
AbstractList
,是List
拓展出来的抽象类,定义了一部分通用的方法,弥补了List
是接口,不能对方法有所实现的不足,相当于加了一个中间层。里面定义了两个迭代器Iterator类,也提供了获取它们的方法,可以供不同的子类使用。
此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~
技术之路不在一时,山高水长,纵使缓慢,驰而不息。
公众号:秦怀杂货店
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库