Java基础之(三十)Set集合

说明
Set集合,类似于一个罐子,程序依次把多个对象“丢进”Set集合,而Set集合通常不能记住元素的添加顺序。**Set集合与Collection基本相同,没有提供额外的方法。实际上Set就是Collection,只是行为略有不同(**Set不允许包含重复元素)。

Set集合不允许包含相同的元素,如果试图把两个相同的元素加入同一个Set集合中,则添加操作失败,add()方法返回false,且新元素不会被加入。

上面介绍的是Set集合的通用知识,因此完全适合后面介绍的HashSet、TreeSet和EnumSet三个实现类,只是三个实现类各有特色。

Set判断两个对象相同是使用equals方法,若返回true,则Set集合不会接受这两个对象。
下面是使用普通Set的示例程序:

public class TestSet
{
    public static void main(String[] args) 
    {
        Set books = new HashSet();
        //添加一个字符串对象
        books.add(new String("Struts2权威指南"));
        //再次添加一个字符串对象,
        //因为两个字符串对象通过equals方法比较相等,所以添加失败,返回false
        boolean result = books.add(new String("Struts2权威指南"));
        System.out.println(result);
        //下面输出看到集合只有一个元素
        System.out.println(books);  
    }
}

HashSet类

HashSet是Set接口的典型实现,大多数时候使用Set集合就是使用这个集合类。
HashSet按Hash算法来存储集合中的元素,因此具有很好地存取和查找性能。

HashSet具有以下特点:

  • 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。

  • HashSet不是同步的,如果多个线程访问一个HashSet,假设有两个或者两个以
    上的线程同时修改了HashSet集合时,必须通过代码来保证其同步。

  • 集合元素可以是null。

    当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据hashCode值来决定该对象在HashSet中的存储位置。如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等,HashSet将会把它们存储在不同的位置,依然可以添加成功。

也就是说,HashSet集合判断两个元素相等的标准就是两个对象通过equals()方法比较相等,并且两个对象的hashCode()返回值也相等。

//类A的equals()方法总返回true,但没有重写其hashCode()方法
class A
{
    public boolean equals(Object obj)
    {
        return true;
    }
}

//类B的hashCode()方法总是返回1,但没有重写其equals()方法
class B
{
    public int hashCode()
    {
        return 1;
    }
}

//类C的hashCode()方法总是返回2,且重写其equals()方法总是返回true
class C
{
    public int hashCode()
    {
        return 2;
    }
    public boolean equals(Object obj)
    {
        return true;
    }
}

public class HashSetTest {
    public static void main(String[] args) {
        HashSet books = new HashSet();
        //分别向books集合中添加两个A对象,两个B对象,两个C对象
        books.add(new A());
        books.add(new A());
        books.add(new B());
        books.add(new B());
        books.add(new C());
        books.add(new C());

        System.out.println(books);
    }
}

程序中向books集合中分别添加了两个A对象,两个B对象,两个C对象,其中C对象重写了equals()方法总是返回true,hashCode()总是返回2,这将导致HashSet把两个C对象当成同一个对象。运行结果:

[B@1,B@1,C@2,A@15db9742,A@6d06d69c]

从上面程序可以看出,即使两个A对象通过equals()返回true,但HashSet依然把它们当成不同的两个对象;即使两个B对象的hashCode()返回相同值1,但HashSet仍把它们当成两个对象。

这里有个注意点:当把一个对象放入HashSet时,如果需要重写该对应类的equals()方法,则也应该重写其hashCode()方法。

规则是:如果两个对象通过equals()方法比较返回true,这两个对象的hashCode值也应该相同。

如果两个对象通过equals()方法比较返回true,但这两个对象的hashCode()方法返回不同的hashCode时,这将导致HashSet会把这两个对象存在不同的位置,从而使两个对象都可以添加成功,这就与Set的规则冲突了。

