您的位置:首页 > 编程语言 > Java开发

JAVA提高十:ArrayList 深入分析

2017-10-31 08:27 489 查看
前面一章节,我们介绍了集合的类图,那么本节将学习Collection 接口中最常用的子类ArrayList类,本章分为下面几部分讲解(说明本章采用的JDK1.6源码进行分析,因为个人认为虽然JDK1.8进行了部分改动,但万变不离其宗,仍然采用的JDK1.6的引子进行的优化,因此学会了1.6对于1.8也就理解了)。

一、ArrayList 的常见功能

在分析ArrayList的源码前,我们先看下ArrayList的常见的功能:

package study.collection;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class TestDemo01
{
public static void main(String[] args)
{
List list = new ArrayList();
//ArrayList:底层实现时数组,线程不安全,效率高。所以,查询快。修改、插入、删除慢。
//LinkedList:底层实现是链表,线程不安全,效率高。所以,查询慢。修改、插入、删除快。
//Vector:线程安全的,效率低。

list.add("aaa");
list.add("aaa");
list.add(new Date());
list.add(new Dog());
list.add(1234);  //注意,list集合中只能添加引用类型,这里包装类的:自动装箱!
list.remove(new String("aaa"));
System.out.println(list.size());
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}

list.set(3, new String("3333"));
list.add(4, new String("3333"));

System.out.println(list.isEmpty());
list.remove(new Dog());      //hashcode和equals
System.out.println(list.size());

List list2 = new ArrayList();
list2.add("bbb");
list2.add("ccc");

list.add(list2);

//跟顺序的操作
String str = (String) list.get(0);
System.out.println(str);
list.set(1, "ababa");
list.remove(0);
}

}

class Dog
{
}


从上述可以看到了,ArrayList 接口中除了继承自父类Collection 接口中的方法外,还实现了List接口中扩充的和索引相关的方法,这都源于其底层为数组结构。

二、ArrayList 的重要的属性

上面的部分列举了ArrayList中常见的一些功能方法,那么这些方法又是如何使用的呢,下面我们将进行源码的剖析,在剖析前,我们可以自己思考下,我们知道ArrayList 是一个动态扩展的集合,之所以动态扩展的原因或者说比数组强的地方肯定就在于数组的长度是固定的,不能扩展,这是数组的最大缺陷,所以才有了集合,那么ArrayList,那么其底层肯定也采用的是数组结构,因为它叫ArrayList嘛,那么其重要的属性之一,必然是定义了一个数组。如下:

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;

/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer.
*/
private transient Object[] elementData;

/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;


ArrayList就是一个以数组形式实现的集合,其元素的功能为:

private transient Object[] elementData; //ArrayList是基于数组的一个实现,elementData就是底层的数组

private int size; //ArrayList里面元素的个数,这里要注意一下,size是按照调用add、remove方法的次数进行自增或者自减的,所以add了一个null进入ArrayList,size也会加1

三、ArrayList 的构造方法分析

在分析完上面的属性后,我们紧接着来看下ArrayList的构造方法:

/**
*构造一个具有指定容量的list
*/
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}

/**
* 构造一个初始容量为10的list,也就说当我们经常采用new ArrayList()的时候,实际分配的大小就为10
*/
public ArrayList() {
this(10);
}

/**
*构造一个包含指定元素的list,这些元素的是按照Collection的迭代器返回的顺序排列的
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652) 这里是因为toArray可能不一定是Object类型的,因此判断如果不是就进行了拷贝转换操作
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}


可以看到,ArrayList 提供了三个构造方法,分别的含义,已经注释到代码上面了,那么想一下指定容量的构造方法的意义,既然默认为10就可以那么为什么还要提供一个可以指定容量大小的构造方法呢?思考下,下面会说到。

四、ArrayList 的常见方法分析

1.add 添加元素



添加的方法,共有四个,下面我们分别分析下其功能源码:

/**
*添加一个元素
*/
public boolean add(E e)
{
// 进行扩容检查
ensureCapacity(size + 1);  // Increments modCount!!
//将e增加至list的数据尾部,容量+1
elementData[size++] = e;
return true;
}

