Java学习

Collection接口(无序、可重复),子接口List接口(有序(引入索引号,可插入修改顺序)、可重复),子接口Set接口(无序、不可重复)

线性表:数组、链表

Iterator 和 Iterable 接口

Collection 接口作为集合框架的顶级接口之一,继承于 Iterable 接口

public interface Collection <E> extends Iterable<E>

Iterable 接口

Iterable 接口用于引入可遍历的功能,要求接口的具体实现类必须提供 Iterator 的实现

Iterator 接口

Iterator 迭代器,走访器,可以理解为集合中元素的指针,用于实现集合中所有元素的遍历访问

public interface Iterator<E> { 
    boolean hasNext(); //用于判断容器内是否还有可供访问的元素 
    E next();//返回迭代器刚越过的元素的引用,返回是 Object,需要强制转换或者使用泛型 
    default void remove() { //迭代器提供的删除元素的方法 
        throw new UnsupportedOperationException("remove"); 
    }
    default void forEachRemaining(Consumer < ? super E > action) { //获取剩余的元素,针对每个元素应用 action 函数 
        Objects.requireNonNull(action); 
        while (hasNext()) 
            action.accept(next());
    } 
}

编程使用

Collection cc = new ArrayList();
		for (int i = 0; i < 5; i++) {
			cc.add(cc);
		}
		Iterator it = cc.iterator();
		while (it.hasNext()) {
			Object tmp = it.next();
			System.out.println(tmp);
		}

对于大多数实现了Iterable接口的集合,可以多次调用forEach进行数据元素的遍历操作

Iterator和Iterable接口Collection接口作为集合框架的顶级接口之一,继承于Iterable接口public interface Collection<E> extends Iterable<E>Iterable接口Iterable接口用于引入可遍历的功能,具体的实现类必须提供Iterator的实现

一般不使用匿名内部类,代码编写太繁琐。一般建议使用lambda表达式

cc.forEach((obj)->{System.out.println(obj); });
cc.forEach(System.out::println); 

forEachRemaining

Iterator接口的默认实现中提供了forEachRemaing方法,用于获取对应集合的迭代器Iterator访问的每个元素

Iterator it=list.iterator(); //获取迭代器对象

it.forEachRemaining((obj)-&#62;{System.out.pritln(obj); })

安全失败和快速失败

在多线程操作同时遍历集合时可以出现两种失败的情况—线程安全

  • 安全失败是基于对底层集合进行拷贝,从而隔离修改和遍历访问,遍历不会受到修改的影响,是 juc 包中提供的实现类,不会报出异常

  • java.util 包中提供的集合类都是快速失败 fail-fast,当遍历的同时其它线程修改了集合结构,则抛出异常 ConcurrentModificationException。

快速失败
Collection cc = new ArrayList();
for (int i = 0; i < 20; i++) {
    cc.add(i);
}
new Thread(()->{
    Iterator it = cc.iterator();
    while(it.hasNext()){
        System.out.println(it.next());
    	try {
	        Thread.sleep(100);
    	} catch (InterruptedException e) {
        	e.printStackTrace();
        }
	}
}).start();
try {
    Thread.sleep(110);
} catch (InterruptedException e) { 
    e.printStackTrace();
}
cc.add(999);
安全失败
Collection cc = new CopyOnWriteArrayList();
for (int i = 0; i < 20; i++) {
    cc.add(i);
}
new Thread(()->{
    Iterator it = cc.iterator();
    while(it.hasNext()){
        System.out.println(it.next());
    	try {
	        Thread.sleep(100);
    	} catch (InterruptedException e) {
        	e.printStackTrace();
        }
	}
}).start();
try {
    Thread.sleep(110);
} catch (InterruptedException e) { 
    e.printStackTrace();
}
cc.add(999);

实现类

ArrayList底层数据结构就是数组,随机查询速度快、增删慢,线程不安全,效率高,允许存放重复元素

LinkedList底层数据结构是双向链表,查询慢、增删快,线程不安全,效率高,允许存放重复元素

Vector底层数据结构是数组,随机查询速度快、增删慢,线程安全,效率低,允许存放重复数组

ArrayList实现类

ArrayList实现了List接口的可扩容的数组【System.arrayCopy】,它的内部不是基于数组实现的,相较于Java中的数组,容量能动态增长

继承于AbstractList抽象类,实现了List接口

RandomAccess接口,提供了随机访问支持。遍历元素的方法有两种,为快速随机访问和通过迭代器Iterator访问

Clonable接口,可以被克隆clone()

Serializable接口,支持序列化

ArrayList不是线程安全的,所以在无需考虑线程安全时使用,在需要考虑线程安全的多线程环境下可以考虑使用Vector或者juc中的CopyOnWriteArrayList

常量定义

private static final int DEFAUIT_CAPACITY = 10;默认数组元素的初始化容积

private static final Object[] EMPTY_ELEMENTDATA = {}; 没有元素的空对象数组,没有存储任何元素,数组长度为 0

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 构建 ArrayList 时默认使用的空数组

属性

transient Object[] elementData;就是用于存放元素的数组

private int size; 真实存放的元素个数

构造器 List list = new ArrayList();

默认构造器ArrayList(); 实际上并没有直接开辟空间,而是使用常量空数组

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

带有初始化容积设置的构造器 new ArrayList(2000);

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为参数构建对象

public ArrayList(Collection&#60;?extends E>c) {
    elementData =c.toArray();  //将collection转换为数组,并赋值给elementData 属性       
    if ((size =elementData.length) != 0) {  //获取元素个数并赋值给size属性            // c.toArray might (incorrectly) not return Object[] (see 6260652)            
        if (elementData.getClass() != Object[].class)  //如果是对象数组则执行数组的拷贝       
            elementData = Arrays.copyOf(elementData,size,Object[].class); 
    }else {       
        this.elementData =EMPTY_ELEMENTDATA;      
    }   
}

概述
  1. ArrayList实际上是通过数组保存数据,当构建ArrayList时,如果使用默认构造器,默认容积是10,但是采用延迟构建
  2. 当ArrayList容积不足以存放元素时,会自动进行增容处理。新容积 = 原始容积 * 3 / 2 + 1
  3. ArrayList克隆函数是将全部元素克隆到一个数组中。elementData前面有transient修饰
  4. ArrayList实现序列化时,先写出容积值,再依次写出每个元素
add方法解析

public void add(int index,E element)

ArrayList在添加元素时,首先进行index范围检查,防止传入参数小于0或者超过数组的size大小,再和默认分配的大小值进行比对,如果大于默认大小则需要进行扩容处理。扩容时首先将创建一个大小为原始数组1.5倍的新数组,新数组大小不能超过Integer的最大值,检查完毕后进行数组元素的拷贝

public boolean add(E e) {
		ensureCapacityInternal(size + 1);
		elementData[size++] = e;
	}

首先进行容积检查和修改次数的统计,然后储存元素,并对 size+1

public void add(int index, E element){
	rangeCheckForAdd(index);
	
    ensureCapacityInternal(size +1);// Increments modCount!!
    System.arraycopy(elementData, index, elementData,index+1,
size - index);
    elementData[ index ] =element;
    size++;
}

rangeCheckForAdd 进行 index 的合法性检查,要求 index 必须是[0,size]之间,否则运行时异常

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

ensureCapacityInternal 用于保证容积正确

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity( calculateCapacity( elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
	// overflow- conscious code
	if (minCapacity - elementData.length > 0)
		grow(minCapacity);
}

如果所需要的最小容积大于数组的长度时调用 grow 方法进行扩容处理

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

int len=10;

len=len>>1; //5 相当于 len/2 使用位运算比直接的除法计算效率高

涉及一个思路–尽量减少扩容次数:如果需要 11 个容积值,不是新建数组为 11,因为这样后续的数据添加,例如添加第 12 个数据

还需要进行扩容,若以一次扩容比例为新增 50%容积,也就是新容积为 15,后续 5 个数据的添加则不需要进行扩容

这里涉及的常量值为 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private static int hugeCapacity(int minCapacity) {
	if (minCapacity <0) // overflow
		throw new outofMemoryError();
	return (minCapacity > MAX_ ARRAY_ SIZE) ?
		Integer .MAX_ VALUE :
		MAX_ ARRAY_ SIZE;
}

minCapacity 所需要的最小容积小于 0,一定是超出 int 范围,此时抛出 Error, hugeCapacity 用于实现最大容积值为 Integer.max_value

Arrays 工具类是用于封装一组相关数组的操作。

public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}
public static <T,U> T[] copyof(U[] original, int newLength, Class<? extends T[]> newType) {
    @suppres sWarnings ("unchecked")
        T[] copy =((object )newType=(Object )0bject[].class)
        ? (T[]) new object [newLength]
		: (T[]) Array.newInstance( newType.getComponentType(), newLength);
    System. arraycopy(original,0, copy, 0, Math.min(original.length, newLength));
    return copy;
}

Arrays.copyOf(elementData, newCapacity)可以创建一个指定长度的新数组,并且将原始数组中的内容拷贝到新数组,并返回新数组

System.arraycopy(elementData, index, elementData, index + 1,size - index);从 index 位置开始到整个数组中的有效元素全部向后拷贝移动一个位置

elementData[index] = element; 覆盖原始 index 位置上的数据,从而实现将输入插入 index 的目的

remove 方法解析

E remove(int index)

remove 方法首先进行 index 的范围检查,然后用 elementData(index)获取指定数组位置 index 对应的元素,在使用 size-index-1 转换为 numMoved 移动元素个数,接着使用 System.arrycopy 进行自我复制,将最后一 个位置的元素赋值为 null

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IrpEDZGq-1646556359078)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216203934481.png)]

要求 index 必须在[0,size-1]的范围内

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1sgQ65ps-1646556359079)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216204014072.png)]

modCount++; 统计修改次数,用于实现遍历同时修改数据的发现

E oldValue = elementData(index); 从数组中获取 index 位置上的元素

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rCU62RQw-1646556359079)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216204035372.png)]

int numMoved = size - index - 1; 获取到移动元素个数

if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index,numMoved);将原数组elementData 中的数据从 index+1 位置开始拷贝到目标数组 elemenentData 的 index 位置上,总共拷贝numMoved 个

elementData[–size] = null; 将最后一个位置上的元素值赋 null 值,同时 size-1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QHBmZQ2G-1646556359080)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216204149342.png)]

remove(Object obj):boolean 方法中是先查找参数 obj 的位置,然后调用 fastRemove 通过索引号进行快速删除

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yvmdGiUI-1646556359080)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216204206212.png)]

modCount++进行修改次数统计

使用 size-index-1 获取需要拷贝一定的元素个数

使用 System.arraycopy 进行拷贝移动操作

数组的最后位置赋值为 null

其它方法解析

size() 获取集合中的元素个数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yPL3Gwwd-1646556359081)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216204301305.png)]

isEmpty()判断集合中的元素个数为 0

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YzqLO0NP-1646556359081)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216204319447.png)]

contains 判断是否包含指定的元素indexOf 用于在集合中查找指定元素的第一个索引序号值,如果查询不到则返回-1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ufcZz448-1646556359082)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216204334046.png)]

lastIndexOf 用于在集合中从后向前查找第一个满足条件的元素下标

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VAe1dEIX-1646556359082)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216204343023.png)]

快速失败

由于 ArrayList 不是线程安全的容器,所以多个线程操作过程中会出现线程安全问题。

fail-fast 产生的条件:

当多个线程对 Collection 集合进行操作时,如果某个线程通过 Iterator 遍历集合时,该集合的内容被其它线程所改变,则会抛出 ConcurrentModificationException 异常

实现原理:

modCount 修改次数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W2HuQ4Wc-1646556359083)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216204444651.png)]

调用 Itr 中的遍历方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XZTarbzo-1646556359083)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216204501390.png)]

具体编程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i8P1ckah-1646556359083)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216204513248.png)]

读取数据 get 方法

根据指定的索引编号获取指定位置上的元素。首先检查 index 的合法性,要求[0,size-1]范围内,如果不合法则异常,如果合法则根据下标从数组中获取对应的数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jIZ0cBCy-1646556359083)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216204547718.png)]

线程安全问题

ArrayList 并没有提供线程安全处理,如果在开发中需要线程安全,则可以考虑 2 种解决方案:

方案 1:利用工具类实现线程安全

Collection ccc=Collections.synchronizedCollection(collection);

List lll=Collections.synchronizedList(list);

在整个集合种引入一个锁,实现整个集合的线程安全

方案 2:利用 juc 包中的类替代 java.util 包中的类不使用 ArrayList,而改成 CopyOnWriteArrayList。CopyOnWrite 将读写进行分离,读取数据时读真实存放的数据,如果修改操作时,首先拷贝原始数据,对备份进行修改,最后再使用备份替换原始数据

clone 操作

首先 clone 当前对象,然后将数组拷贝到新对象的数组属性上,将修改次数设置为 0

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xgl7r2H3-1646556359084)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216204646730.png)]

序列化处理

写出对象时首先写出 size 表示当前集合中的元素个数,然后逐个写出集合存放的元素

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D5snVsae-1646556359084)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216204706923.png)]

Vector 实现类

内部实现仍旧是采用数组的方式实现,但是大部分方法上都有 synchronized 同步约束,所以当前类是一个线程安全的类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vy1BBAE0-1646556359085)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216204759022.png)]

elementData 用于存放数据的数组

elementCount 存放的数据个数—size

capacityIncrement 数组扩容时的步长值

构造器
public Vector() { //无参构造器 
    this(10); //调用当前类的只有一个 int 类型参数的构造器,无参构造的初始化数组长度为 10 
}public Vector(int initialCapacity) { //参数就是初始化容积值,也就是默认的数组长度 
    this(initialCapacity, 0); //调用其它构造器 
}public Vector(int initialCapacity, int capacityIncrement) { //参数 1 是初始化容积值,参数 2 是扩容的步长值 
    super(); 
    if (initialCapacity < 0) //初始化容积值不能小于 0 
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; //初始化数组 
    this.capacityIncrement = capacityIncrement; 
}
add 方法解析

最大的区别点在于 synchronized 同步处理,默认扩容 100%

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R0G1FE6r-1646556359085)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216204954879.png)]

ensureCapacityHelper 方法在发现当前容积不足时会调用 grow 方法进行扩容处理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nuZTqnRi-1646556359085)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216205030837.png)]

如果不设置 capacityIncrement 参数,则默认扩容为原始容积的 2 倍。

LinkedList 实现类

LinkedList 底层的数据结构是基于双向链表的结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3ryb0l0C-1646556359086)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216205113189.png)]

size 元素个数

first 和 last 是头节点和尾节点

Node 节点定义

item 就是具体存储的数据

next 用于指向下一个节点的引用

prev 用于指向前一个节点的引用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X9x0lFwn-1646556359086)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216205126032.png)]

构造器

因为底层采用的是链表结构,所以没有容器的概念,从理论上来说可以无限制添加元素;但是 int size 限制了其中最大的元素个数为 Integer.max_value

public LinkedList(){
    
}
public LinkedList(Collection<? extends E > c){
    this();
    addAll(c);
}

特性:

可以添加 null 值元素

允许元素重复

可以通过索引编号有序

非线程安全

add 方法的实现

新增元素时就是向链表的末尾追加元素

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;
    }
    size++;
    modCount++;
}
remove 方法解析

删除指定元素值则是从头指针开始遍历整个链表,查找指定值的 Node 节点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mc9hw7Ub-1646556359087)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216205809592.png)]

具体的删除操作是通过 unlink 实现的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L2Fg9IxU-1646556359087)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216205900308.png)]

按照索引序号删除指定元素

public E remove (int index){
    checkElementIndex(index);
    return unlink(node(index));
}

checkElementIndex 用于判断 index 的合法性,要求取值必须[0,size-1],否则抛出异常

node 方法用于获取指定索引位置上的节点 Node 对象,size>>1 表示 size/2,根据 index 距离头部还是尾部近,从头部(index<size/2)或者尾部(index>size/2)开始查找

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BPGPI2sN-1646556359088)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220216210056883.png)]

获取指定位置的元素
public E get(int index){
	checkElementIndex(index);
	return node(index).item;
}

checkElementIndex 用于判断 index 的合法性,要求取值必须[0,size-1],否则抛出异常

node 方法用于获取指定索引位置上的节点 Node 对象,size>>1 表示 size/2,根据 index 距离头部还是尾部近,从头部(index<size/2)或者尾部(index>size/2)开始查找

实现类总结

ArrayListLinkedListVector
实现方式数组,按照索引编号访问速度快 O(1),但是删除或者添加元素可能会导致元素的移动,速度慢 O(n)双向链表,按照索引编号访问元素速度慢 O(n),但是删除或者添加元素,速度快 O(1)数组,按照索引编号访问速度快O(1),但是删除或者添加元素可能会导致元素的移动,速度慢 O(n)
是否同步不同步,线程不安全,但是并发性高,访问效率高不同步,线程不安全,但是并发性高,访问效率高同步,所以线程安全,但是并发性低,访问效率低
如何选择经常需要快速访问,较少在中间增加删除元素时使用;如果多线程访问,则需要自行编程解决线程安全问题经常需要在内部频繁增删元素,但是很少需要通过索引编号进行访问时使用。如果多线程访问,则需要自行编程解决线程安全问题一般不使用,如果在多线程并发访问并要求线程安全时可以考虑使用

--------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 实现方式 | 数组,按照索引编号访问速度快 O(1),但是删除或者添加元素可能会导致元素的移动,速度慢 O(n) | 双向链表,按照索引编号访问元素速度慢 O(n),但是删除或者添加元素,速度快 O(1) | 数组,按照索引编号访问速度快O(1),但是删除或者添加元素可能会导致元素的移动,速度慢 O(n) |
| 是否同步 | 不同步,线程不安全,但是并发性高,访问效率高 | 不同步,线程不安全,但是并发性高,访问效率高 | 同步,所以线程安全,但是并发性低,访问效率低 |
| 如何选择 | 经常需要快速访问,较少在中间增加删除元素时使用;如果多线程访问,则需要自行编程解决线程安全问题 | 经常需要在内部频繁增删元素,但是很少需要通过索引编号进行访问时使用。如果多线程访问,则需要自行编程解决线程安全问题 | 一般不使用,如果在多线程并发访问并要求线程安全时可以考虑使用 |

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值