如果两个对象的hashCode()方法返回hashCode值相同,但它们通过equals()方法返回false时将更麻烦:因为两个对象的hashCode值相同,HashSet试图向把它们保存在同一位置,但又不行(否则将只剩下一个对象),所以实际上会在这个位置使用链式结构来保存多个对象;而HashSet访问集合元素时也是根据元素的hashCode值来快速定位的,如果HashSet中两个以上的元素具有相同的HashCode值,将会导致性能下降。

重写hashCode()方法的规则:

  • 在程序运行过程中,同一个对象多次调用hashCode()值应该返回相等的值

  • 当两个程序通过equals()方法比较返回true时,这个两个对象的hashCode()方法也应该返回相等的值。

  • 对象中用作equals()方法比较的实例变量,都应该用于计算hashCode值。

下面给出重写hashCode()方法的一般步骤。

  • 把对象内每个有意义的实例变量(即每个参数equals()方法比较的标准的实例变量)计算出一个int类型hashCode值。

  • 用第一步计算出来的多个hashCode值合计算出一个hashCoe值返回。为了避免直接相加偶然相等,可以通过为各实例变量的hashCode值乘以任意一个质数后相加。

    例如:

return f1.hashCode() + (int)f2 * 31;

如果向HashSet中添加一个可变对象后,后面程序修改了该可变对象的实例变量,则可能导致它与集合中的其他元素相同(即两个对象通过equals()方法比较返回true,两个对象的hashCode值也相等),这就有可能导致HashSet中包含两个相同的对象。

下面程序演示了这种情况:

class R
{
    int count;
    public R(int count)
    {
        this.count = count;
    }
    public String toString()
    {
        return "R(count属性:" + count + ")";
    }
    public boolean equals(Object obj)
    {
        if (obj instanceof R)
        {
            R r = (R)obj;
            if (r.count == this.count)
            {
                return true;
            }
        }
        return false;
    }
    public int hashCode()
    {
        return this.count;
    }
}
public class TestHashSet2
{
    public static void main(String[] args) 
    {
        HashSet hs = new HashSet();
        hs.add(new R(5));
        hs.add(new R(-3));
        hs.add(new R(9));
        hs.add(new R(-2));
        //打印TreeSet集合,集合元素是有序排列的
        System.out.println(hs);
        //取出第一个元素
        Iterator it = hs.iterator();
        R first = (R)it.next();
        //为第一个元素的count属性赋值
        first.count = -3;
        //再次输出count将看到TreeSet里的元素处于无序状态
        System.out.println(hs);
        hs.remove(new R(-3));
        System.out.println(hs);
        //输出false
        System.out.println("hs是否包含count为-3的R对象?" + hs.contains(new R(-3)));
        //输出false
        System.out.println("hs是否包含count为5的R对象?" + hs.contains(new R(5)));

    }
}

上面代码更改了Set集合中第一个R对象的count值,导致R对象与集合中的其他对象实例变量相同。

HashSet集合中的第1个元素和第2个元素完全相同,这表明两个元素已经重复,此时HashSet会比较混乱,当试图删除count为-3的R对象时先计算HashCode再使用equals()比较——此时只有第二个元素符合,所以第二个元素被删除。第一个元素保存在-2的位置,但使用equals()方法拿它和count为-2的R对象进行比较又返回false——这将导致HashSet不可能准确访问该元素。

LinkedHashSet类

HashSet还有一个子类LinkedHashSet,LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。也就是说当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。

LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时将由很好的性能,因为它以链表来维护内部的次序。

public class TestLinkedHashSet
{
    public static void main(String[] args) 
    {
        LinkedHashSet books = new LinkedHashSet();
        books.add("Struts2权威指南");
        books.add("轻量级J2EE企业应用实战");
        //删除 Struts2权威指南
        books.remove("Struts2权威指南");
        //重新添加 Struts2权威指南
        books.add("Struts2权威指南");
        System.out.println(books);
    }
}
输出结果:
[轻量级J2EE企业应用实战,Struts2权威指南]

上面的集合里,元素顺序正好与添加顺序一致。

TreeSet类