/**
*在指定位置添加一个元素
*/
public void add(int index, E element) {
// 判断索引是否越界
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: "+size);
// 进行扩容检查
ensureCapacity(size+1);  // Increments modCount!!
// 对数组进行复制处理,目的就是空出index的位置插入element,并将index后的元素位移一个位置
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 将指定的index位置赋值为element
elementData[index] = element;
// list容量+1
size++;
}

/**
*增加一个集合元素
*/
public boolean addAll(Collection<? extends E> c) {
//将c转换为数组
Object[] a = c.toArray();
int numNew = a.length;
//扩容检查
ensureCapacity(size + numNew);  // Increments modCount
//将c添加至list的数据尾部
System.arraycopy(a, 0, elementData, size, numNew);
//更新当前容器大小
size += numNew;
return numNew != 0;
}

/**
* 在指定位置,增加一个集合元素
*/
public boolean addAll(int index, Collection<? extends E> c) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: " + index + ", Size: " + size);

Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew);  // Increments modCount

// 计算需要移动的长度(index之后的元素个数)
int numMoved = size - index;
// 数组复制,空出第index到index+numNum的位置,即将数组index后的元素向右移动numNum个位置
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
// 将要插入的集合元素复制到数组空出的位置中
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}

/**
* 数组容量检查,不够时则进行扩容
*/
public void ensureCapacity( int minCapacity) {
modCount++;
// 当前数组的长度
int oldCapacity = elementData.length;
// 最小需要的容量大于当前数组的长度则进行扩容
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
// 新扩容的数组长度为旧容量的1.5倍+1
int newCapacity = (oldCapacity * 3)/2 + 1;
// 如果新扩容的数组长度还是比最小需要的容量小,则以最小需要的容量为长度进行扩容
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity is usually close to size, so this is a win:
// 进行数据拷贝,Arrays.copyOf底层实现是System.arrayCopy()
elementData = Arrays.copyOf(elementData, newCapacity);
}
}


2.remove 删除

/**
* 根据索引位置删除元素
*/
public E remove(int index) {
// 数组越界检查
RangeCheck(index);

modCount++;
// 取出要删除位置的元素,供返回使用
E oldValue = (E) elementData[index];
// 计算数组要复制的数量
int numMoved = size - index - 1;
// 数组复制,就是将index之后的元素往前移动一个位置
if (numMoved > 0)
System. arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将数组最后一个元素置空(因为删除了一个元素,然后index后面的元素都向前移动了,所以最后一个就没用了),好让gc尽快回收
// 不要忘了size减一
elementData[--size ] = null; // Let gc do its work

return oldValue;
}

/**
* 根据元素内容删除,只删除匹配的第一个
*/
public boolean remove(Object o) {
// 对要删除的元素进行null判断
// 对数据元素进行遍历查找,知道找到第一个要删除的元素,删除后进行返回,如果要删除的元素正好是最后一个那就惨了,时间复杂度可达O(n) 。。。
if (o == null) {
for (int index = 0; index < size; index++)
// null值要用==比较
if (elementData [index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
// 非null当然是用equals比较了
if (o.equals(elementData [index])) {
fastRemove(index);
return true;
}
}
return false;
}

/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
modCount++;
// 原理和之前的add一样,还是进行数组复制,将index后的元素向前移动一个位置,不细解释了,
int numMoved = size - index - 1;
if (numMoved > 0)
System. arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size ] = null; // Let gc do its work
}

/**
* 数组越界检查
*/
private void RangeCheck(int index) {
if (index >= size )
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: " +size);
}


分析:

增加和删除方法到这里就解释完了,代码是很简单,主要需要特别关心的就两个地方:1.数组扩容,2.数组复制,这两个操作都是极费效率的,最惨的情况下(添加到list第一个位置,删除list最后一个元素或删除list第一个索引位置的元素)时间复杂度可达O(n)。

