zoukankan      html  css  js  c++  java
  • ArrayList介绍2(contains方法!)

    这章开始我们对Collection的具体实现类来进行介绍,首先是List,而List中ArrayList是最常用的。

    ArrayList介绍

    ArrayList简介

    1. ArrayList是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List,RandomAccess,Cloneable,java.io.Serializable这些接口。
    2. ArrayList与Collection的关系如下图,实现代表继承,虚线代表实现接口:
       
      image
    3. ArrayList继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
    4. ArrayList实现了RandmoAccess接口,即提供了随机访问的功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。
    5. ArrayList实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
    6. ArrayList实现了java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。

    注意:<font color="FF0000">ArrayList中的操作不是线程安全的!</font>所以,建议在单线程中使用,多线程情况下可以选择CopyOnWriteArrayList或者使用Collections中的synchronizedList方法将其包装成一个线程安全的List。

    ArrayList的API

    // Collection中定义的API
    boolean             add(E object)
    boolean             addAll(Collection<? extends E> collection)
    void                clear()
    boolean             contains(Object object)
    boolean             containsAll(Collection<?> collection)
    boolean             equals(Object object)
    int                 hashCode()
    boolean             isEmpty()
    Iterator<E>         iterator()
    boolean             remove(Object object)
    boolean             removeAll(Collection<?> collection)
    boolean             retainAll(Collection<?> collection)
    int                 size()
    <T> T[]             toArray(T[] array)
    Object[]            toArray()
    // AbstractCollection中定义的API
    void                add(int location, E object)
    boolean             addAll(int location, Collection<? extends E> collection)
    E                   get(int location)
    int                 indexOf(Object object)
    int                 lastIndexOf(Object object)
    ListIterator<E>     listIterator(int location)
    ListIterator<E>     listIterator()
    E                   remove(int location)
    E                   set(int location, E object)
    List<E>             subList(int start, int end)
    // ArrayList新增的API
    Object               clone()
    void                 ensureCapacity(int minimumCapacity)
    void                 trimToSize()
    void                 removeRange(int fromIndex, int toIndex)
    

    ArrayList源码分析

    属性

    ArrayList的主要属性如下代码所示:

    //序列化id
    private static final long serialVersionUID = 8683452581122892189L;
    //容器默认初始化大小
    private static final int DEFAULT_CAPACITY = 10;
    //一个空对象
    private static final Object[] EMPTY_ELEMENTDATA = {};
    //一个空对象,如果使用默认构造函数创建ArrayList,则默认对象内容是该值
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    //ArrayList存放对象的容器,后面的添加、删除等操作都是基于该属性来进行操作
    transient Object[] elementData;
    //当前列表已使用的长度
    private int size;
    //数组最大长度(2147483639),这里为什么是Integer.MAX_VALUE - 8是因为有些虚拟机在数组中保留了一些头部信息,防止内存溢出
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    //这个是从AbstractList继承过来的,代表ArrayList集合修改的次数
    protected transient int modCount = 0;
    

    关于Java中transient关键字的解释:

     我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。

     然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。

    总之,java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。

    构造函数

    无参构造函数

    如果不传入参数,则使用默认无参构造方法创建ArrayLisy对象,如下:

    public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }
    

    注意:此时我们创建的ArrayList对象中的elementData中的长度是0,size是0,当进行第一次add的时候,elementDate将会变成默认的长度:10。

    带int类型的构造函数

    如果传入参数,则代表指定ArrayList的初始数组长度;传入参数如果是大于0,则使用用户的参数初始化;如果参数等于0,则用内部的空对象EMPTY_ELEMENTDATA的地址直接赋值给elementData;否则抛出异常,如下:

    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);
            }
        }
    

    带Collection对象的构造函数

    1. 将Collection对象转换成数组,然后将数组的地址赋值给elementData。
    2. 更新size的值,如果size的值等于0直接将内部空对象EMPTY_ELEMENTDATA的地址赋值给elementData。
    3. 如果size的值大于0,则执行Arrays.copy方法,把Collection对象的内容copy(可以理解为深拷贝)到elementData中,并且这些元素是按照该collection的迭代器返回它们的顺序排列的。
     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;
            }
        }
    

    这里介绍下System.arraycopyArrays.copy方法,因为后分析源码时会经常用到。

    System.arraycopy方法:它就是从指定的源数组将元素中复制到目标数组,复制从指定的位置开始,到设定的复制长度结束,然后从目标数组的指定起始位置依次插入。

        // src 源数组
        // srcPos 源数组要复制的起始位置
        // dest 要赋值到的目标数组
        // destPos 目标数组放置的起始位置
        // length 复制的长度
        // 使用了native关键字,说明调用的是其他语言写的底层函数
        public static native void arraycopy(Object src,  int  srcPos,
                                            Object dest, int destPos,
                                            int length);
    

    Arrays.copy方法:它新建了一个数组并且将原数组的内容拷贝到长度为newLength的新数组中,并且返回该新数组。

        // original 要复制的数组
        // newLength 要返回副本的长度
        // newwType 要返回的副本类型
        // 内部调用了System.arraycopy方法
        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;
        }
    

    区别:

    1. System.arraycopy需要目标数组,将原数组拷贝到你自己定义的数值里,而且可以选择拷贝的起点和长度以及放入新数组中的位置。
    2. Arrays.copyof是系统自动在内部新建一个数组,调用System.arraycopy将原数组的内容拷贝到长度为newLength的新数组中,并返回新建的数组。

    添加元素

    ArrayList提供了add(E e)、add(int index, E element)、addAll(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c)、set(int index, E element)这个五个方法来实现ArrayList增加。

    add(E e)方法
    
    //官方解释:将指定的元素追加到列表(elementData)的末尾
        public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
    

    接下来我们看ensureCapacityInternal方法,以及它内部调用的方法。

        //参数值实际是size+1
        private void ensureCapacityInternal(int minCapacity) {
            ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
        }
    
     //这个方式是判断当前数组是否是个空数组,如果是就返回默认长度10,否则就返回size+1;也就是说如果你是用无参构造函数初始化ArrayList,那么在第一次调用add方法时,默认长度会变成10
      private static int calculateCapacity(Object[] elementData, int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            }
            return minCapacity;
        }
    
        //这个方法首先将集合修改次数加1,然后判断数组的长度是否可以存入下一个元素,如果长度不够会调用grow方法进行扩容
        private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }
    
        //这个方法首先定义数组新的长度为原来数组长度的1.5倍,如果新长度减去所需数组的最小长度小于0,那么新长度就等于所需数组最小长度;再下面的判断是如果新长度大于MAX_ARRAY_SIZE(ArrayList内部定义MAX_ARRAY_SIZE的值是:2147483639)就调用hugeCapacity方法,最后调用Arrays.copyOf将扩容后的新数组地址赋值给elementData
        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    
        //如果扩容长度超过MAX_ARRAY_SIZE,则设置长度为Integer.MAX_VALUE,但不是能百分百成功的,这取决于虚拟机。(如果我们可以在某些虚拟机上可以避免OutOfMemory,我们将另外分配Integer.MAX_VALUE,如果你很幸运(取决于虚拟机),我们将成功)
        private static int hugeCapacity(int minCapacity) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
        }
    

    最后总结一下add方法的逻辑:

    1. 确保数组已使用长度(size)加1后可以存入下一个元素。

    2. 修改次数modCount标识自增1,如果当前数组已使用长度+1后大于当前数组长度,则调用grow方法,扩容数组,grow方法会将当前数组的长度变为原来容量的1.5倍。

    3. 确保新加的元素有地方存储后,则将新元素添加到位于size++的位置上。

    4. 返回添加成功的布尔值。

    add(int index, E element)
    

    这个方法和上面的add类型,该方法可以按照元素的位置,指定新元素位置插入。

        public void add(int index, E element) {
            //判断索引位置是否正确
            rangeCheckForAdd(index);
            //扩容检测
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            //对源数组进行复制处理(位移),从index + 1到size - index
            //即向后移动位于当前位置和后面的元素
            System.arraycopy(elementData, index, elementData, index + 1,
                             size - index);
            //在指定的位置赋值
            elementData[index] = element;
            size++;
        }   
    

    该方法首先调用rangeCheckForAdd方法判断指定的位置小于当前数组的长度并且大于0,否则抛出异常。

        private void rangeCheckForAdd(int index) {
            if (index > size || index < 0)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }
    

    第二步调用的ensureCapacityInternal方法和上面的add方法逻辑一样。

    第三步调用System.arraycopy方法把指定下标以及后面的元素全部往后移一位

    最后将新的元素放到指定位置(index)上,并将size+1。

    addAll(Collection<? extends E> c)
    
        //按照指定的Collection迭代器所返回的顺序,依次插入到列表尾部。
        public boolean addAll(Collection<? extends E> c) {
            // 将c转换为数组
            Object[] a = c.toArray();
            int numNew = a.length;
            //扩容处理,大小为size + numNew
            ensureCapacityInternal(size + numNew);  // Increments modCount
            System.arraycopy(a, 0, elementData, size, numNew);
            size += numNew;
            return numNew != 0;
        }
    

    该方法首先传过来的Collection集合转换为数组,然后做扩容处理,接着使用System.arraycopy把转换后的数组复制到列表尾部。

    addAll(int index, Collection<? extends E> c)
    
        public boolean addAll(int index, Collection<? extends E> c) {
            //判断索引位置是否正确
            rangeCheckForAdd(index);
            // 将c转换为数组
            Object[] a = c.toArray();
            int numNew = a.length;
            //扩容处理,大小为size + numNew
            ensureCapacityInternal(size + numNew);  // Increments modCount
            //如果插入索引小于列表长度,则将当前索引等于index和大于index的元素往后移numMoved个位置
            int numMoved = size - index;
            if (numMoved > 0)
                System.arraycopy(elementData, index, elementData, index + numNew,
                                 numMoved);
            //将数组添加到列表尾部
            System.arraycopy(a, 0, elementData, index, numNew);
            //更新列表长度
            size += numNew;
            return numNew != 80;
        }
    
    set(int index, E element)
    
        public E set(int index, E element) {
            //判断插入位置是否正确,如果大于列表长度会抛出异常
            rangeCheck(index);
            //获取插入位置的当前元素
            E oldValue = elementData(index);
            //将新的元素替换当前插入位置的元素
            elementData[index] = element;
            //返回插入位置老的值
            return oldValue;
        }
    

    删除元素

    ArrayList提供了外界remove(int index)、remove(Object o)、removeAll(Collection<?> c)、clear()四个方法进行元素的删除。

    remove(int index) //移除指定位置上的元素
    
        public E remove(int index) {
            //判断删除位置是否正确,如果大于列表长度会抛出异常
            rangeCheck(index);
            //将集合修改次数加1
            modCount++;
            //获取当前删除位置上的元素
            E oldValue = elementData(index);
            //判断是否删除的是最后一个元素,如果不是将删除位置后的元素向左移numMoved个位置
            int numMoved = size - index - 1;
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index,
                                 numMoved);
            //将列表最后的元素置为null,等待垃圾收集器收集
            elementData[--size] = null; // clear to let GC do its work
            //返回删除位置老的值
            return oldValue;
        }
    
    remove(Object o) //移除指定元素
    
        public boolean remove(Object o) {
            //因为ArrayList允许存在null,所以需要进行null判断
            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;
        }
    
        private void fastRemove(int index) {
            //将集合修改次数加1      
            modCount++;
            //判断是否删除的是最后一个元素,如果不是将删除位置后的元素向左移numMoved个位置
            int numMoved = size - index - 1;
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index,
                                 numMoved);
            //将列表最后的元素置为null,等待垃圾收集器收集
            elementData[--size] = null; // clear to let GC do its work
        }
    
    removeAll(Collection<?> c)
    
        public boolean removeAll(Collection<?> c) {
            //进行判断,如果c为null抛出异常
            Objects.requireNonNull(c);
            return batchRemove(c, false);
        }
    
        private boolean batchRemove(Collection<?> c, boolean complement) {
            final Object[] elementData = this.elementData;
            int r = 0, w = 0;
            boolean modified = false;
            try {
                //遍历数组,并检查这个集合是否包含对应的值,移动要保留的值到数组前面,w最后值为要保留的元素的数量
                //若保留,就将相同的元素移动到前段;不删除,就将不同元素移动到前段
                for (; r < size; r++)
                    if (c.contains(elementData[r]) == complement)
                        elementData[w++] = elementData[r];
            } finally {
                // 确保异常抛出前的部分可以完成期望的操作,而被遍历的部分会被接到后面
                //r不等于size表示可能出错了
                if (r != size) {
                    System.arraycopy(elementData, r,
                                     elementData, w,
                                     size - r);
                    w += size - r;
                }
                //如果w等于size,表示全部元素都保留了,所以也就没有删除操作发生,所以会返回false;反之,返回true,并更改数组
                //而w不等于size的时候,即使try块抛出异常,也能正确处理异常抛出前的操作,因为w始终为要保留的前段部分的长度,数组也不会因此乱序
                if (w != size) {
                    // clear to let GC do its work
                    for (int i = w; i < size; i++)
                        elementData[i] = null;
                    modCount += size - w;
                    size = w;
                    modified = true;
                }
            }
            return modified;
        }
    
    clear() //清空ArrayList内的所有元素,不减小数组容量
    
        public void clear() {
            //将集合修改次数加1  
            modCount++;
            //循环将列表中的所有元素置为null,等待垃圾收集器收集
            // clear to let GC do its work
            for (int i = 0; i < size; i++)
                elementData[i] = null;
            //将列表长度设为0
            size = 0;
        }
    

    查找元素

    ArrayList提供了get(int index)用读取ArrayList中的元素。由于ArrayList是动态数组,所以我们完全可以根据下标来获取ArrayList中的元素,而且速度还比较快。

        public E get(int index) {
            //判断删除位置是否正确,如果大于列表长度会抛出异常
            rangeCheck(index);
            //直接返回列表中下标等于index的元素
            return elementData(index);
        }
    

    判断元素是否存在列表中

    ArrayList提供了contains(Object o)用于判断元素是否存在于列表中。

    注意:contains方法会遍历ArrayList。

    public boolean contains(Object o) {
            //调用indexOf方法判断需要查找的元素在列表中的下标是否大于等于0,小于0则不存在
            return indexOf(o) >= 0;
        }
    
        public int indexOf(Object o) {
            //因为ArrayList允许存在null,所以需要进行null判断
            if (o == null) {
                //遍历列表,如果列表存在null值的元素,直接返回其下标位置
                for (int i = 0; i < size; i++)
                    if (elementData[i]==null)
                        return i;
            } else {
                //遍历列表,使用equals判断是否有相等的元素,有的话直接返回其下标位置
                for (int i = 0; i < size; i++)
                    if (o.equals(elementData[i]))
                        return i;
            }
            //列表中不能存在传进来的元素,返回-1
            return -1;
        }
    

    最小化ArrayList的实际存储量

    ArrayList提供了trimToSize()方法用于将底层数组的容量调整为当前列表保存的实际元素的大小

        public void trimToSize() {
            //将集合修改次数加1
            modCount++;
            //如果当前ArrayList的实际长度小于列表的长度,将列表超过size后的空余的空间(包括null值)去除,调用Arrays.cppyof方法拷贝elementData,长度为size
            if (size < elementData.length) {
                elementData = (size == 0)
                  ? EMPTY_ELEMENTDATA
                  : Arrays.copyOf(elementData, size);
            }
        }
    

    截取ArrayList部分内容

    ArrayList提供了subList(int fromIndex, int toIndex)方法来实现部分数据的截取。

    可以从源码中看到其实是创建了一个SubList的内部对象,可以理解为是返回当前ArrayList的部分视图,其实指向的存放数据的还是一个地方。如果修改了subList返回的内容的话,原来的内容也会被修改。

        public List<E> subList(int fromIndex, int toIndex) {
            //检查需要截取的下标位置是否正确
            subListRangeCheck(fromIndex, toIndex, size);
            return new SubList(this, 0, fromIndex, toIndex);
        }
    

    其它方法

        //判断ArrayList是否为空
        public boolean isEmpty() {
            return size == 0;
        }
    
        //反向查找元素位置,与上述的indexOf相反
        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;
        }
    
        //将元素全部拷贝到v中
        public Object clone() {
            try {
                ArrayList<?> v = (ArrayList<?>) super.clone();
                v.elementData = Arrays.copyOf(elementData, size);
                v.modCount = 0;
                return v;
            } catch (CloneNotSupportedException e) {
                // this shouldn't happen, since we are Cloneable
                throw new InternalError(e);
            }
        }
    
        //返回ArrayList拷贝后的Object数组
        public Object[] toArray() {
            return Arrays.copyOf(elementData, size);
        }
    
        //返回ArrayList的模板数组。所谓模板数组,即可将T设置为任意数据类型
        public <T> T[] toArray(T[] a) {
            //若a的长度小于ArrayList中的元素个数,返回拷贝了ArrayList中全部元素的新数组
            if (a.length < size)
                // Make a new array of a's runtime type, but my contents:
                return (T[]) Arrays.copyOf(elementData, size, a.getClass());
            //若a的长度大于等于ArrayList中的元素个数,则将ArrayList中的元素全部拷贝到a中
            System.arraycopy(elementData, 0, a, 0, size);
            if (a.length > size)
                a[size] = null;
            return a;
        }
    
        //将ArrayList中的元素写入到输入流中,先写容量,在写元素
        private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException{
            // Write out element count, and any hidden stuff
            int expectedModCount = modCount;
            s.defaultWriteObject();
    
            // Write out size as capacity for behavioural compatibility with clone()
            s.writeInt(size);
    
            // Write out all elements in the proper order.
            for (int i=0; i<size; i++) {
                s.writeObject(elementData[i]);
            }
    
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
        }
    
        //从输入流中读取数据到elementData中,一样是先读容量,再读数据
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            elementData = EMPTY_ELEMENTDATA;
    
            // Read in size, and any hidden stuff
            s.defaultReadObject();
    
            // Read in capacity
            s.readInt(); // ignored
    
            if (size > 0) {
                // be like clone(), allocate array based upon size not capacity
                int capacity = calculateCapacity(elementData, size);
                SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
                ensureCapacityInternal(size);
    
                Object[] a = elementData;
                // Read in all elements in the proper order.
                for (int i=0; i<size; i++) {
                    a[i] = s.readObject();
                }
            }
        }
    

    小结

    • ArrayList自己实现了序列化和反序列化,因为它实现了writeObject和readObject方法。
    • ArrayList基于数组实现,会自动扩容。
    • 添加元素时会自己判断是否需要扩容,最好指定一个大概的大小,防止后面多次扩容带来的内存消耗;删除元素时不会减少容量,删除元素时,将删除掉的位置元素置为null,下次gc就会自动回收这些元素所占的空间。
    • ArrayList是线程不安全的。
    • 使用iterator遍历可能会引发多线程异常。


    作者:leisurexi
    链接:https://www.jianshu.com/p/4a403049a4a2
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 相关阅读:
    WCF+EntityFramework+mysql总结
    实现Win7远程桌面关机和重启
    EF 4.1 一些操作
    Ado.net利用反射执行SQL得到实体
    .net IL 指令速查
    VS2010 /VC/bin/rcdll.dll 无法找到资源编译器
    Win7下 httpRequest带证书请求https网站
    VS2010 自动关闭的问题解决方法
    Android 之 悬浮窗口
    论 Java 中获取一组不重复的随机数之性能问题
  • 原文地址:https://www.cnblogs.com/lzh1043060917/p/12767227.html
Copyright © 2011-2022 走看看