• ArrayList LinkedList Vector


    ArrayList是基于数组实现的,没有容量的限制。

    在删除元素的时候,并不会减少数组的容量大小,可以调用ArrayList的trimeToSize()来缩小数组的容量。

    ArrayList, LinkedList, Vector, Stack是List的4个实现类。
      ArrayList 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。
      LinkedList 是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率低。
      Vector 是矢量队列,和ArrayList一样,它也是一个动态数组,由数组实现。但是ArrayList是非线程安全的,而Vector是线程安全的。
      Stack 是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。

     

     

    如果涉及到“栈”、“队列”、“链表”等操作,应该考虑用List,具体的选择哪个List,根据下面的标准来取舍。
    (01) 对于需要快速插入,删除元素,应该使用LinkedList。
    (02) 对于需要快速随机访问元素,应该使用ArrayList。
    (03) 对于“单线程环境” 或者 “多线程环境,但List仅仅只会被单个线程操作”,此时应该使用非同步的类(如ArrayList)。
           对于“多线程环境,且List可能同时被多个线程操作”,此时,应该使用同步的类(如Vector)。

    ArrayList,LinkedList,Vestor这三个类都实现了java.util.List接口,但它们有各自不同的特性,主要如下:
    ArrayList:底层用数组实现的List

    特点:查询效率高,增删效率低 轻量级 线程不安全

    LinkedList:底层用双向循环链表 实现的List

    特点:查询效率低,增删效率高

    Vector: 底层用数组实现List接口的另一个类

    特点:重量级,占据更多的系统开销 线程安全


    一、同步性

    ArrayList,LinkedList是不同步的,而Vestor是同步的。所以如果不要求线程安全的话,可以使用ArrayList或 LinkedList,可以节省为同步而耗费的开销。但在多线程的情况下,有时候就不得不使用Vector了。当然,也可以通过一些办法包装 ArrayList,LinkedList,使他们也达到同步,但效率可能会有所降低。

    二、数据增长
    从内部实现机制来讲ArrayList和Vector都是使用Objec的数组形式来存储的。当你向这两种类型中增加元素的时候,如果元素的数目超出了内 部数组目前的长度它们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度,ArrayList是原来的50%,所以最后你获得 的这个集合所占的空间总是比你实际需要的要大。所以如果你要在集合中保存大量的数据那么使用Vector有一些优势,因为你可以通过设置集合的初始化大小 来避免不必要的资源开销。

    三、检索、插入、删除对象的效率

    ArrayList和Vector中,从指定的位置(用index)检索一个对象,或在集合的末尾插入、删除一个对象的时间是一样的,可表示为O(1)。 但是,如果在集合的其他位置增加或移除元素那么花费的时间会呈线形增长:O(n-i),其中n代表集合中元素的个数,i代表元素增加或移除元素的索引位 置。为什么会这样呢?以为在进行上述操作的时候集合中第i和第i个元素之后的所有元素都要执行(n-i)个对象的位移操作。
    LinkedList中,在插入、删除集合中任何位置的元素所花费的时间都是一样的—O(1),但它在索引一个元素的时候比较慢,为O(i),其中i是索引的位置。


    一般大家都知道ArrayList和LinkedList的大致区别:
         1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
         2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
         3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。

    ArrayList和LinkedList是两个集合 类,用于存储一系列的对象引用(references)。例如我们可以用ArrayList来存储一系列的String或者Integer。那么 ArrayList和LinkedList在性能上有什么差别呢?什么时候应该用ArrayList什么时候又该用LinkedList呢?


    一.时间复 杂度
    首先一点关键的是,ArrayList的内部实现是基于基础的对象数组的,因此,它使用get方法访问列表中的任意一个元素时 (random access),它的速度要比LinkedList快。LinkedList中的get方法是按照顺序从列表的一端开始检查,直到另外一端。对 LinkedList而言,访问列表中的某个指定元素没有更快的方法了。
    假设我们有一个很大的列表,它里面的元素已经排好序了,这个列表可能是ArrayList类型 的也可能是LinkedList类型的,现在我们对这个列表来进行二分查找(binary search),比较列表是ArrayList和LinkedList时的查询速度,看下面的程序:

    Java代码
    package com.mangocity.test;   
    import java.util.LinkedList;   
    import java.util.List;   
    import java.util.Random;   
    import java.util.ArrayList;   
    import java.util.Arrays;   
    import java.util.Collections;   
    public class TestList ...{   
         public static final int N=50000;   
      
         public static List values;   
      
         static...{   
             Integer vals[]=new Integer[N];   
      
             Random r=new Random();   
      
             for(int i=0,currval=0;i<N;i++)...{   
                 vals=new Integer(currval);   
                 currval+=r.nextInt(100)+1;   
             }   
      
             values=Arrays.asList(vals);   
         }   
      
         static long timeList(List lst)...{   
             long start=System.currentTimeMillis();   
             for(int i=0;i<N;i++)...{   
                 int index=Collections.binarySearch(lst, values.get(i));   
                 if(index!=i)   
                     System.out.println("***错误***");   
             }   
             return System.currentTimeMillis()-start;   
         }   
         public static void main(String args[])...{   
             System.out.println("ArrayList消耗时间:"+timeList(new ArrayList(values)));   
             System.out.println("LinkedList消耗时间:"+timeList(new LinkedList(values)));   
         }   
    }   

    我得到的输出 是:ArrayList消耗时间:15
                     LinkedList消耗时间:2596
    这个结果不是固定的,但是基本上ArrayList的 时间要明显小于LinkedList的时间。因此在这种情况下不宜用LinkedList。二分查找法使用的随机访问(random access)策略,而LinkedList是不支持快速的随机访问的。对一个LinkedList做随机访问所消耗的时间与这个list的大小是成比例 的。而相应的,在ArrayList中进行随机访问所消耗的时间是固定的。
    这是否表明ArrayList总是比LinkedList性能要好呢?这并不一定,在某些情况 下LinkedList的表现要优于ArrayList,有些算法在LinkedList中实现时效率更高。比方说,利用 Collections.reverse方法对列表进行反转时,其性能就要好些。
    看这样一个例子,加入我们有一个列表,要对其进行大量的插入和删除操作,在这种情况下 LinkedList就是一个较好的选择。请看如下一个极端的例子,我们重复的在一个列表的开端插入一个元素:

    Java代码
    package com.mangocity.test;   
      
    import java.util.*;   
    public class ListDemo {   
         static final int N=50000;   
         static long timeList(List list){   
         long start=System.currentTimeMillis();   
         Object o = new Object();   
         for(int i=0;i<N;i++)   
             list.add(0, o);   
         return System.currentTimeMillis()-start;   
         }   
         public static void main(String[] args) {   
             System.out.println("ArrayList耗时:"+timeList(new ArrayList()));   
             System.out.println("LinkedList耗时:"+timeList(new LinkedList()));   
         }   
    }   
    这时我的输出结果是:ArrayList耗时:2463


                               LinkedList耗时:15
    这和前面一个例子的结果截然相反,当一个元素被加到ArrayList的最开端时,所有已经存在的元素都会后 移,这就意味着数据移动和复制上的开销。相反的,将一个元素加到LinkedList的最开端只是简单的未这个元素分配一个记录,然后调整两个连接。在 LinkedList的开端增加一个元素的开销是固定的,而在ArrayList的开端增加一个元素的开销是与ArrayList的大小成比例的。


    二.空间复 杂度
    在LinkedList中有一个私有的内部类,定义如下:

    Java代码
    private static class Entry {   
             Object element;   
             Entry next;   
             Entry previous;   
         }   

    每个Entry对象 reference列表中的一个元素,同时还有在LinkedList中它的上一个元素和下一个元素。一个有1000个元素的LinkedList对象将 有1000个链接在一起的Entry对象,每个对象都对应于列表中的一个元素。这样的话,在一个LinkedList结构中将有一个很大的空间开销,因为 它要存储这1000个Entity对象的相关信息。
    ArrayList使用一个内置的数组来存储元素,这个数组的起始容量是10.当数组需要增长时,新的容量按 如下公式获得:新容量=(旧容量*3)/2+1,也就是说每一次容量大概会增长50%。这就意味着,如果你有一个包含大量元素的ArrayList对象, 那么最终将有很大的空间会被浪费掉,这个浪费是由ArrayList的工作方式本身造成的。如果没有足够的空间来存放新的元素,数组将不得不被重新进行分 配以便能够增加新的元素。对数组进行重新分配,将会导致性能急剧下降。如果我们知道一个ArrayList将会有多少个元素,我们可以通过构造方法来指定 容量。我们还可以通过trimToSize方法在ArrayList分配完毕之后去掉浪费掉的空间。


    三.总结
    ArrayList和LinkedList在性能上各 有优缺点,都有各自所适用的地方,总的说来可以描述如下:
    1.对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对 ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是 统一的,分配一个内部Entry对象。


    2.在ArrayList的 中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。


    3.LinkedList不 支持高效的随机元素访问。


    4.ArrayList的空 间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间

    5.当需要插入数据的时候,如果是在集合的前段(大概集合容量的前1/10)处插入数据时,linkedlist性能明显优于arraylist,但 是!!!!当在集合的中部甚至靠后的位置插入大量数据时,arraylist的性能反而远远优于linkedlist,所以,linkedlist相较于 arraylist的唯一优势在于集合前段的数据的插入。

    可以这样说:当操作是在一列 数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;当你的操作是在一列数据的前面或中 间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。

    所以,如果只是查找特定位置的元素或只在集合的末端增加、移除元素,那么使用Vector或ArrayList都可以。如果是对其它指定位置的插入、删除操作,最好选择LinkedList。


    List

    1、List是一个接口,不能实例化,需要实例化一个ArrayList或者LinkedList

       List myList = new ArrayList();

    2、List中可以添加任何对象,包括自己定义的新的类。

    3、使用myList.add(任何对象);就可以进行添加了。

    4、取值的时候myList.get(索引);取出来的值都是Object,使用时需要类型转换。

    5、可用Iterator迭代器对List中的元素进行迭代操作。List 集合中的对象按照一定的顺序排放,里面的内容可以重复。

    List接口实现的类:ArrayList(实现动态数组), Vector(实现动态数组) ,LinkedList(实现链表), Stack(实现堆栈)

    ArrayList

     

    1.如何使用ArrayList

        最简单的例子:

    ArrayList List = new ArrayList();

    for( int i=0;i<10;i++ ) //给数组增加10个Int元素

    List.Add(i);

    //..程序做一些处理

    List.RemoveAt(5);//将第6个元素移除

    for( int i=0;i<3;i++ ) //再增加3个元素

      List.Add(i+20);

    Int32[] values = (Int32[])List.ToArray(typeof(Int32));//返回ArrayList包含的数组

    这是一个简单的例子,虽然没有包含ArrayList所有的方法,但是可以反映出ArrayList最常用的用法

    2、ArrayList重要的方法和属性

    (1)构造器

        ArrayList提供了三个构造器:

    public ArrayList();

    默认的构造器,将会以默认(16)的大小来初始化内部的数组

    public ArrayList(ICollection);

    用一个ICollection对象来构造,并将该集合的元素添加到ArrayList

    public ArrayList(int);

    用指定的大小来初始化内部的数组

    (2)IsSynchronized属性和ArrayList.Synchronized方法

        IsSynchronized属性指示当前的ArrayList实例是否支持线程同步,而ArrayList.Synchronized静态方法则会返回一个ArrayList的线程同步的封装。

        如果使用非线程同步的实例,那么在多线程访问的时候,需要自己手动调用lock来保持线程同步,例如:

    ArrayList list = new ArrayList();

    //...

    lock( list.SyncRoot ) //当ArrayList为非线程包装的时候,SyncRoot属性其实就是它自己,但是为了满足ICollection的SyncRoot定义,这里还是使用SyncRoot来保持源代码的规范性

    {

    list.Add( “Add a Item” );

    }

         如果使用ArrayList.Synchronized方法返回的实例,那么就不用考虑线程同步的问题,这个实例本身就是线程安全的,实际上 ArrayList内部实现了一个保证线程同步的内部类,ArrayList.Synchronized返回的就是这个类的实例,它里面的每个属性都是用 了lock关键字来保证线程同步。

    ****

    但是,使用这个方法(ArrayList.Synchronized)并不能保证枚举的同步,例如,一个线程正在删除或添加集合项,而另一个线程同时进行枚举,这时枚举将会抛出异常。所以,在枚举的时候,你必须明确使用 SyncRoot 锁定这个集合。

    Hashtable与ArrayList关于线程安全性的使用方法类似。

    ****

    (3)Count属性和Capacity属性

        Count属性是目前ArrayList包含的元素的数量,这个属性是只读的。

    Capacity属性是目前ArrayList能够包含的最大数量,可以手动的设置这个属性,但是当设置为小于Count值的时候会引发一个异常。

    (4)Add、AddRange、Remove、RemoveAt、RemoveRange、Insert、InsertRange

        这几个方法比较类似

    Add方法用于添加一个元素到当前列表的末尾

    AddRange方法用于添加一批元素到当前列表的末尾

    Remove方法用于删除一个元素,通过元素本身的引用来删除

    RemoveAt方法用于删除一个元素,通过索引值来删除

    RemoveRange用于删除一批元素,通过指定开始的索引和删除的数量来删除

    Insert用于添加一个元素到指定位置,列表后面的元素依次往后移动

    InsertRange用于从指定位置开始添加一批元素,列表后面的元素依次往后移动

        另外,还有几个类似的方法:

    Clear方法用于清除现有所有的元素

    Contains方法用来查找某个对象在不在列表之中

        其他的我就不一一累赘了,大家可以查看MSDN,上面讲的更仔细

    (5)TrimSize方法

        这个方法用于将ArrayList固定到实际元素的大小,当动态数组元素确定不在添加的时候,可以调用这个方法来释放空余的内存。

    (6)ToArray方法

        这个方法把ArrayList的元素Copy到一个新的数组中。

    3、ArrayList与数组转换

        例1:

    ArrayList List = new ArrayList();

    List.Add(1);

    List.Add(2);

    List.Add(3);

    Int32[] values = (Int32[])List.ToArray(typeof(Int32));

        例2:

    ArrayList List = new ArrayList();

    List.Add(1);

    List.Add(2);

    List.Add(3);

    Int32[] values = new Int32[List.Count];

    List.CopyTo(values);

        上面介绍了两种从ArrayList转换到数组的方法

        例3:

    ArrayList List = new ArrayList();

    List.Add( “string” );

    List.Add( 1 );

    //往数组中添加不同类型的元素

    object[] values = List.ToArray(typeof(object)); //正确

    string[] values = (string[])List.ToArray(typeof(string)); //错误

    和数组不一样,因为可以转换为Object数组,所以往ArrayList里面添加不同类型的元素是不会出错的,但是当调用ArrayList方法的时候,要么传递所有元素都可以正确转型的类型或者Object类型,否则将会抛出无法转型的异常。

    LinkedLis

     

     LinkedList类扩展AbstractSequentialList并执行List接口。它提供了一个链接列表数据结构。它具有如下的两个构造函数,说明如下

    – LinkedList( )

    – LinkedList(Collection c)

    – 第一个构造函数建立一个空的链接列表。

    – 第二个构造函数建立一个链接列表,该链接列表由类集c中的元素初始化

    LinkedList类的一些方法public boolean add(Object element) 向链表的末尾添加一个新的节点对象element

    public void add(int index,Object element)向链表的指定位置尾添加一个新的节点对象element

    public void addFirst(Object element)把节点对象element添加到链表的表头

    public void addLast(Object element)把节点对象element添加到链表的末尾

    public void clear()删除链表的所有节点对象

    public Object remove(int index)删除指定位置上的节点对象

    public boolean remove(Object element)将首次出现的节点对象element删除

    public Obiect removeFirst() 删除第一个节点对象,并返回这个节点对象.

    public Object removeLast() 删除最后一个节点对象

    public Object get(int index)得到链表中指定位置上的节点对象

    public Object getFirst()得到链表中的第一个节点对象

    public Object getLast()得到链表中的最后一个节点对象

    public int indexOf(Object element) 返回节点对象element,在链表中首次出现的位置,如果链表中无此节点,则返回-1

    public int lastIndexOf(Object element) 返回节点对象element,在链表中最后出现的位置,如果链表中无此节点,则返回-1

    public Object set(int index ,Object element) 用节点对象element替换指定位置的节点对象,并返回链表中先前位置处的节点对象

    public int size()返回链表的长度,即节点的个数

    public boolean contains(Object element) 判断链表节点对象中是否含有element

    LinkedList是采用双向循环链表实现的。利用LinkedList实现栈(stack)、队列(queue)、双向队列(double-ended queue )。

    Vector

    ArrayList会比Vector快,他是非同步的,如果设计涉及到多线程,还是用Vector比较好一些
    import java.util.*;

    /**
    * 演示Vector的使用。包括Vector的创建、向Vector中添加元素、从Vector中删除元素、
    * 统计Vector中元素的个数和遍历Vector中的元素。
    */

    public class VectorDemo{
    public static void main(String[] args){

    //Vector的创建
    //使用Vector的构造方法进行创建
    Vector v = new Vector(4);

    //向Vector中添加元素
    //使用add方法直接添加元素
    v.add("Test0");
    v.add("Test1");
    v.add("Test0");
    v.add("Test2");
    v.add("Test2");

    //从Vector中删除元素
    v.remove("Test0"); //删除指定内容的元素
    v.remove(0); //按照索引号删除元素

    //获得Vector中已有元素的个数
    int size = v.size();
    System.out.println("size:" + size);

    //遍历Vector中的元素
    for(int i = 0;i < v.size();i++){
    System.out.println(v.get(i));
    }
    }
    }
    -------------
    Vector 类提供了实现可增长数组的功能,随着更多元素加入其中,数组变的更大。在删除一些元素之后,数组变小。
    Vector 有三个构造函数,
    public Vector(int initialCapacity,int capacityIncrement)          public Vector(int initialCapacity)          public Vector()   Vector 运行时创建一个初始的存储容量initialCapacity,存储容量是以capacityIncrement 变量定义的增量增长。初始的存储容量和capacityIncrement 可以在Vector 的构造函数中定义。第二个构造函数只创建初始存储容量。第三个构造函数既不指定初始的存储容量也不指定capacityIncrement。   Vector 类提供的访问方法支持类似数组运算和与Vector 大小相关的运算。类似数组的运算允许向量中增加,删除和插入元素。它们也允许测试矢量的内容和检索指定的元素,与大小相关的运算允许判定字节大小和矢量中 元素不数目。   现针对经常用到的对向量增,删,插功能举例描述:
    addElement(Object obj)     把组件加到向量尾部,同时大小加1,向量容量比以前大1  
    insertElementAt(Object obj, int index)     把组件加到所定索引处,此后的内容向后移动1 个单位  
    setElementAt(Object obj, int index)   把组件加到所定索引处,此处的内容被代替。   removeElement(Object obj) 把向量中含有本组件内容移走。   removeAllElements() 把向量中所有组件移走,向量大小为0。    

    同步是个很大的问题,尤其多线程,和进程中,因此,我们在多线程中同时对某个数组操作时,支持同步的vector无疑是个很好的选择,一般在需要将多个元素存在一个集合里的时候用。
    java.util 类 Vector<E>
    boolean add(E o)
    将指定元素追加到此向量的末尾。
    void add(int index, E element)
    在此向量的指定位置插入指定的元素。
    boolean addAll(Collection<? extends E> c)
    将指定 Collection 中的所有元素追加到此向量的末尾,按照指定集合的迭代器所返回的顺序追加这些元素。
    boolean addAll(int index, Collection<? extends E> c)
    在指定位置将指定 Collection 中的所有元素插入到此向量中。
    void addElement(E obj)
    将指定的组件添加到此向量的末尾,将其大小增加 1。
    int capacity()
    返回此向量的当前容量。
    void clear()
    从此向量中移除所有元素。
    Object clone()
    返回向量的一个副本。
    boolean contains(Object elem)
    测试指定的对象是否为此向量中的组件。
    boolean containsAll(Collection<?> c)
    如果此向量包含指定 Collection 中的所有元素,则返回 true。
    void copyInto(Object[] anArray)
    将此向量的组件复制到指定的数组中。
    E elementAt(int index)
    返回指定索引处的组件。
    Enumeration<E> elements()
    返回此向量的组件的枚举。
    void ensureCapacity(int minCapacity)
    增加此向量的容量(如有必要),以确保其至少能够保存最小容量参数指定的组件数。
    boolean equals(Object o)
    比较指定对象与此向量的相等性。
    E firstElement()
    返回此向量的第一个组件(位于索引 0 处的项)。
    E get(int index)
    返回向量中指定位置的元素。
    int hashCode()
    返回此向量的哈希码值。
    int indexOf(Object elem)
    搜索给定参数的第一个匹配项,使用 equals 方法测试相等性。
    int indexOf(Object elem, int index)
    搜索给定参数的第一个匹配项,从 index 处开始搜索,并使用 equals 方法测试其相等性。
    void insertElementAt(E obj, int index)
    将指定对象作为此向量中的组件插入到指定的 index 处。
    boolean isEmpty()
    测试此向量是否不包含组件。
    E lastElement()
    返回此向量的最后一个组件。
    int lastIndexOf(Object elem)
    返回指定的对象在此向量中最后一个匹配项的索引。
    int lastIndexOf(Object elem, int index)
    向后搜索指定的对象,从指定的索引处开始搜索,并返回一个索引。
    E remove(int index)
    移除此向量中指定位置的元素。
    boolean remove(Object o)
    移除此向量中指定元素的第一个匹配项,如果向量不包含该元素,则元素保持不变。
    boolean removeAll(Collection<?> c)
    从此向量中移除包含在指定 Collection 中的所有元素。
    void removeAllElements()
    从此向量中移除全部组件,并将其大小设置为零。
    boolean removeElement(Object obj)
    从此向量中移除变量的第一个(索引最小的)匹配项。
    void removeElementAt(int index)
    删除指定索引处的组件。
    protected void removeRange(int fromIndex, int toIndex)
    从此 List 中移除其索引位于 fromIndex(包括)与 toIndex(不包括)之间的所有元素。
    boolean retainAll(Collection<?> c)
    在此向量中仅保留包含在指定 Collection 中的元素。
    E set(int index, E element)
    用指定的元素替换此向量中指定位置处的元素。
    void setElementAt(E obj, int index)
    将此向量指定 index 处的组件设置为指定的对象。
    void setSize(int newSize)
    设置此向量的大小。
    int size()
    返回此向量中的组件数。
    List<E> subList(int fromIndex, int toIndex)
    返回此 List 的部分视图,元素范围为从 fromIndex(包括)到 toIndex(不包括)。
    Object[] toArray()
    返回一个数组,包含此向量中以正确顺序存放的所有元素。
    <T> T[]
    toArray(T[] a)
    返回一个数组,包含此向量中以正确顺序存放的所有元素;返回数组的运行时类型为指定数组的类型。
    String toString()
    返回此向量的字符串表示形式,其中包含每个元素的 String 表示形式。
    void trimToSize()
    对此向量的容量进行微调,使其等于向量的当前大小。

  • 相关阅读:
    设计模式(行为模式)之 观察者模式
    java 内存区域 (程序计数器)
    java 枚举的应用
    ServiceLoad 应用(链式编程:统一执行某一类功能、、分支语句优化)
    python前端学习之css
    python学习四十四天(前端之HTML标签)
    python学习四十三天(网络IO模型)
    python学习四十二天(协程)
    python学习四十一天(线程)
    python学习四十天(进程池)
  • 原文地址:https://www.cnblogs.com/wzyxidian/p/4415686.html
Copyright © 2020-2023  润新知