还记得上面那个坑吗(为什么提供一个可以指定容量大小的构造方法 )?看到这里是不是有点明白了呢,简单解释下:如果数组初试容量过小,假设默认的10个大小,而我们使用ArrayList的主要操作时增加元素,不断的增加,一直增加,不停的增加,会出现上面后果?那就是数组容量不断的受挑衅,数组需要不断的进行扩容,扩容的过程就是数组拷贝System.arraycopy的过程,每一次扩容就会开辟一块新的内存空间和数据的复制移动,这样势必对性能造成影响。那么在这种以写为主(写会扩容,删不会缩容)场景下,提前预知性的设置一个大容量,便可减少扩容的次数,提高了性能。

数组扩容伴随着开辟新建的内存空间以创建新数组然后进行数据复制,而数组复制不需要开辟新内存空间,只需将数据进行复制。

上面讲增加元素可能会进行扩容,而删除元素却不会进行缩容,如果在已删除为主的场景下使用list,一直不停的删除而很少进行增加,那么会出现什么情况?再或者数组进行一次大扩容后,我们后续只使用了几个空间,会出现上面情况?当然是空间浪费啦啦啦,怎么办呢?

/**
* 将底层数组的容量调整为当前实际元素的大小,来释放空间。
*/
public void trimToSize() {
modCount++;
// 当前数组的容量
int oldCapacity = elementData .length;
// 如果当前实际元素大小 小于 当前数组的容量,则进行缩容
if (size < oldCapacity) {
elementData = Arrays.copyOf( elementData, size );
}


3.更新 set

/**
* 将指定位置的元素更新为新元素
*/
public E set( int index, E element) {
// 数组越界检查
RangeCheck(index);

// 取出要更新位置的元素,供返回使用
E oldValue = (E) elementData[index];
// 将该位置赋值为行的元素
elementData[index] = element;
// 返回旧元素
return oldValue;
}


4.查找

/**
* 查找指定位置上的元素
*/
public E get( int index) {
RangeCheck(index);

return (E) elementData [index];
}


由于ArrayList使用数组实现,更新和查找直接基于下标操作,变得十分简单。

5.是否包含.

/**
* Returns <tt>true</tt> if this list contains the specified element.
* More formally, returns <tt>true</tt> if and only if this list contains
* at least one element <tt>e</tt> such that
* <tt>(o==null ? e==null : o.equals(e))</tt>.
*
* @param o element whose presence in this list is to be tested
* @return <tt> true</tt> if this list contains the specified element
*/
public boolean contains(Object o) {
return indexOf(o) >= 0;
}

/**
* Returns the index of the first occurrence of the specified element
* in this list, or -1 if this list does not contain the element.
* More formally, returns the lowest index <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>,
* or -1 if there is no such index.
*/
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;
}

/**
* Returns the index of the last occurrence of the specified element
* in this list, or -1 if this list does not contain the element.
* More formally, returns the highest index <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>,
* or -1 if there is no such index.
*/
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;
}


contains主要是检查indexOf,也就是元素在list中出现的索引位置也就是数组下标,再看indexOf和lastIndexOf代码是不是很熟悉,没错,和public booleanremove(Object o) 的代码一样,都是元素null判断,都是循环比较。

6.容量判断

/**
* Returns the number of elements in this list.
*
* @return the number of elements in this list
*/
public int size() {
return size ;
}

/**
* Returns <tt>true</tt> if this list contains no elements.
*
* @return <tt> true</tt> if this list contains no elements
*/
public boolean isEmpty() {
return size == 0;
}


说明:modCount 是干什么的,怎么到处都在操作这个变量,还有遍历呢,为啥不讲?由于iterator遍历相对比较复杂,而且iterator 是GoF经典设计模式比较重要的一个,之后会对iterator单独分析。

五、transient 分析 和 ArrayList的优缺点

