集合List之ArrayList详解

一、ArrayList的定义


 ArrayList是一个用数组实现的集合,支持随机访问,元素有序且可重复

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

1、实现RandomAccess接口

 这是一个标记接口,一般此标记接口用于List实现,以表明它支持快速(通常是恒定时间)的随机访问。该接口的主要目的是允许通用算法改变其行为,以便在应用于随机或顺序访问列表时提供良好的性能。
 比如在工具类Collections中,应用二分查找方法时判断是否实现了RandomAccess接口,来确认使用哪种查找方法:

int binarySearch(List<? extends Comparable<? super T>> list, T key) {
    if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
        return Collections.indexedBinarySearch(list, key);
    else
        return Collections.iteratorBinarySearch(list, key);
}

一般RandomAccess并没有任何实际的作用,数据集合数据是否能随机访问取决于集合的数据结构

2、实现Cloneable接口

 这个类java.lang.Cloneable的原理可以参考Java的深拷贝和浅拷贝,这是一个实现克隆的接口,正常情况下我们可以调用Object.clone()方法来实现浅克隆,但是调用该方法的对象必须实现Cloneable接口,否则就会抛出CloneNoSupportException异常。
CloneableRandomAccess接口一样也是标记接口,接口内无任何方法体和常量的声明,也就是说如果想克隆对象,必须要实现Cloneable接口,标明该类是可以被克隆的。

3、实现Serializable

 序列化

4、实现List接口

 这个接口是List类集合的上层接口,定义了实现该接口的类都必须要实现的一组方法,如下图所示,下面我们会对这些方法的实现做详细的介绍。

image

二、字段属性


//集合的默认大小
private static final int DEFAULT_CAPACITY = 10;
//空的数组实例(当有参构造方法中大的initialCapacity=0时赋值给elementData)
private static final Object[] EMPTY_ELEMENTDATA = {};
//这也是一个空的数组实例,和EMPTY_ELEMENTDATA空数组相比是用于了解添加元素时数组膨胀多少(用于无参构造方法使用的空的数组对象)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存储 ArrayList集合的元素,集合的长度即这个数组的长度
//1、当 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时将会清空 ArrayList,即当时扩容的容量为0
//2、当添加第一个元素时,elementData 数组长度会扩展为 DEFAULT_CAPACITY=10(调用方法java.util.ArrayList#grow)
transient Object[] elementData;
//表示集合的长度(而非数组elementData)
private int size;

三、构造函数