TreeSet是SortedSet接口唯一的实现类,正如SortSet名字所暗示的,TreeSet集合可以确保元素处于排序状态。与HashSet集合相比,TreeSet还提供了几个额外的方法。如first()、last()、headSet()等。
看起来方法很多,其实它们很简单:因为TreeSet中的元素是有序的,所以增加了访问第一个、前一个、后一个、最后一个元素的方法,并提供了三个从TreeSet中截取子TreeSet的方法。

Comparator comparator():返回当前set使用的Comparator,或者返回null,表示以自然方式排序。

Object first():返回集合中的第一个元素。

Object last():返回集合中的最后一个元素。

Object lower(Object e):返回集合中位于指定元素之前的一个元素。

Object higher(Object e):返回集合中位于指定元素之后的一个元素。

SortedSet subSet(from Element,to Element):返回此set的子集合,范围从from Element到to Element(闭包)。

SortedSet headSet(toElement):返回此Set的子集,由小于toElement的元素组成。

SortedSet tailSet(fromElement):返回此Set的子集,由大于等于fromElement的元素组成。

下面测试的TreeSet的通用用法:

public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet nums = new TreeSet();
        //向TreeSet中添加四个Integer对象
        nums.add(5);
        nums.add(2);
        nums.add(10);
        nums.add(-9);
        //输出集合元素,看到集合元素处于排序状态
        System.out.println(nums);
        //输出集合中的第一个元素和最后一个元素
        System.out.println(nums.first());//输出9
        System.out.println(nums.last());//输出10
        //返回小于4的子集,不包含4
        System.out.println(nums.headSet(4));//输出[-9,2]
        //返回大于5的子集,不包含5 
        System.out.println(nums.tailSet(5));//输出[5,10]
        //返回大于等于-3,小于4的自己 [-3,4)
        System.out.println(nums.subSet(-3, 4));//输出[2]
    }
}
输出结果:
[-9, 2, 5, 10]
-9
10
[-9, 2]
[5, 10]
[2]

可以看出TreeSet是根据元素的实际大小顺序进行排序的。
TreeSet采用红黑树数据结构来存储集合元素,并且支持两种排序方法:自然排序和定制排序。

自然排序

TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将进而元素按升序排列,这种方式就是自然排序。

Java提供了Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法。当一个对象调用该方法与另一个对象进行比较时,例如obj1.compareTo(obj2),返回0表示两个对象相等,返回正整数表明obj1 > obj2;负整数,obj1 < obj2。

Java的一些常用类已经实现了Comparable接口,并提供了比较大小的标准。

BigDecimal、BigInteger即所有数值型对于的包装类
Character:按字符的UNICODE值进行比较
Booleantrue大于false对于的包装类
String:按字符的UNICODE
Date、Time:后面的时间、日期比前面的大

当把一个对象添加到TreeSet时,该对象必须实现Comparable接口,否则程序抛出异常。

class Err()

public class TreeSetErrorTest
{
    public static void main(String[] args)
    {
        TreeSet ts = new TreeSet();
        //向TreeSet集合中添加两个Err对象
        ts.add(new Err());
        ts.add(new Err());//①
    }
}

上面程序添加第一个对象时候没有,TreeSet里没有任何元素,所以不会出现问题;当加入第二个Err对象时,TreeSet会调用该对象的compareTo(Object obj)方法与集合中的其他元素进行比较——如果没有实现Comparable接口,则会引发ClassException异常。

注意:当我们没有实现Comparable接口,即使只添加一个元素,当我们取出该元素时,依然会引发ClassCastException异常。

还有一点需要指出:大部分类在实现compareTo(Object obj)方法时,都需要将被比较对象强制类型转换成相同类型,因为只有相同类型的两个实例才会比较大小,当试图把一个对象加入到TreeSet集合时,TreeSet会调用该对象的compareTo(Object obj)。也就是说,向TreeSet中添加的应该是同一个类的对象,否则也会引发ClassCastException。

public class TreeSetErrorTest2
{
    public static void main(String[] args)
    {
        TreeSet ts = new TreeSet();
        //向TreeSet中添加两个对象
        ts.add(new String("Java"));
        ts.add(new Date());///
    }
}

上述代码在①处引发异常,原因是当添加第二个元素时,无法使用compareTo()来比较大小。

如果向TreeSet中添加的对象时程序员自定义类的对象,则可以向TreeSet中添加多种类型的对象,前提是用户自定义实现了compareTo()接口,且实现compareTo(Object obj)方法没有进行强制类型抓换。

总结:如果希望TreeSet能够正常工作,TreeSet只能添加同一种类型的对象。

当把一个对象加入到TreeSet中石化,TreeSet调用该对象的compareTo(Object obj)方法与容器中的其他对象比较大小,然后根据红黑树找到它的存储位置。如果通过compareTo()方法比较相等,新对象将无法添加到TreeSet集合中。

对于TreeSet而言,判断两个对象是否相等的标准就是:两个对象通过compareTo()方法0。

class Z implements Comparable
{
    int age;
    public Z(int age)
    {
        this.age = age;
    }
    //重写equals方法,总是返回true
    public boolean equals(Object obj)
    {
        return true;
    }
    //重写了ComparabTo(Object obj)方法,总是返回1
    public int compareTo(Object obj)
    {
        return 1;//总是不相等
    }
    public class TreeSetTest2
    {
        public static void main(String[] args)
        {
         TreeSet set = new TreeSet();
            Z z1 = new Z(6);
            set.add(z1);
            //第二次添加同一个对象,输出true,表明添加成功
            System.out.println(set.add(z1));
            //下面输出set集合,将看到有两个元素
            System.out.println(set);
            //修改第一个set集合的第一个元素的age变量
            ((Z)(set.first())).age = 9;
            //输出set集合的最后一个元素的age变量
            System.out.println((Z)(set.last()).age);
        }
    }
}

程序①代码把同一个对象再次添加到TreeSet中,因为z1对象的compareTo(Object obj)方法总是返回1,虽然它的equals()方法总是返回true,但TreeSet认为z1对象和它自己也不相等,因此TreeSet可以添加两个z1对象。

虽然集合中保存的是两个元素(集合的元素实际上是引用,但习惯上把被引用的对象称为集合元素),实际上是同一个元素。所以当修改TreeSet集合里的第一个元素的age变量后,该TreeSet集合里的最后一个元素的age变量也变了。

由此应该注意一个问题,当需要把一个对象放入TreeSet中时,重写该对象对于类的equals()方法时,应该保证该方法与compareTo(Object obj)方法有一致的结果。

其规则是:如果两个对象通过equals()方法比较返回true,那么通过compareTo()方法比较应该返回。

果向TreeSet中添加一个可变对象后,并且后面程序修改了该可变对象的实例变量,这将导致它与其他对象的大小顺序发生变化,但TreeSet不会调整他们的顺序,甚至导致TreeSet中保存的这两个对象通过compareTo(Object obj)方法比较返回0。
下面程序演示了这种情况。

class X implements Comparable
{
    int count;
    public X(int count)
    {
        this.count = count;
    }
    public String toString()
    {
        return "X[count:" + count + "]";
    }
    //重写equals()方法,根据count来判断
    public boolean equals(Object obj)
    {
        if(this == obj)
        {
            return true;
        }
        if(obj != null && obj.getClass() == X.class)
        {
            X r = (X)obj;
            return r.count == this.count;
        }
         return false;
    }
    //重写compareTo()方法
    public int compareTo(Object obj)
    {
        X r = (X)obj;
        return count > r.count ? 1 : count == r.count ? 0 : -1;
    }
}

public class TreeSetTest3 {
    public static void main(String[] args) {
        TreeSet ts = new TreeSet();
        ts.add(new X(5));
        ts.add(new X(-3));
        ts.add(new X(9));
        ts.add(new X(-2));
        //打印TreeSet集合,集合元素是有序排列的
        System.out.println(ts);//①
        //取出第一个元素
        X first = (X)ts.first();
        //对第一个元素的count赋值
        first.count = 20;
        //取出最后一个元素
        X last = (X)ts.last();
        //对最后一个元素赋值-2,与第二个元素相同
        last.count = -2;
        //再次看到输出TreeSet元素处于无序状态,且有重复元素
        System.out.println(ts);
        //删除实例变量被改变的元素,删除失败
        System.out.println(ts.remove(new X(-2)));//false
        System.out.println(ts.remove(new X(20)));//false
        System.out.println(ts);//②
        //删除实例变量没有被改变的元素,成功
        System.out.println(ts.remove(new X(5)));//③
        System.out.println(ts);
        //接下来可以删除被修改的元素
        System.out.println(ts.remove(new X(-2)));//true
        System.out.println(ts.remove(new X(-2)));//true
        System.out.println(ts);
    }
}

程序执行①处代码输出时,将看到集合处于无序状态,而且集合中包含了重复元素。

一旦改变了TreeSet集合里元素的实例变量,当再试图删除该对象时候,TreeSet也会删除失败(甚至集合中原有的、实例变量没被修改但与修改后的元素相等的元素也无法删除),比如②处,删除-2和20都删除失败。

但是当程序执行了③处的代码后,TreeSet会集合中的元素重新索引(并不是重新排序)。接下来就可以删除所有元素了。

与HashSet类似的是,如果TreeSet中包含了可变对象,当那些可变对象的实例变量被修改时,TreeSet在处理这些对象时变得十分复杂,而且容易出错。因此为了让程序更贱健壮,推荐不要修改防卫HashSet和TreeSet集合元素的关键实例变量。

定制排序

如果需要实现定制排序,则可以使用Comparator接口的帮助,该接口包含一个int compare(T o1,T o2)方法,该方法用于计较两个数的大小,如果返回正整数,则表明o1>o2;如果返回0,则表明o1=o2;如果返回负整数,则表明o1<02。
如果需要实现定制排序,则需要在创建TreeSet集合对象时,并提供一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑。

class M
{
    int age;
    public M(int age)
    {
        this.age = age;
    }

    public String toString(){
        return "M对象(age:" + age + ")";
    }
}

public class TestTreeSet3
{
    public static void main(String[] args) 
    {
        TreeSet ts = new TreeSet(new Comparator() 
        {

            @Override
            public int compare(Object o1, Object o2) {
                // TODO Auto-generated method stub
                M m1 = (M)o1;
                M m2 = (M)o2;

                if (m1.age > m2.age)
                    {
                        return -1;
                    }
                    else if (m1.age == m2.age)
                    {
                        return 0;
                    }
                    else
                    {
                        return 1;
                    }
            }
        });
        ts.add(new M(5));
        ts.add(new M(-3));
        ts.add(new M(9));
        System.out.println(ts);
    }
}

上面程序中创建了一个Comparator接口的匿名内部类对象,该对象负责ts集合的排序。所以当我们把M对象添加到ts集合中时,无须M类实现Comparable接口,因为此时TreeSet无需通过M对象来比较大小,而是由与TreeSet关联的Comparator对象来负责集合元素的排序。

输出结果:
[M对象(age:9), M对象(age:5), M对象(age:-3)]

注意:不可以向TreeSet中添加类型不同的对象,否则会引起ClassCastException异常。

EnumSet类

EnumSet是一个专门为枚举类设计的集合类,EnumSet中所有元素都必须是指定枚举类的枚举值,该枚举类在创建EnumSet时显式或隐式地指定。EnumSet的集合元素也是有序的,EnumSet以枚举值在Enum类内部的定义顺序来决定集合元素的顺序。

EnumSet在内部以位向量的形式存储,这种存储方式非常紧凑高效。因此EnumSet对象占用内存很小,而且运行效率很好。尤其是在进行批量操作时候,如果其参数是EnumSet集合,则该批量操作的速度也非常快。

EnumSet集合不允许加入null元素,如果试图插入null元素,将会报NullPointerException异常。如果只是判断是否包含null元素或者试图删除null元素,则不会报错,只是删除时返回false。

EnumSet类并没有暴露任何构造器来创建该类的实例,程序应该通过它的类方法来创建EnumSet对象。
比如:

EnumSet allOf(Class elementType)
EnumSet complement(EnumSet s)
EnumSet copyOf(Collection c)

下面示范了如何创建EnumSet实例:

enum Season
{
    SPRING,SUMMER,FALL,WINTER
}
public class EnumSetTest {
    public static void main(String[] args) {
        //创建一个EnumSet,集合元素就是Season枚举类的全部枚举值
        EnumSet es1 = EnumSet.allOf(Season.class);
        System.out.println(es1);//输出[SPRING,SUMMER,FALL,WINTER]
        //创建一个EnumSet空集合,指定其集合元素是Season类的枚举类
        EnumSet es2 = EnumSet.noneOf(Season.class);
        System.out.println(es2);//输出[]
        //主动添加两个元素
        System.out.println(es2.add(Season.SPRING));
        System.out.println(es2.add(Season.WINTER));
        System.out.println(es2);//输出[SPRING,WINTER]
        //以指定枚举值创建EnumSet集合
        EnumSet es3 = EnumSet.of(Season.SUMMER , 
            Season.WINTER);
        System.out.println(es3);//输出[SUMMER,WINTER]
        EnumSet es4 = EnumSet.range(Season.SUMMER, 
            Season.WINTER);
        System.out.println(es4);//输出[SUMMER,FALL,WINTER]
        //新创建的EnumSet集合元素和es4集合元素有相同的类型
        //es5 + es4的集合元素 = Season枚举类的全部枚举值
        EnumSet es5 = EnumSet.complementOf(es4);
        System.out.println(es4);//输出[SPRING]
    }
}
输出结果:
[SPRING, SUMMER, FALL, WINTER]
[]
[SPRING, WINTER]
[SUMMER, WINTER]
[SUMMER, FALL, WINTER]
[SPRING]

上面程序代表了EnumSet的常规用法。除此之外,还可以复制另一个Enumset集合中的所有元素,或者赋值另一个Collection集合中的所有元素。当复制Collection集合中的所有元素时,集合中的元素必须是同一个枚举类的枚举值。

enum Season
{
    SPRING,SUMMER,FALL,WINTER
}
public class TestEnumSet
{
    public static void main(String[] args) 
    {
        Collection c = new HashSet();
        c.clear();
        c.add(Season.FALL);
        c.add(Season.SPRING);
        //复制Collection集合中所有元素来创建EnumSet集合
        EnumSet enumSet = EnumSet.copyOf(c);
        //输出[SPRING,FALL]
        System.out.println(enumSet);
        c.add("Struts2权威指南");
        c.add("轻量级J2EE企业应用实战");
        //下面代码出现异常:因为c集合里的元素不是全部都为枚举值
        //enumSet = EnumSet.copyOf(c); 
    }
} 
输出结果:
[SPRING, FALL]

程序执行②处代码时,c集合中的元素不全是枚举值所以抛出ClassCastException异常。

各Set实现类的性能分析

HashSet和TreeSet是Set的两个典型实现。

HashSet的性能总是要优于TreeSet,特别是最常用的添加、查询等操作。因为TreeSet需要使用额外的红黑树算法来维护集合元素的次序。只有当需要一个排序的Set时,才需要TreeSet。

HashSet还有一个子类,LinkedHashSet,对于普通的插入、删除操作,LinkedHashSet比HashSet要略慢一点,这是由于维护链表所带来的额外开销,但是由于有链表,遍历LinkedHashSet会很快。

EnumSet是所有类中性能最好的,但它只能保存枚举类的枚举值。

Set的三个实现类HashSet、TreeSet、EnumSet都是线程不安全的。如果有多个线程同时访问Set集合,并且有超过一个线程修改了Set集合,必须手动保证该Set集合的同步性。
通常可以通过Collections的工具类synchronizedSortedSet方法来“包装”该Set集合。此操作最好在创建时进行,以防止对Set集合的意外非同步访问。
例如:

SortedSet s = Collections.synchronizedSorted(new
     TreeSet(...));
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值