一、集合的用途
在java中,为了方便操作多个对象,我们需要使用到集合(容器);
二、集合的基本框架体系
三、List集合
ArrayList
1、基本特性
- ArrayList是基于动态数组实现的,在增删时候,需要数组的拷贝复制;
- ArrayList的默认初始化容量是10,每次扩容时候增加原先容量的一半,也就是变为原来的1.5倍;
- 插入元素时,可插入到最前面、中间任一位置、最后面,但不能隔空插入元素(例如ArrayList中已有两个元素,分别在index为0和1的位置,此时可以插入到index为0,1,2的位置,但不可以插入到index为3及3以上的位置(IndexOutOfBoundsException));
- 删除元素时不会减少容量,若希望减少容量则调用trimToSize();
- 随机查找速度快,插入删除速度慢(插入删除均需移动元素);
- 非线程安全(需要同步时可使用Vector类)
- 能存放null值;
2、基本属性
查看源码可知:在未指定具体初始化容量值时,容器的初始容量则为10,。我们注意到源码中有两个空数组对象EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA,它俩有什么区别和作用呢?继续看源码:
再结合了整体源码,我们可以发现只有无参构造用了DEFAULTCAPACITY_EMPTY_ELEMENTDATA常量,另外都是用了EMPTY_ELEMENTDATA。也就是说,虽然都是空数组对象,但是EMPTY_ELEMENTDATA是使用带初始化值的构造方法(初始值为0)时使用的,而DEFAULTCAPACITY_EMPTY_ELEMENTDATA是使用默认的构造方法,也即是无参的构造方法时使用的,从意义上做了区分。另外,EMPTY_ELEMENTDATA的存在,
在创建空数组的时候,可以减少不必要的空数组的创建,因为它们都指向了EMPTY_ELEMENTDATA。
3、常见方法及原理分析
a、add(E element)
该方法用于在集合中添加元素,添加的位置是在末尾。
首先我们来看一下方法的具体代码:
可以看到,添加元素之前,会先去检查一下数组容量是否满足要求,我们来看一下ensureCapacityInternal()的方法,如下:
这里其他两个相关的函数也一并贴上来了,整体的流程就是,根据ensureCapacityInternal()方法中传入的minCapacity,来和现有数组的容量做对比,判断此插入操作是否需要进行数组扩容。另外,在calculateCapacity()方法中可以发现:如果是无参构造函数生成的ArrayList对象,在第一次添加元素的时候,数组都需要扩容,且初始容量是10。接着,我们来看一下数组的扩容操作grow()方法:
在代码中我们可以看到,扩容的时候,首先是扩展到原有数组容量的1.5倍,如果还是不够(小于minCapacity),就直接扩展到目标值(minCapacity),接着再跟MAX_ARRAY_SIZE做比较,如果比MAX_ARRAY_SIZE还大,就直接扩容到Integer.MAX_VALUE,最后,进行数据拷贝(底层调用的是System.arraycopy())。
小结:先检查容量是否满足需求:满足,直接添加;不满足,扩容,然后添加。
b、add(int index, E element)
该方法会将元素添加到指定的位置。
同样,我们先来看一下源码:
可以看到,指定位置插入元素时,会先去检查是否越界插入,rangeCheckForAdd()代码如下:
(注意该处范围检测与get和set有些不同)
接着,会去检查容量是否满足插入元素的操作,然后调用底层的System.arraycopy()对数组的内容进行迁移(也就是复制拷贝),最后,将元素插入到数组中。
c、get(int index)
该方法用于返回指定位置上的元素。
源码如下,比较简单:
两个步骤:先检查下标是否越界,如没有越界则返回对应位置元素的值。
检测越界的代码如下:
可以看到,rangeCheck()与rangeCheckForAdd()是不同的。
d、set(int index, E element)
该方法用于设置对应位置的元素值
源码如下:
同样,首先会检查下标是否越界,如果不越界,则用新传入的元素覆盖掉原来的元素,并把原来的元素返回。
e、remove(int index)
该方法用于移除指定位置上的元素。
源码如下:
同样,先判断下标是否越界,如果不越界,则判断该移除操作是否需要移动元素(移除的是最后一个元素则不需要移动其他元素),需要则移动(复制拷贝),最后,将移除的元素返回。
注意到 elementData[–size] = null,将无效元素置空,能更及时GC。
LinkedList
1、基本特性
- 一种可以在任何位置进行高效地插入和删除操作的有序序列;
- 随机查找速度慢,插入删除速度快;
- 底层实现是链表(双向链表);
2、基本属性
可以看到,特有的变量不是很多,其中比较重要的就是first和last,用来指向链表的头部和尾部。其中,节点Node的结构如下:
3、基本方法
a、add(E e)
该方法用于将元素添加到链表的末端。
源码如下:
我们来看一下linkLast()方法的内部是怎样实现的,
大概流程就是,先备份原有末端节点,再根据传递进来的元素生成新的节点,并让last变量指向它。再判断是否是第一个添加的节点,如果是,则first变量也指向它,如果不是,则把原先末端节点的next变量指向它。
b、add(int index, E element)
该方法用于将元素添加到链表中的指定位置。
源码如下:
流程:先检查下标是否越界,然后判断是否为尾部插入,如果是,则效果类似add(E e),如果不是则调用linkBefore()方法(这里注意一下,node(index)是返回对应下标的节点),我们看一下linkBefore()的内部是怎样实现的:
流程:备份index原先对应位置的节点的前节点,再根据传递进来的元素生成新节点,并把index原先对应位置的节点的prev指向新生成的节点,接下来判断新节点插入的位置是不是链表的头部,如果是,将first变量指向新节点,如果不是,则将index原先对应位置节点的前节点的next指针指向新节点(感觉还是看代码容易懂,这样太绕)。
c、remove(int index)
该方法用于在链表中移除指定位置的元素。
源码如下:
流程:同样,先检查下标是否超过范围(这里注意到检查的范围跟add(int index, E element)不一样,下面get和set也是一样),如果不超过范围,则根据该下标对应节点找到它的前驱节点和后继节点,然后将前驱节点的next指向后继节点,后继节点的prev指向前驱节点,细节操作请unlink()的代码。
d、get(int index)
该方法用于返回链表中指定位置的元素。
源码如下:
流程:检查下标是否合法,如果合法,则返回节点中元素的值。
e、set(int index, E element)
该方法用于更新链表中指定位置的元素。
源码如下:
流程:同样,先检查下标是否合法,如果合法,则将新元素覆盖掉该节点的旧元素,并旧元素返回。
Vector
1、基本特性
- Vector底层也是数组,与ArrayList最大的区别就是:同步(线程安全);
- ArrayList在底层数组不够用时在原来的基础上扩展0.5倍,Vector是扩展1倍;
- Vector的默认初始化容量是10;
- 支持null值;
2、基本属性
可以看到,属性并不是很多,值得注意的是增量capacityIncrement,由图中解释可知,当capacityIncrement小于或等于0时,扩容时是增加1倍的容量。
3、基本方法
Vector这个类中的方法基本与ArrayList类似,大家可以参照ArrayList看一下源码,这里就不赘述了。
ArrayList与Vector的异同小结
相同点
- 两者底层都是使用数组来实现的;
- 基本的方法操作内部实现都差不多;
不同点
- ArrayList是非线程安全的,Vector是线程安全的;
- ArrayList扩容时是增加0.5倍,Vector扩容时是增加1倍;
Tip
如果ArrayList要实现同步,可以使用Collections的方法:List list = Collections.synchronizedList(new ArrayList(…)),该方法返回一个线程安全的List,可以实现同步。
ArrayList与LinkedList对比
相同点
- 具有逻辑相同的基本操作,例如add、get、set等等;
- 都是非线程安全的集合;
不同点
- ArrayList底层实现是数组,LinkedList的底层实现是双向链表;
- ArrayList随机查找速度快,插入删除速度慢;LinkedList随机查找速度慢,插入删除速度快;
使用场景
ArrayList更适合不经常改变集合内容,但是经常查找的场景;
LinkedList更适合经常改变集合内容,但是查找较少的场景;