集合类源码(二)Collection之List(ArrayList, LinkedList, Vector)

ArrayList

功能

完全命名

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

简介

实现List接口的可调整大小的数组,这是个非线程安全的实现,如果需要线程安全,可以使用 List list = Collections.synchronizedList(new ArrayList(...)); 来包装。

方法

// 将ArrayList实例的容量调整为列表的当前大小。应用程序可以使用此操作最小化ArrayList实例的存储
public void trimToSize()

// 增加这个ArrayList实例的容量
public void ensureCapacity(int minCapacity)

// 返回列表中元素的数目。
public int size()

// 如果此列表不包含任何元素,则返回true。
public boolean isEmpty()

// 如果此列表包含至少一个指定的元素,则返回true。
public boolean contains(Object o)

// 返回该列表中指定元素第一次出现的索引,如果该列表不包含该元素,则返回-1。
public int indexOf(Object o)

// 返回此列表中指定元素的最后一次出现的索引,如果此列表不包含该元素,则返回-1。
public int lastIndexOf(Object o)

// 返回此ArrayList实例的浅拷贝。
public Object clone()

// 返回一个数组,该数组按适当的顺序(从第一个元素到最后一个元素)包含列表中的所有元素。返回的数组将是“安全的”,因为这个列表不维护对它的引用。(换句话说,这个方法分配一个新数组)。因此,调用者可以自由地修改返回的数组。
public Object[] toArray()

// 返回一个数组,该数组按适当的顺序包含列表中的所有元素(从第一个元素到最后一个元素);返回数组的运行时类型是指定数组的运行时类型。
public <T> T[] toArray(T[] a)

// 返回列表中指定位置的元素。
public E get(int index)

// 将列表中指定位置的元素替换为指定元素。
public E set(int index, E element)

// 将指定的元素追加到此列表的末尾。
public boolean add(E e)

// 将指定元素插入到列表中的指定位置,然后将当前位于该位置的元素和任何后续元素向右移动。
public void add(int index, E element)

// 删除列表中指定位置的元素,然后将任何后续元素向左移动
public E remove(int index)

// 从该列表中删除指定元素的第一个匹配项(如果存在)。如果列表不包含该元素,它将保持不变。
public boolean remove(Object o)

// 从列表中删除所有元素。该调用返回后,列表将为空。
public void clear()

// 将指定集合中的所有元素按照顺序追加到此列表的末尾。
public boolean addAll(Collection<? extends E> c)

// 从指定位置开始,将指定集合中的所有元素插入此列表。指定位置的元素以及后续元素都将向后移动。
public boolean addAll(int index, Collection<? extends E> c)

// 从该列表中删除其索引在fromIndex(包含)和toIndex(不包含)之间的所有元素。任何后续元素向左移动。
protected void removeRange(int fromIndex, int toIndex)

// 从此列表中移除指定集合中包含的所有元素。
public boolean removeAll(Collection<?> c)

// 仅保留此列表中包含在指定集合中的元素。换句话说,从这个列表中删除指定集合中不包含的所有元素。
public boolean retainAll(Collection<?> c)

// 从列表中的指定位置开始,返回此列表中元素的列表迭代器(按适当的顺序)。
public ListIterator<E> listIterator(int index)

// 返回此列表中元素的列表迭代器
public ListIterator<E> listIterator()

// 按适当的顺序对列表中的元素返回一个迭代器。
public Iterator<E> iterator()

// 返回指定的fromIndex(包含)和toIndex(不包含)之间的列表视图。习惯用法:从列表删除一部分元素( list.subList(from, to).clear(); )
public List<E> subList(int fromIndex, int toIndex)

// 为可迭代的每个元素执行给定的操作,直到处理完所有元素或操作引发异常。
public void forEach(Consumer<? super E> action)

// 在此列表中的元素上创建延迟绑定和快速故障的 Spliterator。
public Spliterator<E> spliterator()

// 删除此集合中满足给定谓词的所有元素。迭代期间或由谓词抛出的错误或运行时异常将传递给调用者。
public boolean removeIf(Predicate<? super E> filter)

// 将此列表中的每个元素替换为将操作符应用于该元素的结果。操作符抛出的错误或运行时异常将传递给调用者。
public void replaceAll(UnaryOperator<E> operator)

// 根据制定的规则对列表进行排序
public void sort(Comparator<? super E> c)

原理

添加

