【java-集合框架】ArrayList类

📢java基础语法,集合框架是什么?顺序表的底层模拟实现都是看本篇前的基础必会内容,本篇不再赘述,详情见评论区文章。
📢编程环境:idea

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.java.util包
在这里插入图片描述

当然不需要掌握上述所有的成员方法,本篇只重点讨论ArrayList类实现List接口后重写的常用成员方法。需要用到其他方法时,可自行查询源代码或者java文档学习后使用。

接下来就让我们开始打怪升级之旅吧~

3. 创建一个ArrayList对象

3.1 ArrayList以泛型方式实现

一步到位正确创建一个ArrayList对象之前,先读下列源码:

在这里插入图片描述

如上述源码所示,

  1. 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<>();
    }
}
  1. 不要省略E类型。
    在这里插入图片描述

  2. 另外,

    • ArrayList不是线程安全的。即ArrayList在单线程下可以使用,在多线程中一般使用CopyOnWriteArrayList或者Vector,CopyOnWriteArrayList和Vector和ArrayList相似,都是动态的顺序表。
    • 多线程编程的内容,本篇不详细说明。

3.2 ArrayList类中的成员变量

先读下列源码:
在这里插入图片描述自己已经模拟实现过顺序表结构的铁子其实很容易看明白,上述源码中的6个成员变量,

  1. Object类型的变量elementDate存一个地址,指向顺序表中的第一个数据所在的空间。
  2. 整型变量size是数组的有效长度
  3. 静态1常量DEFAULT_CAPACITY是数组的默认最大长度。
  4. 还有三个成员变量和成员方法有关。

3.3 ArrayList类中的构造方法

在这里插入图片描述

ArrayList类中有三种构造方法,分别是

  1. 构造的时候指定顺序表的容量 :ArrayList(int initialCapacity)
  2. 无参构造:ArrayList()
  3. 利用已实现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的类型,其中

  1. Collection< >规定c的类型必须是集合框架中实现了Collection接口的类,且这个类以泛型方式实现。在这里插入图片描述

  2. ? 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)的底层实现源码:
在这里插入图片描述
在这里插入图片描述
从源码中可以看出,调用这个构造方法创建一个顺序表,

  1. 如果指定容量initialCapacity大于0,给elementDate指向的空间分配一个大小为initialCapacity的内存。即相当于创建了一个大小为initialCapacity的顺序表。
  2. 如果指定容量initialCapacity等于0,让elementDate指向Object[]类型的变量EMPTY_ELEMENTDATA,EMPTY_ELEMENTDATA是没有分配内存的。即相当于创建了一个大小为0的顺序表。

3.34 ArrayList(Collection<? extends E> c)的实现逻辑

以下是构造方法ArrayList(Collection<? extends E> c)的实现源码:
在这里插入图片描述
从源码可以看出:

  1. 如果c的有效长度大于0,elmentDate会指向c的“复制体”。
  2. 如果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的扩容机制,解决以下两个问题:

  1. 当顺序表为空时,向顺序表中添加元素,如何扩容?扩大多少容量?
  2. 当顺序表不为空,向顺序表中添加元素,如何扩容?扩大多少容量?

当顺序表为空时:(以调用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语句。
在这里插入图片描述
从上面源码可知,向顺序表中添加元素:

  1. 当顺序表是通过调用无参构造方法创建的,顺序表空时,通常给数组扩容大小为10个单位的空间;ensureCapacityInternal()的参数minCapacity大于10时,扩容minCapacity个单位的空间。
  2. 当顺序表是通过调用非无参构造方法创建的,顺序表空时,给数组扩容大小为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()有哪些注意事项:

  1. subList(int fromIndex, int toIndex)的参数是截取范围,左闭右开;
  2. 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的优缺点及使用场景

顺序表的优点:

  1. 底层是连续的数组,通过下标进行随机访问元素很方便,时间复杂度是O(1)。

顺序表的缺点:

  1. 向顺序表中添加和删除一个元素,都要进行挪动很多元素,平均时间复杂度是O(n)。无法做到不移动就能向顺序表中添加或删除元素。
  2. 还有扩容的时候:无法做到随用随分配。主要体现在以下两点:
    • 向顺序表中新添加元素时,只能按照一定倍数扩容,会浪费空间。
    • 扩容最终是调用Arrays.copyOf()实现的。需要申请新空间,也就是先拷贝全部数据,然后把全部数据放到一个新申请的更大的空间中,再释放旧空间。这个过程会有不小的消耗。

顺序表的使用场景:
顺序表适合存储静态数据,即经常对数据进行查找和更新的操作。

附:idea中如何打开ArrayList源码?

打开idea,双击shift,在弹出的搜索框里按照下面搜索,打开ArrayList.java.util包,就能看到这个包底下所有的源码了。
在这里插入图片描述点击左下角的结构,就能看到ArrayList.java.util包中所有内部类,成员方法和成员变量了。
在这里插入图片描述

ArrayList.java.util包
其他类或接口的源码打开方式同上。

附:idae中如何在一个类中搜索对应的成员变量和成员方法

我们在查看源码的时候,常常会遇到这种情况,在某个方法里,又调用了另一种方法,或者使用了某个变量,此时我们想查看这个方法或变量具体是什么:
可以双击shift,然后再弹出的输入框中输入变量名或者方法名,通常搜索结果的第一条就是我们要找的内容,按回车,就可以跳转过去了。
或者同时按住ctrl+f,搜索该类中的关键字。

在这里插入图片描述

恭喜闯关成功。

通过本篇,相信你对集合框架中的ArrayList类已经有非常多的了解,同时,本篇浅浅提到了迭代器的用法。下一关是LinkedList类,邀请你继续来挑战!在这里插入图片描述


  1. DEFAULT_CAPACITY为什么是静态常量?static修饰成员变量,表明该变量是类的属性,无论这个类实例化多少个对象,所有对象都有这个属性。 ↩︎

  • 31
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值