1.ArrayList的优缺点

1、ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快

2、ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已

不过ArrayList的缺点也十分明显:

1、删除元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能

2、插入元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能

因此,ArrayList比较适合顺序添加、随机访问的场景

2.ArrayList和Vector的区别

ArrayList是线程非安全的,这很明显,因为ArrayList中所有的方法都不是同步的,在并发下一定会出现线程安全问题。那么我们想要使用ArrayList并且让它线程安全怎么办?一个方法是用Collections.synchronizedList方法把你的ArrayList变成一个线程安全的List,比如:

List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");
for (int i = 0; i < synchronizedList.size(); i++)
{
System.out.println(synchronizedList.get(i));
}


另一个方法就是Vector,它是ArrayList的线程安全版本,其实现90%和ArrayList都完全一样,区别在于:

1、Vector是线程安全的,ArrayList是线程非安全的

2、Vector可以指定增长因子,如果该增长因子指定了,那么扩容的时候会每次新的数组大小会在原数组的大小基础上加上增长因子;如果不指定增长因子,那么就给原数组大小

3.为什么ArrayList的elementData是用transient修饰的?

private transient Object[] elementData;


为什么elementData是使用transient修饰的呢?我们看一下ArrayList的定义:

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


看到ArrayList实现了Serializable接口,这意味着ArrayList是可以被序列化的,用transient修饰elementData意味着我不希望elementData数组被序列化。这是为什么?因为序列化ArrayList的时候,ArrayList里面的elementData未必是满的,比方说elementData有10的大小,但是我只用了其中的3个,那么是否有必要序列化整个elementData呢?显然没有这个必要,因此ArrayList中重写了writeObject方法:

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 array length
s.writeInt(elementData.length);
// 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();
}
}


每次序列化的时候调用这个方法,先调用defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData不去序列化它,然后遍历elementData,只序列化那些有的元素,这样:

1、加快了序列化的速度

2、减小了序列化之后的文件大小

六、自己编写一个MyArrayList

package study.collection;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

public class MyArrayList implements List {

//底层用一个数组接收
private Object[] elementData;

//用于记录集合的元素个数
private int size;

/**
* 无参数构造,默认为大小10
*/
public MyArrayList()
{
this(10);
}

/**
* 初始化带容量的构造方法
* @param cap
*/
public MyArrayList(int cap)
{
super();
if(cap < 0)
{
throw new IllegalArgumentException("Illegal cap: "+
cap);
}
elementData = new Object[cap];
}

@Override
public boolean add(Object e) {
//1.添加之前确认集合中的大小是够,因此扩容判断
ensureCapacity(size +1); //添加元素一个一个添加,因此size+1;
//2.填充元素
elementData[size++] = e;
return true;
}

/**
* 扩容判断,因为只要添加元素,就需要判断容器的大小是否满足
* @param i
*/
private void ensureCapacity(int minCapacity) {
//扩容前,需要获取当前的数组元素的大小
int oldCapacity = elementData.length;
//只有当前的容量不满足,则扩容处理
if(oldCapacity < minCapacity)
{
//新大小的比例,采用原来大小的1.5倍
int newCapacity = (oldCapacity * 3)/2 + 1;
//如果新算出来的大小不满足当前需要扩容的大小,则就以用户需要的为主,如果以满足则以算出来的最佳大小为主
if(newCapacity < minCapacity)
{
newCapacity = minCapacity;
}
//比例算好后,开始执行数组的拷贝操作
Arrays.copyOf(elementData, newCapacity,Object[].class);
}
}

@Override
public void add(int index, Object element) {
//添加指定位置,首先需要做的就是确保索引满足要求,如果要添加的索引超过了元素个数中的大小
if(index > size || index < 0)
{
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: "+size);
}
//如果没有超过,那么就需要开始添加元素,这个时候同样需要判断扩容
ensureCapacity(size +1); //添加元素一个一个添加,因此size+1;
//接着需要做的事情是需要将原来位于index 位置的元素,向后面移动
//首先判断,index的后面是否还有元素
int modNum = size - index;
if(modNum > 0)
{
System.arraycopy(elementData, index, elementData, index+1, size - index);
}
//如果没有元素或者已经拷贝完成,则直接在对应的索引处放置元素即可
elementData[index] = element;
//元素个数加+1
size++;
}

@Override
public boolean addAll(Collection c) {
//添加集合元素,首先需要将集合转换为数组,计算出数组的大小
Object[] a = c.toArray();
//计算出需要的长度
int numNew = a.length;
//开始扩容判断
ensureCapacity(size +numNew); //添加元素的个数为numNew
//开始数组拷贝
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}

@Override
public boolean addAll(int index, Collection c) {
//首先索引的正确性
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: " + index + ", Size: " + size);
//添加的元素集合转换为数组,计算要拷贝的长度,准备扩容
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew);  // Increments modCount
//因为是指定位置扩容,因此需要判断下index后面是否有元素
int numMoved = size - index;
//如果大于0,说明要先空出位置来给a数组
if(numMoved > 0)
{
System.arraycopy(elementData, index, elementData, index+1, size-index);
}
//空出为位置后,然后将集合的元素放到空出的位置上面
System.arraycopy(a, 0, elementData,index, numNew);
size += numNew;
return numNew != 0;
}