public boolean add(E e) {
    // 确保能容纳新元素
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 确保无误后,插入数组最后的位置。这里的size既起到了下标的作用,也起到了记录元素数量的作用。
    elementData[size++] = e;
    return true;
}
// 这里的minCapacity实际上是预期需要的最小容量。
private void ensureCapacityInternal(int minCapacity) {
    // 先去计算容量(如果此时数组为空,则在默认容量大小和minCapacity之间选个最大的;否则直接返回minCapacity)
    // 然后进入ensureExplicitCapacity方法
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
    // 这个列表在结构上被修改的次数 +1
    modCount++;

    // 如果需要的容量大于当前容量,则执行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 新容量 = 老容量 + 老容量/2(左移操作,等价于oldCapacity/2^1)。也就是说,新容量是老容量的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果新容量小于需要的最小容量,新容量等于需要的最小容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果新容量大于数组最大容量(MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8),则针对大容量做一个处理
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        // 如果minCapacity为负数,抛出内存溢出异常;否则minCapacity = Integer.MAX_VALUE
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    // minCapacity通常最接近实际大小,这是个胜利,奥利给!!
    // 执行数组复制,根据newCapacity创建一个新数组,然后把老数据复制进来,返回新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

删除

public boolean remove(Object o) {
    if (o == null) {
        // 删除一个null值,遍历数组,找到第一个匹配的,删除返回true
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        // 删除一个非null值,遍历数组,找到第一个匹配的,删除返回true
        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++;
    // 删除要移动index位置之后的元素,这里计算需要移动元素的数量
    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
}

// 从指定的源阵列(从指定位置开始)复制阵列到目标阵列的指定位置。数组组件的子序列从src引用的源数组复制到dest引用的目标数组,复制的组件数量等于长度参数。
public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);

最后那个本地方法画图解释

contains

public boolean contains(Object o) {
    // 实际上调用的是indexOf
    return indexOf(o) >= 0;
}

public int indexOf(Object o) {
    if (o == null) {
        // 如果为null,遍历寻找第一个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;
}

ForEach

public void forEach(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    @SuppressWarnings("unchecked")
    final E[] elementData = (E[]) this.elementData;
    final int size = this.size;
    // forEach内部实际上也是for循环遍历,把每一个对象交给我们定义的处理方法处理
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        action.accept(elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

优缺点

优点:查询快,但是仅限于按照index查找,其它的比如contains,是遍历查找。

缺点:添加和删除慢,要移动元素。

LinkedList

功能

完全命名

public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, Serializable

简介

实现了List和Deque接口的双链表。这是非线程安全的,如果需要,可以使用 List list = Collections.synchronizedList(new LinkedList(...)); 来包装

方法

// 返回列表第一个元素
public E getFirst()

// 返回列表最后一个元素
public E getLast()

// 移除并返回列表第一个元素
public E removeFirst()

// 移除并返回列表最后一个元素
public E removeLast()

// 将指定的元素插入此列表的开头。
public void addFirst(E e)

// 将指定的元素追加到此列表的末尾。这个方法等价于add(E)。
public void addLast(E e)

// 如果此列表包含至少一个指定的元素,则返回true。
public boolean contains(Object o)

// 返回列表中元素的数目。
public int size()

// 将指定的元素追加到此列表的末尾。
public boolean add(E e)

// 从该列表中删除指定元素的第一个匹配项(如果存在)。如果列表不包含该元素,它将保持不变。
public boolean remove(Object o)

// 将指定集合中的所有元素按照顺序追加到此列表的末尾。
public boolean addAll(Collection<? extends E> c)

// 从指定位置开始,将指定集合中的所有元素插入此列表。
public boolean addAll(int index, Collection<? extends E> c)

// 从列表中删除所有元素。该调用返回后,列表将为空。
public void clear()

// 返回列表中指定位置的元素。
public E get(int index)

// 将列表中指定位置的元素替换为指定元素。
public E set(int index, E element)

// 将指定元素插入到列表中的指定位置。
public void add(int index, E element)

// 删除列表中指定位置的元素。
public E remove(int index)

// 返回该列表中指定元素第一次出现的索引,如果该列表不包含该元素,则返回-1。
public int indexOf(Object o)

// 返回此列表中指定元素的最后一次出现的索引,如果此列表不包含该元素,则返回-1。
public int lastIndexOf(Object o)

// 检索但不删除此列表的头(第一个元素)。
public E peek()

// 检索但不删除此列表的头(第一个元素)。
public E element()

// 检索并删除此列表的头(第一个元素)。
public E poll()

// 检索并删除此列表的头(第一个元素)。
public E remove()

// 将指定的元素添加为此列表的末尾(最后一个元素)。
public boolean offer(E e)

// 将指定的元素插入此列表的开头。
public boolean offerFirst(E e)

// 将指定的元素插入此列表的末尾。
public boolean offerLast(E e)

// 检索但不删除此列表的第一个元素,或在此列表为空时返回null。
public E peekFirst()

// 检索但不删除此列表的最后一个元素,或在此列表为空时返回null。
public E peekLast()

// 检索并删除此列表的第一个元素,如果该列表为空,则返回null。
public E pollFirst()

// 检索并删除此列表的最后一个元素,如果此列表为空,则返回null。
public E pollLast()

// 将元素推入此列表所表示的堆栈。换句话说,将元素插入到列表的前面。这个方法相当于addFirst(E)。
public void push(E e)

// 从该列表表示的堆栈中弹出一个元素。换句话说,删除并返回这个列表的第一个元素。这个方法相当于removeFirst()。
public E pop()

// 删除此列表中指定元素的第一个匹配项(在从头到尾遍历列表时)。如果列表不包含该元素,它将保持不变。
public boolean removeFirstOccurrence(Object o)

// 删除此列表中指定元素的最后一次出现(在从头到尾遍历列表时)。如果列表不包含该元素,它将保持不变。
public boolean removeLastOccurrence(Object o)

// 从列表中的指定位置开始,返回此列表中元素的列表迭代器(按适当的顺序)。列表迭代器是【快速失败】的:如果列表在迭代器创建后的任何时候在结构上被修改(除了通过列表迭代器自己的删除或添加方法),列表迭代器将抛出ConcurrentModificationException。
public ListIterator<E> listIterator(int index)

// 以相反的顺序返回deque中元素的迭代器。元素将按从最后(尾)到第一个(头)的顺序返回。
public Iterator<E> descendingIterator()

// 返回该链表的浅拷贝。
public Object clone()

// 返回一个数组,该数组按适当的顺序(从第一个元素到最后一个元素)包含列表中的所有元素。返回的数组将是“安全的”,因为这个列表不维护对它的引用。(换句话说,这个方法分配一个新数组)。因此,调用者可以自由地修改返回的数组。
public Object[] toArray()

// 返回一个数组,该数组按适当的顺序包含列表中的所有元素(从第一个元素到最后一个元素);返回数组的运行时类型是指定数组的运行时类型。
public <T> T[] toArray(T[] a)

// 在此列表中的元素上创建延迟绑定和快速失败的Spliterator。
public Spliterator<E> spliterator()

原理

添加

public boolean add(E e) {
    // 插入到最后
    linkLast(e);
    return true;
}

void linkLast(E e) {
    // 获取尾指针
    final Node<E> l = last;
    // 创建新结点
    final Node<E> newNode = new Node<>(l, e, null);
    // 尾指针指向新结点
    last = newNode;
    if (l == null)
        // 列表第一个元素
        first = newNode;
    else
        // 添加到尾部
        l.next = newNode;
    // 列表大小 +1
    size++;
    // 列表结构更改次数 +1
    modCount++;
}

删除

public boolean remove(Object o) {
    if (o == null) {
        // 如果删除对象为null,遍历链表,寻找第一个为null的对象
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                // 解除
                unlink(x);
                return true;
            }
        }
    } else {
        // 否则,遍历链表,寻找第一个匹配的对象
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                // 解除
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

E unlink(Node<E> x) {
    // assert x != null;
    // 目标元素
    final E element = x.item;
    // 后继
    final Node<E> next = x.next;
    // 前驱
    final Node<E> prev = x.prev;

    if (prev == null) {
        // 当前元素是链表第一个元素,头指针直接指向当前元素后继。
        first = next;
    } else {
        // 当前元素的前驱的后继指向当前元素的后继
        prev.next = next;
        // 当前元素的前驱置null
        x.prev = null;
    }

    if (next == null) {
        // 当前元素是链表最后一个元素,尾指针直接指向当前元素前驱。
        last = prev;
    } else {
        // 当前元素后继的前驱指向当前元素的前驱
        next.prev = prev;
        // 当前元素的后继置null
        x.next = null;
    }

    // 数据置null,方便回收
    x.item = null;
    // 链表元素数量 -1
    size--;
    // 链表结构修改次数 +1
    modCount++;
    // 返回老数据
    return element;
}

实际上LinkedList还提供了push和pop,这就使得它可以发挥栈的作用,原理无非是在链表头部插入和删除,这不在赘述。

优缺点

优点:添加和删除效率比较高。只需要更改引用指向即可。

缺点:查找是从头遍历链表,最好情况第一个就是,最坏匹配到最后一个。不过如果仅限于首尾,只需要调用提供的getFirst和getLast等等方法

Vector

功能

完全命名

public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable

简介

Vector类实现了一个可增长的对象数组,它是线程安全的,如果不需要线程安全的实现,建议使用ArrayList代替Vector。

方法

// 将此Vector的组件复制到指定的数组中。这个Vector中下标k的项被复制到anArray的下标k中。
public void copyInto(Object[] anArray)

// 将该向量的容量修剪为该向量的当前大小。如果该向量的容量大于其当前大小,则通过将其保存在字段elementData中的内部数据数组替换为一个更小的数组,将容量(Capacity)更改为size。
public void trimToSize()

// 增加这个向量的容量
public void ensureCapacity(int minCapacity)

// 设置这个向量的大小。如果新大小大于当前大小,则在向量的末尾添加新的空项。如果新大小小于当前大小,则额外的所有组件都将被丢弃。
public void setSize(int newSize)

// 返回此向量的当前容量。
public int capacity()

// 返回此向量中组件的数量
public int size()

// 如果无元素,则返回true
public boolean isEmpty()

// 返回此向量中组件的枚举。返回的枚举对象将生成此向量中的所有项。生成的第一个项是索引0处的项,然后是索引1处的项,以此类推。
public Enumeration<E> elements()

// 如果该向量包含至少一个指定的元素,则返回true。
public boolean contains(Object o)

// 返回该向量中指定元素第一次出现的索引,如果该向量不包含该元素,则返回-1。
public int indexOf(Object o)

// 返回该向量中指定元素第一次出现时的索引(从指定索引向前搜索),如果没有找到该元素,则返回-1。
public int indexOf(Object o, int index)

// 返回该向量中指定元素的最后一次出现的索引,如果该向量不包含该元素,则返回-1。
public int lastIndexOf(Object o)

// 返回此向量中指定元素的最后一次出现的索引(从指定索引向后搜索),如果没有找到该元素,则返回-1。
public int lastIndexOf(Object o, int index)

// 返回指定索引处的组件。此方法在功能上与get(int index)方法相同(后者是List接口的一部分)。
public E elementAt(int index)

// 返回此向量的第一个组件(索引为0的项)。
public E firstElement()

// 返回向量的最后一个组件。
public E lastElement()

// 将此向量的指定索引处的组件设置为指定对象。索引的值必须大于或等于0,并且小于向量的当前大小。此方法在功能上与set(int, E)方法(它是List接口的一部分)相同。注意,set方法颠倒了参数的顺序,以便更接近数组的用法。set方法返回存储在指定位置的旧值。
public void setElementAt(E obj, int index)

// 删除指定索引处的组件。索引的值必须大于或等于0,并且小于向量的当前大小。该方法在功能上与remove(int)方法相同(后者是List接口的一部分)。注意,remove方法返回存储在指定位置的旧值。
public void removeElementAt(int index)

// 在指定索引处将指定对象作为此向量中的组件插入。这个向量中索引大于或等于指定索引的每个分量向上移动,使索引大于先前的值。索引的值必须大于或等于0,并且小于或等于向量的当前大小。(如果索引等于向量的当前大小,则将新元素追加到向量。)此方法在功能上与add(int, E)方法(List接口的一部分)相同。注意,add方法颠倒了参数的顺序,以便更接近于匹配数组的使用。
public void insertElementAt(E obj, int index)

// 将指定的组件添加到该向量的末尾,将其大小增加1。此方法在功能上与add(E)方法(List接口的一部分)相同。
public void addElement(E obj)

// 从这个向量中移除第一个匹配到的元素。此方法在功能上与remove(Object)方法(List接口的一部分)相同。
public boolean removeElement(Object obj)

// 从这个向量中移除所有的分量,并将其大小设置为0。此方法在功能上与clear()方法(List接口的一部分)相同。
public void removeAllElements()

// 返回此向量的克隆。副本将包含对内部数据数组的克隆的引用,而不是对这个向量对象的原始内部数据数组的引用。
public Object clone()

// 返回一个数组,该数组顺序包含这个向量中的所有元素。
public Object[] toArray()

// 返回一个数组,该数组顺序包含这个向量中的所有元素。返回数组的运行时类型是指定数组的运行时类型
public <T> T[] toArray(T[] a)

// 返回该向量中指定位置的元素。
public E get(int index)

// 将此向量中指定位置的元素替换为指定元素。
public E set(int index, E element)

// 将指定的元素附加到此向量的末尾。
public boolean add(E e)

// 移除此向量中指定元素的第一个匹配项,如果该向量不包含该元素,则该元素将保持不变。
public boolean remove(Object o)

// 将指定元素插入到此向量的指定位置。
public void add(int index, E element)

// 移除此向量中指定位置的元素。
public E remove(int index)

// 从这个向量中移除所有的元素。
public void clear()

// 如果该向量包含指定集合中的所有元素,则返回true。
public boolean containsAll(Collection<?> c)

// 将指定集合中的所有元素按照指定集合的迭代器返回的顺序追加到此向量的末尾。
public boolean addAll(Collection<? extends E> c)

// 从该向量中移除指定集合中包含的所有元素。
public boolean removeAll(Collection<?> c)

// 仅保留此向量中包含在指定集合中的元素。换句话说,从这个向量中删除指定集合中不包含的所有元素。
public boolean retainAll(Collection<?> c)

// 将指定集合中的所有元素插入到此向量的指定位置。
public boolean addAll(int index, Collection<? extends E> c)

// 将指定的对象与此向量进行比较以确定是否相等。当且仅当指定的对象也是一个列表时,才返回true,两个列表的大小相同,且两个列表中所有对应的元素对都是相等的。
public boolean equals(Object o)

// 返回此向量的哈希码值
public int hashCode()

// 返回此向量的字符串表示形式,其中包含每个元素的字符串表示形式。
public String toString()

// 返回该列表中fromIndex(包含)和toIndex(不包含)之间的元素。
public List<E> subList(int fromIndex, int toIndex)

// 删除该列表中fromIndex(包含)和toIndex(不包含)之间的元素。
protected void removeRange(int fromIndex, int toIndex)

// 从列表中的指定位置开始,返回此列表中元素的列表迭代器(按适当的顺序)。
public ListIterator<E> listIterator(int index)

// 返回该列表中元素的列表迭代器(按适当的顺序)。
public ListIterator<E> listIterator()

// 按适当的顺序对列表中的元素返回一个迭代器。
public Iterator<E> iterator()

// 为可迭代的每个元素执行给定的操作,直到处理完所有元素或操作引发异常。
public void forEach(Consumer<? super E> action)

// 删除此集合中满足给定谓词的所有元素。迭代期间或由谓词抛出的错误或运行时异常将传递给调用者。
public boolean removeIf(Predicate<? super E> filter)

// 将此列表中的每个元素替换为将操作符应用于该元素的结果。操作符抛出的错误或运行时异常将传递给调用者。
public void replaceAll(UnaryOperator<E> operator)

// 根据比较器产生的顺序对这个列表进行排序。
public void sort(Comparator<? super E> c)

// 在此列表中的元素上创建延迟绑定和快速失败的Spliterator。
public Spliterator<E> spliterator()

原理

添加

// 与ArrayList唯一的区别就是加了个同步关键字,以保证线程安全,内部逻辑一样
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

private void ensureCapacityHelper(int minCapacity) {
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

public boolean remove(Object o) {
    return removeElement(o);
}

删除

// 与ArrayList唯一的区别就是加了个同步关键字,以保证线程安全,内部逻辑一样
public synchronized boolean removeElement(Object obj) {
    modCount++;
    int i = indexOf(obj);
    if (i >= 0) {
        removeElementAt(i);
        return true;
    }
    return false;
}

public synchronized void removeElementAt(int index) {
    modCount++;
    if (index >= elementCount) {
        throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                 elementCount);
    }
    else if (index < 0) {
        throw new ArrayIndexOutOfBoundsException(index);
    }
    int j = elementCount - index - 1;
    if (j > 0) {
        System.arraycopy(elementData, index + 1, elementData, index, j);
    }
    elementCount--;
    elementData[elementCount] = null; /* to let gc do its work */
}

优缺点

优缺点和ArrayList大致一样,唯一的不同就是Vector是线程安全的,但是加了同步方法,也就降低了效率,如果没有线程安全问题,直接使用ArrayList即可。

猜你喜欢

转载自www.cnblogs.com/LUA123/p/11928803.html