📢java基础语法,集合框架是什么?顺序表的底层模拟实现都是看本篇前的基础必会内容,本篇不再赘述,详情见评论区文章。
📢编程环境:idea
【java-集合框架】ArrayList类
- 1. 先回忆一下java代码中常见的三个关键字
- 1.1 extends
- 1.2 abstract
- 1.3 interface
- 2. ArrayList简介
- 3. 创建一个ArrayList对象
- 3.1 ArrayList以泛型方式实现
- 3.2 ArrayList类中的成员变量
- 3.3 ArrayList类中的构造方法
- 3.31 ArrayList()和ArrayList(int initialCapacity)的使用
- 3.32 ArrayList(Collection<? extends E> c)的使用
- 3.33 ArrayList(Collection<? extends E> c)的效果
- 3.32 ArrayList()的实现逻辑
- 3.33 ArrayList(int initialCapacity)的实现逻辑
- 3.34 ArrayList(Collection<? extends E> c)的实现逻辑
- 3.4 ArrayList的扩容机制
- 4. ArrayList的常见操作
- 4.1 向顺序表中插入元素
- 4.2 删除顺序表中的元素
- 4.3获取下标 index 位置元素
- 4.4 将下标 index 位置元素设置为 element
- 4.5 清空顺序表
- 4.6 判断元素是否在顺序表中
- 4.7 返回元素所在下标
- 4.8截取部分顺序表
- 5. 遍历ArrayList
- 5.1 用for循坏遍历
- 5.2用foreach循坏遍历
- 5.3使用迭代器遍历
- 5.4直接通过sout输出顺序表中的元素
- 6. ArrayList的优缺点及使用场景
- 附:idea中如何打开ArrayList源码?
- 附:idae中如何在一个类中搜索对应的成员变量和成员方法
- 恭喜闯关成功。
1. 先回忆一下java代码中常见的三个关键字
1.1 extends
继承在代码中的关键字是extends。
子类继承父类,子类会继承父类的成员变量和成员方法。
子类继承父类以后,必须要添加自己持有的成员,否则就没有继承的必要。
1.2 abstract
被关键字abstract修饰的成员方法是抽象方法,当一个类中含有抽象方法的时候,这个类必须是抽象类。抽象类中抽象方法没有具体的实现。
当一个类被关键字abstract修饰,这个类就是抽象类。抽象类必须被继承。
当一个普通类继承了抽象类之后,在普通类中一定要重写抽象类中的所有抽象方法。
1.3 interface
在java代码中,使用interface定义一个接口。
接口中的方法默认被关键字abstract修饰,即接口中的方法默认都是抽象方法。
接口通过implements关键字被类实现,类实现接口后,要重写接口中的所有抽象方法。
2. ArrayList简介
用java语言写代码的时候,当我们要用到顺序表这样的结构存储数据时,我们不需要自己创建类去实现顺序表。java中提供了ArrayList类。
ArrayList类是java集合框架中一个普通的类。它的底层是一个动态类型的顺序表,即一个能动态扩容的数组。
所以我们只要理解ArrayList类中的成员变量,成员方法都是什么意思,代表哪些功能,在用到顺序表时,学会调用它们帮助我们解决问题就可以了。
ArrayList在集合框架中的具体框架图如图所示,从图中可以看出,它继承了一个AbstractList类,实现了四个接口,分别是List, RandomAccess, Cloneable,和Serializable。
其中,
- ArrayList实现了RandomAccess接口,表明ArrayList支持随机访问。
- ArrayList实现了Cloneable接口,表明ArrayList是可以clone的。
- ArrayList实现了Serializable接口,表明ArrayList是支持序列化的
- ArrayList实现了List接口,即对ArrayList进行增删改查操作。 上述前三个接口不是本篇的重点。本篇主要目的是掌握ArrayList实现List接口后重写的成员方法有哪些,怎么调用。
下面两种图片分别是ArrayList类中包含的所有内部类,成员变量和成员方法,和List类中包含的多有成员方法。c代表内部类,m代表成员方法,f代表成员变量。
当然不需要掌握上述所有的成员方法,本篇只重点讨论ArrayList类实现List接口后重写的常用成员方法。需要用到其他方法时,可自行查询源代码或者java文档学习后使用。
接下来就让我们开始打怪升级之旅吧~
3. 创建一个ArrayList对象
3.1 ArrayList以泛型方式实现
一步到位正确创建一个ArrayList对象之前,先读下列源码:
如上述源码所示,
- ArrayList是以泛型方式实现的,ArrayList中存的都是引用数据类型,使用时必须实例化。不要省略E类型。
例如:创建一个空的顺序表有以下两种方式:
import java.util.ArrayList;
import java.util.List;public class Test {public static void main(String[] args) {ArrayList<Integer> list = new ArrayList<>();//下面这种更常用,因为发生了向上转型List<Integer> list2 = new ArrayList<>();}
}
-
不要省略E类型。
-
另外,
- ArrayList不是线程安全的。即ArrayList在单线程下可以使用,在多线程中一般使用CopyOnWriteArrayList或者Vector,CopyOnWriteArrayList和Vector和ArrayList相似,都是动态的顺序表。
- 多线程编程的内容,本篇不详细说明。
3.2 ArrayList类中的成员变量
先读下列源码:
自己已经模拟实现过顺序表结构的铁子其实很容易看明白,上述源码中的6个成员变量,
- Object类型的变量elementDate存一个地址,指向顺序表中的第一个数据所在的空间。
- 整型变量size是数组的有效长度
- 静态1常量DEFAULT_CAPACITY是数组的默认最大长度。
- 还有三个成员变量和成员方法有关。
3.3 ArrayList类中的构造方法
ArrayList类中有三种构造方法,分别是
- 构造的时候指定顺序表的容量 :ArrayList(int initialCapacity)
- 无参构造:ArrayList()
- 利用已实现Collection接口的集合类构造:ArrayList(Collection<? extends E> c)
3.31 ArrayList()和ArrayList(int initialCapacity)的使用
无参构造和指定容量的构造,这两个构造方法的使用无需多说:
例如:构造一个空的顺序表list和构造一个有10个容量的顺序表list1
3.32 ArrayList(Collection<? extends E> c)的使用
在构造方法ArrayList(Collection<? extends E> c)
中,c
是参数,Collection<? extends E>
是c的类型,其中
-
Collection< >
规定c的类型必须是集合框架中实现了Collection接口的类,且这个类以泛型方式实现。 -
? extends E
是上界通配符,表示Collection的泛型参数必须是E或者E的子类。
3.33 ArrayList(Collection<? extends E> c)的效果
例如:利用list3变量构造一个顺序表list2。其中,list3的类型是一个实现了Collection接口的集合类TreeSet,且list3的类型的泛型参数和list2d的类型的泛型参数都是Integer。
3.32 ArrayList()的实现逻辑
以下是构造方法ArrayList()的底层实现源码:
从源码能看出调用无参构造方法创建一个顺序表,让elementDate指向Object[]类的变量DEFAULTCAPACITY_EMPTY_ELEMENTDATA,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是没有分配内存的。即相当于创建了一个大小为0的顺序表。
没有分配内存,那还能不能向顺序表中添加元素?
当然可以。ArrrayList是一个动态顺序表。此时,调用add()方法向顺序表中添加第一个元素时,顺序表的扩容机制会默认给顺序表分配大小为10的内存。
顺序表的扩容机制是ArrayList中的一个方法
ensureCapacityInternal(int minCapacity)
。add方法中会先
调用ensureCapacityInternal(int minCapacity)
给顺序表进行扩容,然后再把元素存到顺序表中。
3.33 ArrayList(int initialCapacity)的实现逻辑
以下是构造方法ArrayList(int initialCapacity)的底层实现源码:
从源码中可以看出,调用这个构造方法创建一个顺序表,
- 如果指定容量
initialCapacity
大于0,给elementDate指向的空间分配一个大小为initialCapacity的内存。即相当于创建了一个大小为initialCapacity的顺序表。- 如果指定容量
initialCapacity
等于0,让elementDate指向Object[]类型的变量EMPTY_ELEMENTDATA,EMPTY_ELEMENTDATA是没有分配内存的。即相当于创建了一个大小为0的顺序表。
3.34 ArrayList(Collection<? extends E> c)的实现逻辑
以下是构造方法ArrayList(Collection<? extends E> c)的实现源码:
从源码可以看出:
- 如果c的有效长度大于0,elmentDate会指向c的“复制体”。
- 如果c是空的,elementDate会指向Object[]类型的变量EMPTY_ELEMENTDATA,EMPTY_ELEMENTDATA是没有分配内存的。即相当于创建了一个大小为0的顺序表。
观察上面ArrayList的三种构造方法,要注意的是:直接调用无参构造ArrayList(),和,调用ArrayList(int initialCapacity)(initialCapacity
等于0)或ArrayList(Collection<? extends E> c)(c是空的),虽然这两种情况都相当于是创建一个大小为0的顺序表。但是,前者elementDate指向的是变量空间DEFAULTCAPACITY_EMPTY_ELEMENTDATA
;后者elementDate指向的是变量空间EMPTY_ELEMENTDATA
。
用不同的构造方法创建大小为0的顺序表,后续的扩容机制也是不同的。
3.4 ArrayList的扩容机制
以上是ArrayList类内部负责扩容功能的方法ensureCapacityInternal()
的源码。
这个方法用private封装,是因为该方法是为了完善ArrayList类的一些功能而存在的,是让ArrayList类内部相关方法调用的。
例如:调用add()方法向顺序表中添加元素前,先调用方法ensureCapacityInternal()
,判断顺序表是否需要扩容。
从上面源码可以看出
ensureCapacityInternal()方法
参数minCapacity
的范围:[1,?]。minCapacity
等于size+1。当顺表中有效元素个数size等于0时,minCapacity
等于1。例如:调用addAll(Collection<? extends E> c)方法向顺序表中添加元素时,先调用方法ensureCapacityInternal(),判断顺序表是否需要扩容。
从上面源码可以看出:
ensureCapacityInternal()方法
参数minCapacity
的范围:[numNew,?]。minCapacity
等于size+numNew。当顺表中有效元素个数size等于0时,minCapacity
等于numNew。
无论在哪种方法中调用
ensureCapacityInternal()方法
,此时ensureCapacityInternal()方法
的参数minCapacity
都是当前操作所需的最小容量。比如顺序表空时,调用add()方法,当前操作顺序表的最小容量就是1,即minCapacity等于1;顺序表为空时,调用addAll(Collection<? extends E> c)方法,当前操作顺序表达额最小容量就是numNew,即minCapacity等于numNew。
此时
ensureCapacityInternal()方法
内部只有一条语句,该语句由两个方法嵌套构成。这种情况下,通常都是先读括号内部方法。从内向外,逐层理解。
接下来我们会继续深入读ArrayList类内部负责扩容功能的方法ensureCapacityInternal()
的源码,详细理解掌握ArrayList的扩容机制,解决以下两个问题:
- 当顺序表为空时,向顺序表中添加元素,如何扩容?扩大多少容量?
- 当顺序表不为空,向顺序表中添加元素,如何扩容?扩大多少容量?
当顺序表为空时:(以调用add方法添加元素为例)
当顺序表为空时,方法ensureCapacityInternal()的参数
minCapacity
等于1。
由ArrayList的构造方法的实现逻辑可知,elementDate有两种可能性,分别是指向变量DEFAULTCAPACITY_EMPTY_ELEMENTDATA
或指向变量EMPTY_ELEMENTDATA
。
- 当elementDate指向
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
时,calculateCapacity()方法
返回参数DEFAULT_CAPACITY,即10。- 当elementDate指向
EMPTY_ELEMENTDATA
时,calculateCapacity()方法
返回参数minCapacity,即1。
也就是说,若该大小为0的顺序表是调用无参构造方法创建的,calculateCapacity()方法
返回10,若大小为0的顺序表是调用非无参构造方法创建的,calculateCapacity()方法
返回1。
ensureExplicitCapacity()方法
的参数minCapacity是1或者10。
elementData指向的空间是空的,elementData.length为0。
所以无论minCapacity是1还是10,程序都会进入grow方法。
modCount是ArrayList类从抽象类AbstractList中继承下来的成员变量。初始值为0。
grow()方法
的参数minCapacity是1或者10。当顺序表位空时,oldCapacity为0,newCapacity的初始值也是0。所以当顺序表为空时,程序总是进入第一个if语句。
从上面源码可知,向顺序表中添加元素:
- 当顺序表是通过调用无参构造方法创建的,顺序表空时,通常给数组扩容大小为10个单位的空间;
ensureCapacityInternal()
的参数minCapacity大于10时,扩容minCapacity个单位的空间。- 当顺序表是通过调用非无参构造方法创建的,顺序表空时,给数组扩容大小为minCapacity个单位的空间(在add方法中调用扩容方法,minCapacity为1)。
当顺序表不为空时:
当顺序表不为空时,
方法ensureCapacityInternal()
的参数minCapacity在(1,?)之间。但其实只要参数minCapacity是大于1且合法的,扩容代码都会走到方法grow()这一步。
如方法grow()的源码所见,此时参数minCapacity的值具体是多少都无所谓了。
当顺序表不为空时,无论该顺序表是调用无参构造方法创建的,还是非无参构造方法创建的,向顺序表中添加元素时,都会先调用ensureCapacityInternal()方法,按照原数组的1.5倍扩容,然后再把新增元素加入到数组中。
最后:解释一下ensureCapacityInternal()方法参数minCapacity的范围:[1,?]
中,?
是什么意思:扩容是有限制的,这个?
就是这个最大限制。当顺序表大小非常接近扩容极限时,按正常1.5倍扩容后很可能会出现问题。在进行真正扩容操作之前,对扩容的大小进行检查,防止太大导致扩容失败。
4. ArrayList的常见操作
方法 | 解释 |
---|---|
boolean add(E e) | 尾插 e |
void add(int index, E element) | 将 e 插入到 index 位置 |
boolean addAll(Collection<? extends E> c) | 尾插 c 中的元素 |
E remove(int index) | 删除 index 位置元素 |
boolean remove(Object o) | 删除遇到的第一个 o |
E get(int index) | 获取下标 index 位置元素 |
E set(int index, E element) | 将下标 index 位置元素设置为 element |
void clear() | 清空 |
boolean contains(Object o) | 判断 o 是否在线性表中 |
int indexOf(Object o) | 返回第一个 o 所在下标 |
int lastIndexOf(Object o) | 返回最后一个 o 的下标 |
List subList(int fromIndex, int toIndex) | 截取部分 list |
4.1 向顺序表中插入元素
向顺序表中插入元素,代码的背后逻辑都是:先扩容,然后把元素插入到适当的位置,最后更新size的值。平均时间复杂度为O(n)。
boolean add(E e)和void add(int index, E element) 的源码如下:
>boolean addAll(Collection<? extends E> c)的源码如下:
调用ArrayList类中的add()方法和addAll()方法,向顺序表中添加元素。
4.2 删除顺序表中的元素
ArrayList类中包括以下两种基本删除操作:一种是删除指定位置的元素;一种是删除某个指定元素。
代码的背后逻辑都是:先找要删除的元素,然后进行删除操作,最后更新elementDate[–size]位置的值为null。
平均时间复杂度为O(n)。
E remove(int index)的源码如下:
boolean remove(Object o)的源码如下:
调用ArrayList类中的remove(int index)方法和remove(Object o)方法,删除顺序表中的元素。调用这两个方法的时候要注意:它们的参数分别int类型和Object类型的。
4.3获取下标 index 位置元素
已知index下标,可以直接获取数组中index下标的元素。时间复杂度为O(1)。
get(int index)的源码如下:
调用get(int index)方法。要注意的是:get(int index)方法的返回值类型是E。
4.4 将下标 index 位置元素设置为 element
已知index下标,通过赋值,可以直接把index位置的元素更新为element。时间复杂度:O(1)。
set(int index, E element)的源码如下:
调用set(int index, E element)方法:
4.5 清空顺序表
ArrayList对象中存储的都是引用数据类型的数据,所以清空顺序表要把顺序表中的所有有效元素都设置为null。
clear()的源码如下:
调用clear()方法:
4.6 判断元素是否在顺序表中
先遍历顺序表中的有效元素,再把数组中的元素和要查找的元素依次做比较,找到元素返回true,没找到返回false。时间复杂度是O(n)。
contains(Object o)的源码如下:
调用contains(Object o)方法:
4.7 返回元素所在下标
ArrayList类中提供了两个方法,分别是indexOf(Object o)和lastIndexOf(Object o)。indexOf(Object o)方法是从头到尾遍历顺序表,找到元素就停止遍历返回其下标;lastIndexOf(Object o)是倒着遍历顺序表,找到元素就停止遍历返回其下标。
indexOf(Object o)的源码如下:
lastIndexOf(Object o)的源码如下:
调用indexOf(Object o)和lastIndexOf(Object o)方法:
4.8截取部分顺序表
List subList(int fromIndex, int toIndex)的源码如下:
List subList(int fromIndex, int toIndex)方法内部的具体实现,这里不做更多探究。
接下来详细说说调用subList()有哪些注意事项:
- subList(int fromIndex, int toIndex)的参数是截取范围,左闭右开;
- subList(int fromIndex, int toIndex)方法返回的是该列表中介于指定的fromIndex(包含)和toIndex(不包含)之间的部分的视图。视图的类型是List< E> 。
通俗点说就是:subList()方法的返回值是一个地址,指向该列表的fromIndex位置。这意味着当我们仅需要操作列表的一部分时,可以不用传递整个列表进行操作,通过调用subList()方法传递子列表视图操作即可。
5. 遍历ArrayList
5.1 用for循坏遍历
5.2用foreach循坏遍历
5.3使用迭代器遍历
使用ListIterator< E>迭代器还能倒着遍历:关于集合框架中的两种迭代器详细理解,见评论区文章。
5.4直接通过sout输出顺序表中的元素
ArrayList类对象是引用数据类型,一般来说,sout打印出来应该是一个地址。但是,sout打印一个ArrayList对象,打印出来的地址所指的内容。此时,ArrayList类中一定有重写的toString()方法。重写的toString()方法在AbstractCollection类中。ArrayList类继承了AbstractList类,AbstractList类继承了AbstractCollection类。
6. ArrayList的优缺点及使用场景
顺序表的优点:
- 底层是连续的数组,通过下标进行随机访问元素很方便,时间复杂度是O(1)。
顺序表的缺点:
- 向顺序表中添加和删除一个元素,都要进行挪动很多元素,平均时间复杂度是O(n)。无法做到不移动就能向顺序表中添加或删除元素。
- 还有扩容的时候:无法做到随用随分配。主要体现在以下两点:
- 向顺序表中新添加元素时,只能按照一定倍数扩容,会浪费空间。
- 扩容最终是调用
Arrays.copyOf()
实现的。需要申请新空间,也就是先拷贝全部数据,然后把全部数据放到一个新申请的更大的空间中,再释放旧空间。这个过程会有不小的消耗。
顺序表的使用场景:
顺序表适合存储静态数据,即经常对数据进行查找和更新的操作。
附:idea中如何打开ArrayList源码?
打开idea,双击shift,在弹出的搜索框里按照下面搜索,打开ArrayList.java.util包
,就能看到这个包底下所有的源码了。
点击左下角的结构
,就能看到ArrayList.java.util包中所有内部类,成员方法和成员变量了。
其他类或接口的源码打开方式同上。
附:idae中如何在一个类中搜索对应的成员变量和成员方法
我们在查看源码的时候,常常会遇到这种情况,在某个方法里,又调用了另一种方法,或者使用了某个变量,此时我们想查看这个方法或变量具体是什么:
可以双击shift,然后再弹出的输入框中输入变量名或者方法名,通常搜索结果的第一条就是我们要找的内容,按回车,就可以跳转过去了。
或者同时按住ctrl+f
,搜索该类中的关键字。
恭喜闯关成功。
通过本篇,相信你对集合框架中的ArrayList类已经有非常多的了解,同时,本篇浅浅提到了迭代器的用法。下一关是LinkedList类,邀请你继续来挑战!
DEFAULT_CAPACITY为什么是静态常量?static修饰成员变量,表明该变量是类的属性,无论这个类实例化多少个对象,所有对象都有这个属性。 ↩︎