ArrayList容器的本质是一个Object[]类型的数组。
即ArrayList容器存储的元素,本质都是存储在Object[]类型数组中。
ArrayList中定义了实例变量 Object[] elementData 来作为底层数组。
构造器
===
public ArrayList()
/**
-
Shared empty array instance used for default sized empty instances. We
-
distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
-
first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
- Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
ArrayList无参构造器创建对象时,elementData固定赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
其中DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空的Object[]的数组。
所以使用new ArrayList()得到的是一个空的Object[]的数组,且该空数组是一个特定对象DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
public ArrayList(int initialCapacity)
/**
- Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
-
Constructs an empty list with the specified initial capacity.
-
@param initialCapacity the initial capacity of the list
-
@throws IllegalArgumentException if the specified initial capacity
-
is negative
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
ArrayList带一个int形参的构造器创建对象时,可以由外部指定一个初始容量initialCapacity。
构造器内部根据initialCapacity来创建一个对应长度的数组。
当initCapacity>0时,构造器内部正常创建一个new Obejct[initialCapacity]对象给elementData。
当initCapacity=0时,构造器就将固定的EMPTY_ELEMENTDATA空Object[]数组赋值给elementData。
当initCapacity<0时,构造器就抛出异常,因为数组的长度不能是负数。
其中EMPTY_ELEMENTDATA也是一个空的Object[]数组。那么此处为什么不将DEFAULTCAPACITY_EMPTY_ELEMENTDATA给elementData呢?
而要使用本质上重复的EMPTY_ELEMENTDATAn呢?
虽然DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA都是空Object[]数组,
但是它们的内存地址值不同,是两个不同的对象。
而后面针对不同构造器初始化的elementData扩容时,会有不同的扩容方案。
比如使用ArrayList()创建对象时,使用该无参构造器的人不确定要初始化的容器容量,
可能很大,也可能很小,所以需要系统的扩容方案偏向折中。
比如使用ArrayList(int initialCapacity)创建对象时,
使用该构造器的人应该已经确切知道容器容量的大小了,
所以就算后期数据超了事先预定的容器容量,也应该不会超过太多,
所以需要系统的扩容方案偏向保守。
但是不论是无参构造器,还是指定初始容器容量的构造器,都是有可能定义一个容量为0的容器的。
所以为了后期扩容时,系统知道选择的是什么构造器,就给
无参构造器专门定义了一个地址值固定的DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空Object[]数组。
其它构造器定义了EMPTY_ELEMENTDATA的空Object[]数组。
public ArrayList(Collection<? extends E> c)
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
ArrayList带一个Collection实现类对象c的形参的构造器,该构造器创建对象,本质是将c中的元素转存到ArrayList容器中。
首先需要判断c是否是空集合,如果是空集合,则直接创建一个空的ArrayList容器返回即可。
这里底层elementData赋值为EMPTY_ELEMENTDATA。
为什么这里不选择使用DEFAULTCAPCITY_EMPTY_ELEMENTDATA呢?
我们需要知道DEFAULTCAPCITY_EMPTY_ELEMENTDATA是给容器容量不确定的情况使用的。
而当前构造器是将一个已经存在的集合转成ArrayList集合,
那么可以理解为要被转的集合是一个已知元素个数的数据源,
那么我们要创建的ArrayList容器的容量就是已知的。所以选择了EMPTY_ELEMENTDATA。
如果不是空集合,则说明c中有元素。那么就将c中的元素取出来存到新的ArrayList集合中就行了。
以上是大致实现思路。但是具体实现如下:
首先c.toArray(),将c转成的数组向上转型赋值给了Object[]数组a。
然后通过判断a.length是否为0,来判断c集合中是否由元素,
如果a.length == 0,则c中没有元素,那么就将elementData = EMPTY_ELEMENTDATA.
如果a.length != 0,则c中有元素,那么就继续判断c的运行时类型是不是ArrayList,
如果是,那么c.toArray()得到的返回值就是Object[]数组,
那么就不需要新创建一个数组给elementData了。
如果不是,那么c.toArray()得到的返回值就可能不是Object[]数组,
那么就需要创建一个新的Object[]数组来转移c.toArray()数组中的元素后,赋值给elementData。
这样新创建的ArrayList对象的底层数组elementData的初始化就完成了。
特性证明
通过ArrayList构造器的实现,我们可以知道ArrayList的底层数据结构是数组,且是一个Object[]类型的数组。则可以证明特性
集合元素类型可以不一致。证明:任何引用类型的元素都可以存入Object[]数组。
集合元素类型只能是引用类型。证明:存入Obejct[]数组的元素类型只能是引用类型。
集合元素只能是单列数据。证明:数组只能存单列数据。
ArrayList元素有索引。证明:数组存储的元素都有索引。
插入元素
====
public boolean add(E e)
/**
-
Appends the specified element to the end of this list.
-
@param e element to be appended to this list
-
@return true (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!! //ensureCapacityInternal用于对elementData数组进行扩容,传入的参数是扩容的最小容量,即size+1,即比当前elementData容量多一个元素的容量
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));//calculateCapacity用于计算最适合elementData的扩容容量
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {// 当elementData==DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,说明elementData刚被new ArrayList()创建,而且当前是第一次add操作
return Math.max(DEFAULT_CAPACITY, minCapacity);//由于此时minCapacity是size+1,即为1,而DEFAULT_CAPACITY是10,所以calculateCapacity方法返回10
}
return minCapacity;// 如果elementData!=DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则说明1.elementData是被new ArrayList()创建的,但当前不是第一次add操作 2.elementData不是被new ArrayLsit()创建的。 此时minCapacity就是size+1
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // modCount提示迭代器是否在迭代过程中有元素被增加或删除,若有,则快速失败
// overflow-conscious code
if (minCapacity - elementData.length > 0)// ,minCapacity可能是size+1或者10,如果minCapacity - elementData.length<=0,则elementData数组无需扩容,否则需要扩容
grow(minCapacity);
}
/**
-
Increases the capacity to ensure that it can hold at least the
-
number of elements specified by the minimum capacity argument.
-
@param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;//将当前elementData数组长度定义为oldCapacity
int newCapacity = oldCapacity + (oldCapacity >> 1);//定义扩容容量为newCapacity,且为oldCapacity的1.5倍
if (newCapacity - minCapacity < 0)// 如果最小扩容容量 比 elementData数组长度的1.5倍还大,则将最小扩容容量作为最终扩容容量,否则就将老容量1.5倍,作为最终扩容容量
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)// 检查最终扩容容量是否比(int最大值-8)大
newCapacity = hugeCapacity(minCapacity);// 如果是的话,则提供一个默认扩容容量
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);//最终将老的elementData数组中的数据复制到一个新的,容量为newCapacity的数组中,并最终赋值给elementData
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
@SuppressWarnings(“unchecked”)
public static T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings(“unchecked”)
T[] copy = ((Object)newType == (Object)Object[].class)
-
? (T[]) new Object[newLength]
- (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
add(E e)方法是在ArrayList尾部插入一个元素e。
该方法有五部分逻辑
1.计算最小扩容容量
2.判断是否需要扩容
3.计算实际扩容容量
4.进行扩容
5.扩容后add操作
1.计算最小扩容容量
默认最小扩容容量就是当前elementData容量+1
但是如果是new ArrayList()后,首次add操作,则最小扩容容量为10
2.判断是否需要扩容
如果
最小扩容容量 比 当前elementData的容量 小,则说明elementData容量是足够当前操作,即无需扩容。
如果
最小扩容容量 比 当前elementData的容量 大,则说明elementData容量不足,无法支持当前操作,需要立即扩容。
3.计算实际扩容容量(前提:第2步判断需要扩容)
默认实际扩容容量 是 当前elementData容量的1.5倍。
若 最小扩容容量 比 默认实际扩容容量 大,则说明默实际认扩容容量不足,则默认实际扩容容量需要改为最小容量的值。
若 最小扩容容量 比 默认实际扩容容量 小,则说明默认实际扩容容量足够。
继续比较默认实际扩容容量和MAX_ARRAY_SIZE的大小:
如果默认实际扩容容量比MAX_ARRAY_SIZE还要大,
则说明默认实际扩容容量已经超出了常规数组的最大容量,
则再用最小扩容容量和MAX_ARRAY_SIZE比,
若连最小扩容容量也比MAX_ARRAY_SIZE大,
则实际扩容容量为Integer.MAX_VALUE,否则为MAX_ARRAY_SIZE。
如果默认实际扩容容量比MAX_ARRAY_SIZE小,则实际扩容容量为默认实际扩容容量。
4.进行扩容
Arrays.copyOf(elementData,实际扩容容量)
System.arrayCopy(elementData,0,new Object[实际扩容容量],0,elementData.length);
即将老数组elementData,从第0位开始,到第elementData.length-1位的元素 全部 复制到 新数组的第0位,到到第elementData.length-1位
5.扩容后add操作
elementData[size++] = e;
扩容前,elementData数组的最大索引是size-1,元素个数是size
扩容后,elementData数组的最大索引至少是size,元素个数是size++
所以扩容后add操作是elementData[size++] = e;
public void add(int index, E element)
/**
-
Inserts the specified element at the specified position in this
-
list. Shifts the element currently at that position (if any) and
-
any subsequent elements to the right (adds one to their indices).
-
@param index index at which the specified element is to be inserted
-
@param element element to be inserted
-
@throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // 对数组进行扩容,和add(E e)扩容逻辑一致,注意该步该返回的数组是elementData = Arrays.copyOf(elementData,扩容后长度),即elementData扩容了,且前xx位元素还是老的数组的元素
System.arraycopy(elementData, index, elementData, index + 1,
size - index);// 该步将elementData的index(插入索引位置)的元素,以及它后面的元素,全部往后挪了一位存储。这样就空出了index位置了
elementData[index] = element;// 完成对index位置元素的插入
size++;//完成元素个数的更新
}
/**
- A version of rangeCheck used by add and addAll.
*/
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)//由于底层数组的索引是[0,index-1],所以可以插入0(头部),index(尾部),或(0,index)(中间)之间的位置,不能插入其他的位置
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
add(int index,E element)该方法是在ArrayList容器的index位置插入一个元素。
其中index位置可以是当前容器的头部(0),尾部(size),或者中间(0~size)任意位置插入。
该方法有六步
1.index检查
2.计算最小扩容容器
2.判断是否需要扩容
3.计算实际扩容容量
4.进行扩容
5.空出index位置
6.插入元素到index位置
其中2,3,4步和add(E e)方法一致。
1.index检查
插入元素的位置不能是头部之前的位置,也不能是尾部之后的位置
所以 index >= 0 && index <= size
2,3,4.进行扩容
举个例子:
扩容前,elementData.length,即容器容量为3,存储的元素为{1,2,3},现在要在索引1处插入1.5元素。
由于此时minCapacity=4 , minCapacity>elementData.length,所以需要进行扩容
默认扩容大小为3+3*0.5 = 4,所以就扩容到4,并将老数组中元素按照索引位置转移到新数组中
elementData = Arrays.copyOf(elementData,4);
即扩容后的新数组为:
elementData = {1,2,3,null}
5.空出index位置
System.arraycopy(elementData,index,elementData,index+1,size-index)
此时 size还没有改变,还是老数组的元素个数,即3。
index是要插入的索引位置,即为1。
System.arraycopy(elementData,1,elementData,2,2);
该步操作后,elementData的数组变为
{1,2,2,3}
6.插入元素到index位置
elementData[index] = element;后即完成插入操作
elementData = {1,1.5,2,3}
public boolean addAll(Collection<? extends E> c)
/**
-
Appends all of the elements in the specified collection to the end of
-
this list, in the order that they are returned by the
-
specified collection’s Iterator. The behavior of this operation is
-
undefined if the specified collection is modified while the operation
-
is in progress. (This implies that the behavior of this call is
-
undefined if the specified collection is this list, and this
-
list is nonempty.)
-
@param c collection containing elements to be added to this list
-
@return true if this list changed as a result of the call
-
@throws NullPointerException if the specified collection is null
*/
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();//将c转成数组
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount// 最小扩容容量=当前数组容量+c集合元素个数
System.arraycopy(a, 0, elementData, size, numNew);// 将c中元素复制到新数组的尾部
size += numNew;//ArrayList集合元素个数变为size+numNew
return numNew != 0;
}
addAll(Collection c)
该方法是ArrayList重写了List接口的抽象方法addAll(Collection c)。
该方法含义是将c集合中的元素按顺序插入到调用者ArrayList集合的尾部。
具体实现逻辑和add(E e)大致相同。只是add(E e)是只添加一个元素,而add(Collection c)是添加多个元素。
public boolean addAll(int index, Collection<? extends E> c)
/**
-
Inserts all of the elements in the specified collection into this
-
list, starting at the specified position. Shifts the element
-
currently at that position (if any) and any subsequent elements to
-
the right (increases their indices). The new elements will appear
-
in the list in the order that they are returned by the
-
specified collection’s iterator.
-
@param index index at which to insert the first element from the
-
specified collection
-
@param c collection containing elements to be added to this list
-
@return true if this list changed as a result of the call
-
@throws IndexOutOfBoundsException {@inheritDoc}
-
@throws NullPointerException if the specified collection is null
*/
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount // 最小扩容容量 = 当前数组容量 + c集合中的元素个数
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew; // 插入后,ArrayList集合元素的个数变为size+numNew
return numNew != 0;
}
addAll(int index, Collection c)
该方法是ArrayList重载的方法。
该方法含义是在当前ArrayList集合的index索引处,插入c集合中的元素。
该方法实现方案和add(int index,E e)大致一致。
特性证明
add(E e)
add(int index,E e)
两个方法的实现证明了,ArrayList的集合元素是基于索引操作的,所以
1.ArrayList集合元素是有索引的。
且这两个方法实现类动态扩容,即证明
2.ArrayList集合的长度是可变的
另外,在add元素时没有特别检查元素本身是否重复
3.ArrayList集合的元素是可重复的
其次,add(E e)操作插入的元素,是将元素插入到当前底层数组的尾部
add(int index,E e)是将元素插入到底层数组指定索引处。
且元素取出的顺序就是插入的顺序,所以,证明:
4.ArrayList集合的元素是有序的
删除元素
====
public E remove(int index)
/**
-
Removes the element at the specified position in this list.
-
Shifts any subsequent elements to the left (subtracts one from their
-
indices).
-
@param index the index of the element to be removed
-
@return the element that was removed from the list
-
@throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
rangeCheck(index);//检查要删除的索引是否合法,要删除的索引必须是elementData已有元素的索引
modCount++;
E oldValue = elementData(index);// 获取要删除索引上的元素,该方法执行完后需要返回被删除的元素
int numMoved = size - index - 1;// (假设已经删除了索引上的元素,那么被删除索引后面的元素,需要自动往前挪动一位),这里numMoved是指有几个元素需要挪动。理解:要删除index位置的元素,那么index+1到size-1位置上元素,要往前挪,则一共有size-1-index个元素需要挪动位置
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);//进行挪位,即将elementDat中从index+1位置开始的numMoved个元素,复制到elementData中的index位置开始的numMoved个元素的位置上
elementData[–size] = null; // clear to let GC do its work//由于元素都往前挪动了一位,所以理论上数组的最后一个元素应该是空的,所以这里将elementData[size-1]的元素设置为了null。另外size是指集合的元素个数,而由于已经删除了一个元素,所以–size
return oldValue;// 返回删除操作前,保留的被删除的元素,告诉外部删除了哪个元素
}
/**
-
Checks if the given index is in range. If not, throws an appropriate
-
runtime exception. This method does not check if the index is
-
negative: It is always used immediately prior to an array access,
-
which throws an ArrayIndexOutOfBoundsException if index is negative.
*/
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
@SuppressWarnings(“unchecked”)
E elementData(int index) {
return (E) elementData[index];
}
remove(int index)
删除集合中指定索引处元素。即删除底层数组中指定索引处的元素。
具体实现:
1.检查需要删除元素的index是否合法
2.计算挪位数
3.进行挪位
4.挪位后,将尾部多余元素设置为null
5.更新size,即更新集合元素个数
关于挪位实现:
举个例子:当前有数组 Objetc[] arr = {1,2,3,4,5,6};
现在需要把索引1处的元素删除,且索引2处的元素自动填补到被删除的索引1处,索引3自动填补到索引2,…
即:删除索引1位置元素后:
{1,null,3,4,5,6}
挪位后
{1,3,4,5,6,null}
理论上应该这样实现。但是这样实现不符合最简代码逻辑。
关于挪位实现:
真实实现逻辑:
当前有数组 Objetc[] arr = {1,2,3,4,5,6};
现在想把arr中索引1处的元素删除。其实代码实现删除的方式有很多中,覆盖了该索引处元素也算是删除。
System.arraycopy(arr,2,arr,1,4)
这行代码即可实现对索引1处元素的覆盖,即删除。
代码含义是:将arr数组从索引2开始的4个元素,复制到arr数组从索引1开始的4个元素中。
即可实现挪位,即将被删除索引1处后面的元素,自动往前面移动一位。
这行代码的运行结果是:
arr = {1,3,4,5,6,6}
这样的结果显然不符合要求,因为虽然已经删除了索引1处的元素,并已经将删除元素后面的元素自动往前挪动了一位。但是挪动后,数组的最后一个元素显得有点突兀,
因为我们期望删除索引1后的数组是{1,3,4,5,6}或者是{1,3,4,5,6,null}
最简单解决方案就是,我们直接将arr[size-1]元素设置为null,即数组最后一个元素设置为null。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
最后的最后
面试题千万不要死记,一定要自己理解,用自己的方式表达出来,在这里预祝各位成功拿下自己心仪的offer。
需要完整面试题的朋友可以点击蓝色字体免费获取
始的4个元素,复制到arr数组从索引1开始的4个元素中。
即可实现挪位,即将被删除索引1处后面的元素,自动往前面移动一位。
这行代码的运行结果是:
arr = {1,3,4,5,6,6}
这样的结果显然不符合要求,因为虽然已经删除了索引1处的元素,并已经将删除元素后面的元素自动往前挪动了一位。但是挪动后,数组的最后一个元素显得有点突兀,
因为我们期望删除索引1后的数组是{1,3,4,5,6}或者是{1,3,4,5,6,null}
最简单解决方案就是,我们直接将arr[size-1]元素设置为null,即数组最后一个元素设置为null。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-kPFvDdQm-1712168646477)]
[外链图片转存中…(img-9ZZw28NH-1712168646477)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
[外链图片转存中…(img-2MgSDOx2-1712168646478)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
最后的最后
面试题千万不要死记,一定要自己理解,用自己的方式表达出来,在这里预祝各位成功拿下自己心仪的offer。
需要完整面试题的朋友可以点击蓝色字体免费获取
[外链图片转存中…(img-x1KcDJCa-1712168646478)]
[外链图片转存中…(img-0j9jBXtG-1712168646478)]
[外链图片转存中…(img-JNbOHe8p-1712168646478)]
[外链图片转存中…(img-GluQTq7l-1712168646479)]