1、无参构造函数java.util.ArrayList#ArrayList()
public ArrayList() {
     this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

 此无参构造函数将创建一个DEFAULTCAPACITY_EMPTY_ELEMENTDATA声明的数组,注意此时初始容量为0,而不是大家以为的默认值10。

注意:根据默认构造函数创建的集合,ArrayList list = new ArrayList();此时集合长度是0

2、指定容量的构造函数java.util.ArrayList#ArrayList(int)
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集合。当大于0时给定多少就创建多大的数组;当等于0时创建一个空数组;当小于0时,抛出异常。

3、将已有集合复制到ArrayList中
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

四、添加元素


 通过前面的字段属性和构造函数,我们知道ArrayList集合是由数组构成的,那么向ArrayList中添加元素也就是向数组赋值。我们都知道一个数组的声明是必须要确定大小的,而在使用ArrayList时,好像是能添加任意多个元素,这就涉及到数组的扩容。

 扩容的核心方法就是调用Arrays.copyOf方法,创建一个更大的数组,然后将原数组元素拷贝过去即可。下面我们看看具体实现:

public boolean add(E e) {
    /**
     * 添加元素之前,首先要确定集合的大小
     * size + 1:执行add方法之后,当前ArrayList对象里面的数据数量
     */
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

 如上所示,在通过调用java.util.ArrayList#add(E)方法添加元素之前,我们首先调用java.util.ArrayList#ensureCapacityInternal方法来确定集合的大小,如果集合满了,则要进行扩容操作。注意是如果集合满了,不是像HashMap一样的临界值

private void ensureCapacityInternal(int minCapacity) {//这里的minCapacity 是集合当前大小+1,即size+1
    //elementData 是实际用来存储元素的数组,注意数组的大小和集合的大小不是相等的,前面的size是指集合大小
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    /**
     * 如果数组元素的引用为DEFAULTCAPACITY_EMPTY_ELEMENTDATA即表示当前对象是通过无参构造方法进行实例化的,那么在执行add方法时,将使用默认初始容量来实例化elementData数组长度
     * todo 暂不明白这里为什么要执行Math.max(DEFAULT_CAPACITY, minCapacity),我的想法:如果能进入到当前的if节点里面去,一定是满足:DEFAULT_CAPACITY > minCapacity的
     */
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;//不为空,则返回执行add方法之后的元素数量size+1
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    /**
     * 如果执行add方法之后的元素数量大于原数组长度了,则执行扩容方法grow
     */
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

 判断minCapacity是否大于当前ArrayList内部数组长度,大于的话调用grow方法对内部数组elementData扩容,grow方法代码如下:

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;//得到原始数组的长度
    int newCapacity = oldCapacity + (oldCapacity >> 1);//新数组的长度等于原数组长度的1.5倍
    if (newCapacity - minCapacity < 0)//当新数组长度仍然比minCapacity小,则为保证最小长度,新数组等于minCapacity
        newCapacity = minCapacity;
    //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 = 2147483639
    if (newCapacity - MAX_ARRAY_SIZE > 0)//当得到的新数组长度比 MAX_ARRAY_SIZE 大时,调用 hugeCapacity 处理大数组
        newCapacity = hugeCapacity(minCapacity);
    //调用 Arrays.copyOf 将原数组拷贝到一个大小为newCapacity的新数组(注意是拷贝引用)
    elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // 
         throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ? //minCapacity > MAX_ARRAY_SIZE,则新数组大小为Integer.MAX_VALUE
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

 代码上注释已经说明的很清楚了,这里重点说下Arrays.copyOf(elementData, newCapacity),实际底层调用的是java.lang.System#arraycopy

image

 这是一个调用底层C语言的本地方法,从一个数组对象复制引用到另外一个数组,这里只是做了一次浅拷贝的操作,如果复制之后重新给源数据中的某个对象的属性赋值,目标数据数组中的该对象值也是会改变的

@Test
public void copyArrayTest(){
    Person[] levelOfAppearance = {
        new Person(1, "huge"),
        new Person(2, "suyoupeng"),
        new Person(3, "chenze"),
        new Person(4, "zhangxueyou")
    };
    final Person[] copyOf = Arrays.copyOf(levelOfAppearance, 4);
    System.out.println(JSON.toJSONString(copyOf));
    levelOfAppearance[2].setName("dog");
    System.out.println(JSON.toJSONString(copyOf));
}

输出结果:

[{"age":1,"name":"huge"},{"age":2,"name":"suyoupeng"},{"age":3,"name":"chenze"},{"age":4,"name":"zhangxueyou"}]
[{"age":1,"name":"huge"},{"age":2,"name":"suyoupeng"},{"age":3,"name":"dog"},{"age":4,"name":"zhangxueyou"}]
1. 定义源数据对象
2. 使用copyOf方法复制生成新的对象
3. 打印复制对象确定值是否正确-是
4. 给源数组中的某个对象属性设置值
5. 打印复制对象是否发生改变-是:即证明为浅拷贝

文章引用:System.arraycopy() in Java

总结:
  1. 当通过 ArrayList() 构造一个空集合,初始长度是为0的,第 1 次添加元素,会创建一个长度为10的数组,并将该元素赋值到数组的第一个位置。
  2. 第 2 次添加元素,集合不为空,而且由于集合的长度size+1是小于数组的长度10,所以直接添加元素到数组的第二个位置,不用扩容。
  3. 第 11 次添加元素,此时 size+1 = 11,而数组长度是10,这时候创建一个长度为10+10*0.5 = 15 的数组(扩容1.5倍),然后将原数组元素引用拷贝到新数组。并将第 11 次添加的元素赋值到新数组下标为10的位置。
  4. 【不重要】第 Integer.MAX_VALUE - 8 = 2147483639,然后 2147483639%1.5=1431655759(这个数是要进行扩容) 次添加元素,为了防止溢出,此时会直接创建一个 1431655759+1 大小的数组,这样一直,每次添加一个元素,都只扩大一个范围。
  5. 【不重要】第 Integer.MAX_VALUE - 7 次添加元素时,创建一个大小为 Integer.MAX_VALUE 的数组,在进行元素添加。
  6. 【不重要】第 Integer.MAX_VALUE + 1 次添加元素时,抛出 OutOfMemoryError 异常。

五、删除元素


1、根据索引删除元素
    public E remove(int index) {
        rangeCheck(index);//判断给定索引的范围,超过集合大小则抛出异常
        modCount++;
        E oldValue = elementData(index);//得到索引处的删除元素
        int numMoved = size - index - 1;
        if (numMoved > 0)
            //通过 System.arraycopy()将数组elementData 的下标index+1之后长度为 numMoved的元素拷贝到从index开始的位置
            /**
             * 参数1:src –源数组。
             * 参数2:srcPos –源数组中的起始位置。
             * 参数3:dest –目标数组。
             * 参数4:destPos –目标数据中的起始位置。
             * 参数5:length –要复制的数组元素的数量。
             */
            System.arraycopy(elementData, index+1, elementData, index,numMoved);
        elementData[--size] = null; // 将数组最后一个元素置为 null,便于垃圾回收
        return oldValue;
    }

元素删除的核心在于System.arraycopy方法,并且将集合元素是最后一个赋值为null,等待GC清除即可

 remove(int index) 方法表示删除索引index处的元素,首先通过 rangeCheck(index) 方法判断给定索引的范围,超过集合大小则抛出异常;接着通过 System.arraycopy 方法对数组进行自身拷贝。

2、直接删除指定元素
public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

/**
 * 私有remove方法,它跳过边界检查并且不返回已删除的值
 */
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

 remove(Object o)方法是删除第一次出现的该元素。然后通过System.arraycopy进行数组自身拷贝

六、修改元素


 通过调用set(int index, E element)方法在指定索引index处的元素替换为element。并返回原数组的元素。

public E set(int index, E element) {
    rangeCheck(index);//判断索引合法性

    E oldValue = elementData(index);//获得原数组指定索引的元素
    elementData[index] = element;//将指定所引处的元素替换为 element
    return oldValue;//返回原数组索引元素
}

七、查找元素


1、根据索引查找元素
public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

 同理,首先还是判断给定索引的合理性,然后直接返回处于该下标位置的数组元素。

2、根据元素查找索引
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

 注意:indexOf(Object o)方法是返回第一次出现该元素的下标,如果没有则返回-1。

 还有lastIndexOf(Object o)方法是返回最后一次出现该元素的下标,也是遍历只是遍历方式相反

public int lastIndexOf(Object o) {
    if (o == null) {
        for (int i = size-1; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = size-1; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

八、遍历集合


1、普通for循环遍历

 前面我们介绍查找元素时,知道可以通过get(int index)方法,根据索引查找元素,那么遍历同理:

ArrayList list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
for(int i = 0 ; i < list.size() ; i++){
    System.out.print(list.get(i)+" ");
}
2、迭代器iterator
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
Iterator<String> it = list.iterator();
while(it.hasNext()){
    String str = it.next();
    System.out.print(str+" ");
}

 在上面的依赖结构图上我们可以看到ArrayList实现了List-->Collection—>Iterable,在Iterable接口中有个Iterator<T> iterator()方法,能获取到Iterator对象,能用该对象进行集合的遍历,什么能用该对象进行集合遍历?我们再看看ArrayList类中该方法的实现:

public Iterator<E> iterator() {
    return new Itr();
}

 该方法返回一个Itr对象,文章开头的依赖结构图里面我们已经提到过,这是ArrayList类里的一个内部类。

private class Itr implements Iterator<E> {
    int cursor;       //游标, 下一个要返回的元素的索引
    int lastRet = -1; // 返回最后一个元素的索引; 如果没有这样的话返回-1.
    int expectedModCount = modCount;

    //通过 cursor != size 判断是否还有下一个元素
    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();//迭代器进行元素迭代时同时进行增加和删除操作,会抛出异常
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;//游标向后移动一位
        return (E) elementData[lastRet = i];//返回索引为i处的元素,并将 lastRet赋值为i
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);//调用ArrayList的remove方法删除元素
            cursor = lastRet;//游标指向删除元素的位置,本来是lastRet+1的,这里删除一个元素,然后游标就不变了
            lastRet = -1;//lastRet恢复默认值-1
            expectedModCount = modCount;//expectedModCount值和modCount同步,因为进行add和remove操作,modCount会加1
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {//便于进行forEach循环
        Objects.requireNonNull(consumer);
        final int size = ArrayList.this.size;
        int i = cursor;
        if (i >= size) {
            return;
        }
        final Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        while (i != size && modCount == expectedModCount) {
            consumer.accept((E) elementData[i++]);
        }
        // update once at end of iteration to reduce heap write traffic
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }

    //前面在新增元素add() 和 删除元素 remove() 时,我们可以看到 modCount++。修改set() 是没有的
    //也就是说不能在迭代器进行元素迭代时进行增加和删除操作,否则抛出异常
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

 注意在进行next()方法调用的时候,会进行checkForComodification()调用,该方法表示迭代器在进行元素迭代时,如果同时进行增加或者删除操作,会抛出ConcurrentModificationException异常。比如:

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    final String next = iterator.next();
    System.out.print(next + "\t");
    list.remove(next);
}
java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at com.cheez.ok.common.test.ArrayListTest.ArrayListLoop(ArrayListTest.java:70)

 解决办法是不调用ArrayList.remove()方法,转而调用迭代器的删除方法:

final Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    final String next = iterator.next();
    System.out.print(next + "\t");
    iterator.remove();
}

 我们来研究下remove的具体实现:

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

 其实际上是通过调用java.util.ArrayList#remove(int)来删除元素的,那么这里的重点在于lastRet的值,我们翻查源码发现其默认值:

int lastRet = -1;

 唯二赋值的方法:

public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    /**
     * 这里给lastRet属性进行赋值,即当前next操作之前游标所在位置
     * 游标初始位置0
     */
    return (E) elementData[lastRet = i];
}
// 忽略掉,和remove方法不相关,这里不多做说明
java.util.ArrayList.Itr#forEachRemaining

总结:迭代器删除的过程实际是调用的ArrayList.remove(int index)的方法,只是通过算法计算出索引位置。

3、迭代器的变种forEach
for (String str : list) {
    System.out.print(str + "\t");
}

 这种可以看成是JDK的一种语法糖,通过反编译class文件,我们可以看到其具体实现还是通过调用迭代器Iterator进行遍历的。如下:

Iterator var6 = list.iterator();
while(var6.hasNext()) {
    String str = (String)var6.next();
    System.out.print(str + "\t");
}
4、迭代器ListIterator
ListIterator<String> stringListIterator = list.listIterator();
/**
 * 迭代器正向循环
 */
while (stringListIterator.hasNext()) {
    System.out.print(stringListIterator.next() + "\t");
}
System.out.println();

/**
 * 迭代器反向循环
 */
while (stringListIterator.hasPrevious()) {
    System.out.print(stringListIterator.previous() + "\t");
}

 还能一边遍历,一天新增或者删除操作:

ListIterator<String> stringListIterator = list.listIterator();
/**
 * 新增数据
 */
while (stringListIterator.hasNext()) {
    System.out.print(stringListIterator.next() + "\t");
    stringListIterator.add("123");
}
/**
 * 删除数据
 */
while (stringListIterator.hasPrevious()) {
    System.out.print(stringListIterator.previous() + "\t");
    stringListIterator.remove();
}

总结:相比于 Iterator 迭代器,这里的 ListIterator 多出了能向前迭代,以及能够新增/删除元素。 下面我们看看具体实现。

public ListIterator<E> listIterator() {
    return new ListItr(0);
}

 获取ListIterator对象即ListItr默认游标0

 从文章开头的类依赖图中我们可以看到ListItr继承至ArrayList.Itr并实现接口ListIterator

private class ListItr extends Itr implements ListIterator<E>

 看下其继承接口ListIterator的方法列表:

image

 接下来就是ArrayList.Itr具体实现:

//注意 内部类 ListItr 继承了另一个内部类 Itr
private class ListItr extends Itr implements ListIterator<E> {
    ListItr(int index) {//构造函数,进行游标初始化
        super();
        cursor = index;
    }

    public boolean hasPrevious() {//判断是否有上一个元素
        return cursor != 0;
    }

    public int nextIndex() {//返回下一个元素的索引
        return cursor;
    }

    public int previousIndex() {//返回上一个元素的索引
        return cursor - 1;
    }

    //该方法获取当前索引的上一个元素
    @SuppressWarnings("unchecked")
    public E previous() {
        checkForComodification();//迭代器进行元素迭代时同时进行增加和删除操作,会抛出异常
        int i = cursor - 1;
        if (i < 0)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i;//游标指向上一个元素
        return (E) elementData[lastRet = i];//返回上一个元素的值
    }


    public void set(E e) {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.set(lastRet, e);
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    //相比于迭代器 Iterator ,这里多了一个新增操作
    public void add(E e) {
        checkForComodification();

        try {
            int i = cursor;
            ArrayList.this.add(i, e);
            cursor = i + 1;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
}

九、SubList


 在ArrayList中有这样一个方法:

public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

 作用是返回从 fromIndex(包括) 开始的下标,到toIndex(不包括) 结束的下标之间的元素视图(非实际数据存储对象)。如下使用:

ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
final List<String> strings = list.subList(0, 2);
System.out.println(JSON.toJSONString(strings));
["a","b"]

 这里出现了SubList类,这也是ArrayList中的一个内部类,从文章开始的图中可以知道继承AbstractList并实现RandomAccess接口。

注意:返回的是原集合的视图,也就是说,如果对subList出来的集合进行修改或新增操作,那么原始集合也会发生同样的操作(SubList对象相当于一个工具类,用于去查询源对象list中的数据)

ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
final List<String> strings = list.subList(0, 2);
System.out.println(JSON.toJSONString(strings));

list.set(0, "aaaa");
System.out.println(JSON.toJSONString(strings));
["a","b"]
["aaaa","b"]

 想要独立出来一个集合,解决办法如下:

ArrayList<String> arrayList = new ArrayList<>(strings);

十、其他方法


public int size() {
    return size;
}
public boolean isEmpty() {
 return size == 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容