@Override
public void clear() {
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}

@Override
public boolean contains(Object o) {
return indexOf(o) >= 0;
}

@Override
public boolean containsAll(Collection c) {
//迭代器暂不去实现
return false;
}

@Override
public Object get(int index) {
//对于数组而言,根据索引获取元素非常简单,但需要先检查inde的合法性,避免越界
RangeCheck(index);
return elementData[index];
}

private void RangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: "+size);
}

@Override
public int indexOf(Object o) {
//循环遍历,找出元素,注意是equals比较
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;
}

@Override
public boolean isEmpty() {
return size == 0;
}

@Override
public Iterator iterator() {
//涉及迭代器,暂不去关注
return null;
}

@Override
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;

}

@Override
public ListIterator listIterator() {
//涉及迭代器,暂不去关注
return null;
}

@Override
public ListIterator listIterator(int index) {
//涉及迭代器,暂不去关注
return null;
}

@Override
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;
}

//找到指定的索引开始删除
private void fastRemove(int index) {
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null;

}

@Override
public Object remove(int index) {
//合法性检查
RangeCheck(index);
//取出原来老的元素,以便返回
Object oldValue = elementData[index];
//需要开始拷贝数组,因为删除了索引处的元素,那么则需要向前移动元素
//需要看后面有没有移动的元素,-1 是减去当前这个删除的元素
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);//从index+1 开始拷贝到 index 处
elementData[--size] = null; //元素个数减去一,同事最后一个位置置空,等待垃圾回收

return oldValue;
}

@Override
public boolean removeAll(Collection c) {
////涉及迭代器,暂不去关注
return false;
}

@Override
public boolean retainAll(Collection c) {
////涉及迭代器,暂不去关注
return false;
}

@Override
public Object set(int index, Object element) {
RangeCheck(index);

Object oldValue = elementData[index];
elementData[index] = element;
return oldValue;
}

@Override
public int size() {
return size;
}

@Override
public List subList(int fromIndex, int toIndex) {
////涉及迭代器,暂不去关注
return null;
}

@Override
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}

@Override
public Object[] toArray(Object[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}

//测试
public static void main(String[] args)
{
MyArrayList list = new MyArrayList();
list.add("333");
list.add("444");
list.add("5");
list.add("344433");
list.add("333");
list.add("333");
System.out.println(list.size());
//        System.out.println(list.get(6));
list.remove("444");
System.out.println(list.size());
}
}


说明:其他关于JDK1.6 和 1.7 和 1.8 的区别可以看:
http://blog.csdn.net/u011392897/article/details/57105709
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: