Java核心知识(二)集合

1、List接口

1.1、ArrayList

初始设定参数,可以看到,ArrayList本质是一个数组。

  • 默认长度为10
  • 空数组
  • 默认长度空数组
  • 数据数组
  • 元素个数
	//默认数组长度
    private static final int DEFAULT_CAPACITY = 10;
    //空数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
	//默认长度的空元素数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
	//存储数据元素的数组
    transient Object[] elementData; // non-private to simplify nested class access
	//集合中元素个数
    private int size;

初始化操作

  • 无参构造:就是将默认长度空数组赋予存储数组
  • 有参构造:根据传入的当前数组长度进行判断,大于0就正常创建,等于0就赋予空数组,小于0就抛出异常
//无参构造
public ArrayList() {
	//将默认的空数组赋予存储数组
	//相当于this.elementData = {}
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//有参构造,赋予ArrayList初始长度
public ArrayList(int initialCapacity) {
	//如果初始长度大于0,创造一个新的Object数组
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    //如果等于0,赋予默认空数组
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
    	//如果小于0,抛出异常
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

添加方法(add)

  • ensureCapacityInternal(size + 1)检测当前数组长度是否会越界,然后赋予数组数值。
  • ensureCapacityInternal方法中,查看当前数据数组是否为空,若为空,就取得默认长度和当前元素个数+1之间比较大的那个,若不为空,直接传入ensureExplicitCapacity方法中
  • ensureExplicitCapacity方法中,modCount为操作次数,来自于AbstractList,执行自增。如果元素个数比数据数组长度大,执行grow函数
  • grow方法中,旧长度是数据数组长度,int newCapacity = oldCapacity + (oldCapacity >> 1);新长度是旧长度加上旧长度的右位移1位的运算结果,其实可以看做1.5倍取整。如果新长度比需求长度小,就取需求长度。如果新长度比MAX_ARRAY_SIZE(MAX_ARRAY_SIZE 是Integer的最大值-8)大,就执行hugeCapacity方法获取新长度。最后,将数据数组复制到一个长度为新长度的新数组中。
  • int newCapacity = oldCapacity + (oldCapacity >> 1);就是1.5倍扩容的根据
public boolean add(E e) {
	//检测当前数组长度,动态扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //将数据放入数组
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
	//如果这是一个空数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    	//取得默认数组长度与传入长度参数比较大的那个
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
	//赋予新的函数
    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
	//操作次数,自增,来自于AbstractList
    modCount++;

    // 如果元素个数比数据数组长度大,执行grow函数
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    //旧长度是数组元素长度
    int oldCapacity = elementData.length;
    //新长度为旧长度加上旧长度的右位移运算结果(就是旧长度的一半,结果取整)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //如果新长度比需求长度小,就将需求长度赋予新长度
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //如果新长度比MAX_ARRAY_SIZE大,执行hugeCapacity方法
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    //复制数组到长度为新长度的新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

获取方法(get)

  • rangeCheck检测数组下标越界
  • elementData根据下标获取对应的数组数据
public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

E elementData(int index) {
    return (E) elementData[index];
}

更新方法(set)

  • rangeCheck检测数组下标越界
  • elementData根据下标获取对应的数组数据
  • 数组数据替换,返回旧数据
public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

移除方法(remove)

  • rangeCheck检测数组下标越界
  • 操作次数自增,elementData根据下标获取对应的数组数据
  • numMoved为要移动元素的个数
  • 使用数组移动,将需要移除的数组位覆盖
  • 将最后一位数组位移除,返回旧数据。
public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(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

    return oldValue;
}

FailFast机制
快速失败机制,Java集合类为了应对并发访问在集合迭代的过程中,内部结构发生变化的一种防护机制,这种错误检查机制为这种可能发生错误通过抛出java.util.ConcurrentModificationException错误来快速失败。

源码示例:

  • expectedModCount预期操作次数被modCount操作次数赋予
  • checkForComodification方法检测,实际操作次数是否与预期操作次数不符合,如果不符合,就代表其他线程也对这个List进行过操作,造成了其内部结构的改变。
int expectedModCount = modCount;

public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

1.2、LinkedList

LinkedList是通过双向链表实现的,因此他的数据结构具有双向链表的优缺点,因此顺序查找有优势,随机查找有劣势,其关键的双向链表实现类为:

private static class Node<E> {
    E item;//节点数据
    Node<E> next;//下一个节点
    Node<E> prev;//上一个节点

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

添加方法(add,push)
添加方法有两种,add和push

  • push是一个链表的添加操作,添加到头部。
private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}
  • add是一个链表的添加操作。添加到尾部
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++;
}

获取方法(get)

  • 二分查找,从如果比中位数小,那就从头开始遍历,反之从末尾开始遍历
Node<E> node(int index) {
	//二分查找,从如果比中位数小,那就从头开始遍历,反之从末尾开始遍历
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

修改方法(set)

  • 检查下表是否合法
  • 获取对应的数据
  • 记录原来的数值,赋予新的值,返回原来的数值
public E set(int index, E element) {
	//检查下表是否合法
    checkElementIndex(index);
    //获取对应的数据
    Node<E> x = node(index);
    //记录原来的数值
    E oldVal = x.item;
    //赋予新的值
    x.item = element;
    //返回原来的数值
    return oldVal;
}

1.3、Vector

类似ArrayList使用动态数组进行存储,线程安全,但已经不常用。
Vector在每一个操作方法中都增加了synchronized关键字,保证线程安全,会消耗很多性能。

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

如果想要在线程不安全的情况下使用ArrayList,可以使用Collections.synchronizedList(list);转换为线程安全的List。

2、Set接口

2.1、HashSet

HashSet实现Set接口,由哈希表支持。

  • 数据结构为哈希表
  • 一个没有重复元素的集合
  • 不保证Set的迭代顺序,不保证顺序永久不变
  • 允许null作为元素
  • 通过HashMap实现

在初始化代码中,可以看到是初始化了一个HashMap

public HashSet() {
    map = new HashMap<>();
}

add方法中,会把数据放到key值里,在value中放入一个自定义Object对象。

private static final Object PRESENT = new Object();
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

2.2、TreeSet

TreeSet实现Set接口。

  • 基于TreeMap的NavigableMap实现
  • 使用元素的自然顺序进行排序,或者根据创建set时提供的Comparator进行排序

在初始化代码里可以看到,TreeSet是将数据保存在TreeMap中

public TreeSet() {
    this(new TreeMap<E,Object>());
}

TreeSet(NavigableMap<E,Object> m) {
    this.m = m;
}

add方法中,会把数据放到key值里,在value中放入一个自定义Object对象。

public boolean add(E e) {
    return m.put(e, PRESENT)==null;
}

3、Map接口

  • 能存储唯一的列的数据(Key唯一,不可重复)Set
  • 能存储可以重复的数据(Value可重复)List
  • 值得顺序取决于键的顺序
  • 键和值都是可以存储null元素的

3.1、TreeMap

TreeMap本质上是红黑树的实现
红黑树节点源码如下:

static final class Entry<K,V> implements Map.Entry<K,V> {
   K key;//key
   V value;//value
   Entry<K,V> left;//左子节点
   Entry<K,V> right;//右子节点
   Entry<K,V> parent;//父节点
   boolean color = BLACK;//根节点默认黑色
}

put方法

  • 赋予局部变量根节点root,检测是否为跟节点
  • 若根节点为空,检查key是否为空,建立新节点,将key,value放入
  • 若根节点不为空,进入比较器判断
  • 循环判断,将t赋予父节点,key与t节点的key比较大小,小于则将父节点的左子节点赋给t,大于则将父节点的右子节点赋给t,相等就直接修改数值,直到t为空
  • 此时t就是要插入节点的父节点
  • 根据之前key的比较结果,小于父节点则插入的节点在父节点的左侧,大于父节点则插入的节点在父节点的右侧
  • fixAfterInsertion实现红黑树的平衡
public V put(K key, V value) {
	//赋予局部变量根节点root
    Entry<K,V> t = root;
    //检测是否为跟节点
    if (t == null) {
    	//检查key是否为空
        compare(key, key); 
		//建立新节点,将key,value放入
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    //父节点
    Entry<K,V> parent;
    //比较器
    Comparator<? super K> cpr = comparator;
    //如果比较器不为空
    if (cpr != null) {
        do {
        	//将t赋予父节点
            parent = t;
            //key与t节点的key比较大小
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
            	//将父节点的左子节点赋给t
                t = t.left;
            else if (cmp > 0)
            	//将父节点的右子节点赋给t
                t = t.right;
            else
            	//相等就直接修改数值
                return t.setValue(value);
        } while (t != null);
    }
    else {
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    //t就是要插入节点的父节点
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
    	//插入的节点在父节点的左侧
        parent.left = e;
    else
    	//插入的节点在父节点的右侧
        parent.right = e;
    //实现红黑树的平衡
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

fixAfterInsertion红黑树平衡
此处处理和红黑树平衡方式一样,不再赘述。

private void fixAfterInsertion(Entry<K,V> x) {
	//将节点设置为红色
    x.color = RED;

	//循环的条件是添加的条件不为空
	//不是root节点
	//父节点是红色
    while (x != null && x != root && x.parent.color == RED) {
    	//父节点是否为祖父节点的左侧节点
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
        	//获取父节点的兄弟节点
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            //叔叔节点是红色
            if (colorOf(y) == RED) {
            	//设置父节点颜色为黑色
                setColor(parentOf(x), BLACK);
                //设置叔叔节点颜色为黑色
                setColor(y, BLACK);
                //设置祖父节点颜色为红色
                setColor(parentOf(parentOf(x)), RED);
                //将祖父节点设置为要插入的节点
                x = parentOf(parentOf(x));
            } else {//叔叔节点为黑色
            	//判断插入节点是否为父节点的右侧节点
                if (x == rightOf(parentOf(x))) {
                	//插入节点为父节点
                    x = parentOf(x);
                    //进行插入节点左旋
                    rotateLeft(x);
                }
                //设置父节点为黑色
                setColor(parentOf(x), BLACK);
                //设置祖父节点为红色
                setColor(parentOf(parentOf(x)), RED);
                //进行祖父节点右旋
                rotateRight(parentOf(parentOf(x)));
            }
        } else {//父节点是祖父节点的右侧节点
        	//获取叔叔节点
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            //如果叔叔节点是红色,操作同上
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {//如果叔叔节点是黑色
            	//判断插入节点是否为父节点的左侧节点
                if (x == leftOf(parentOf(x))) {
                	//父节点变为插入节点
                    x = parentOf(x);
                    //插入节点右旋
                    rotateRight(x);
                }
                //父节点变为黑色
                setColor(parentOf(x), BLACK);
                //祖父节点变为红色
                setColor(parentOf(parentOf(x)), RED);
                //祖父节点进行左旋
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    //根节点变为黑色
    root.color = BLACK;
}

3.2、HashMap

JDK1.7及之前是采用数组加链表的形式实现,JDK1.8之后采用数组加链表或者数组加红黑树进行元素的存储,存储在HashMap集合的元素都将是一个Map.Entry的内部接口实现

HashMap存储数据的流程如下:

  1. 计算Key的哈希值,放入对应的数组位置
  2. 哈希值会出现哈希冲突,相同哈希的key都放在一个双向链表中
  3. 为了提升效率,当链表长度超过8的时候,链表就会转换为一个红黑树提升查询效率。

在这里插入图片描述
初始化数值如下:

	//hashMao默认数组长度
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
	//HashMap中的数组的最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
	//默认扩容平衡因子(数组达到容量的0.75时开始扩容)
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //链表转红黑树的临界值
    static final int TREEIFY_THRESHOLD = 8;
    //红黑树转链表的临界值
    static final int UNTREEIFY_THRESHOLD = 6;
    //链表转红黑树的数组长度的临界值
    static final int MIN_TREEIFY_CAPACITY = 64;

	//HashMap中的数组结构
    transient Node<K,V>[] table;
	//HashMap中的元素个数
    transient int size;
	//对HashMap的操作次数
    transient int modCount;
	//扩容临界值
    int threshold;
	//负载系数
    final float loadFactor;

put方法

  1. 获取key对应的hash数值
  2. key.hashCode()得到一个长度为32位的hash数值,将h右移16位,相当于取前16位,与h做与操作,这样做的目的是让散列更加均匀
  3. 进行初始化判断,如果数组为空或者长度为0,就进行初始化
  4. 确定新插入的Key在数组中的下标
  5. 假如这个下标位置是空的,就直接插入数值。
  6. 如果新旧值哈希相同,新旧值的key也相等,且key不为空,且key与k相等,证明为同一个key,直接修改
  7. 如果数组内存放的是红黑树节点,插入红黑树节点
  8. 都不是,那就只可能是普通链表,插入链表尾部
  9. 是否满足链表转换红黑树的条件,满足则转换
public V put(K key, V value) {
	//获取key对应的hash数值
    return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    //key.hashCode()得到一个长度为32位的hash数值
    //将h右移16位,相当于取前16位,与h做与操作
    //这样做的目的是让散列更加均匀
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //初始化判断
    if ((tab = table) == null || (n = tab.length) == 0)
    	//初始化数组,n数组长度,resize()扩容
        n = (tab = resize()).length;
    //确定新插入的Key在数组中的下标
    //假如这个下标位置是空的,就直接插入数值。
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {//假如这个下标位置是有数值的
        Node<K,V> e; K k;
        //如果新旧值哈希相同
        //新旧值的key也相等
        //且key不为空,且key与k相等,证明为同一个key
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //直接修改
            e = p;
        else if (p instanceof TreeNode)//如果数组内存放的是红黑树节点
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {//表示节点就是就是链表
            for (int binCount = 0; ; ++binCount) {
            	//如果p节点下一个节点是空的,证明到了尾部
                if ((e = p.next) == null) {
                	//将新的节点添加到尾部
                    p.next = newNode(hash, key, value, null);
                    //是否满足链表转换红黑树的条件
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    	//转换红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
  • 初始数组长度为16
  • 扩容因子为0.75
  • 扩容阈值为当前数组大小的3/4,即当前数组长度与扩容因子的乘积
  • 扩容一次数组长度翻倍,扩容阈值翻倍
//扩容函数
final Node<K,V>[] resize() {
	//当前数组赋予旧数组
    Node<K,V>[] oldTab = table;
    //获取旧数组长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //获取旧数组扩容临界值
    int oldThr = threshold;
    int newCap, newThr = 0;
    //若旧长度大于0
    if (oldCap > 0) {
    	//检测是否超过了hashmap的数组上限
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //新容量是原来容量的两倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //扩容的临界值是原来的两倍
            newThr = oldThr << 1; 
    }
    else if (oldThr > 0) // 旧临界值大于0且旧长度不大于0
    	//新容量为旧临界值
        newCap = oldThr;
    else {//如果旧数组长度为0
    	//新数组长度为默认长度16
        newCap = DEFAULT_INITIAL_CAPACITY;
        //新数组扩容临界值为默认扩容因子和默认长度的乘积
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //如果新临界值为0
    if (newThr == 0) {
    	//新长度与系数的乘积
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //更新扩容临界值
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //创建一个容量为新数组容量的新数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //新数组赋值给当前数组
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
//红黑树转换函数
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //tab为空或者数组的长度小于64
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    	//扩容
        resize();
    //如果数组中计算出的下标位置不为空
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        //链表转换红黑树
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值