基础集合1

容器

Hash表

散列表根据关键字值进行访问儿都数据结构,把关键字值映射到一个位置访问记录,加快查找的速度

避免hash冲突

拉链法

多个相同的key连到一个链表里

线性探测法

大小为M的数组保存N个键值对,M>N

概览

主要包括Collection和Map两种,Collection存储着对象的集合,Map存储着键值对的映射表

Collection

在这里插入图片描述

  1. Set
    • TreeSet :基于红黑树实现,支持有序操作,范围内查找元素。查找效率不如HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
    • HashSet:基于HashMap,支持快速查找,不支持有序性操作。失去元素插入顺序信息,使用Iterator遍历得到HashSet结果是不确定的
    • LinkedHashSet:具有HashSet查找效率,内部使用双向链表维护元素的插入顺序
  2. List
    • ArrayList,动态数组,支持随机访问
    • Vector:线程安全
    • LinkedList:双向链表,只能顺序访问,快速在链表中插入,删除元素。可以做栈,队列,双向队列
  3. Queue
    • LinkedList:双向队列
    • PriorityQueue:堆结构实现,优先队列

Map

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D2P1nvUb-1630569077526)(C:\Users\MECHREVCO\Desktop\新征程\pic\image-20210804164304145.png)]

  • TreeMap:红黑树
  • HashMap:哈希表
  • HashTable:线程安全,遗留类,应该使用ConcurrentHashMap
  • LinkedHashMap:双向链表维护元素顺序,顺序为插入顺序或最近最少使用(LRU)顺序

List源码

ArrayList

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JrY8vYgc-1630569077530)(C:\Users\MECHREVCO\Desktop\新征程\pic\image-20210804172310816.png)]

长度为10,从1开始计数;index表示数组下标,从0开始计数

  • DEFAULT_CAPACITY 数组初始长度 默认是10
  • size,当前数组大小,int ,没有volatile修饰,非线程安全的
  • modCount,记录被修改的版本次数,数组变动+1
类注释
  • 允许put null 会自动扩容
  • size,isEmpty,get,set,add等方法时间复杂度都是O(1);
  • 非线程安全的,synchronizedList
  • 增强for循环,或迭代器迭代时,数组大小被改变,会快速失败
初始化

三种初始化

  • 无参,数组大小为空

       //一开始为空,不是10
        public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }
    
  • 指定大小初始化

  • 指定初始数据初始化

添加 add与扩容

扩容本质

Arrays.copyOf()本地方法,先新建符合预期容量的新数组,老数组拷贝进去

  • 先确保内部容量,ensureCapacityInternal,判断是否为空,为空则默认长度,非空则+1

    private void ensureCapacityInternal(int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
            }
    
            ensureExplicitCapacity(minCapacity);
        }
    
  • 增加版本号,确保增加后的长度比现有长度长

        private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // 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 + (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); //复制扩容
    }

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

需要注意:

  1. 扩容不是翻倍,是原来1.5倍
  2. ArrayList数组最大值是 Integer.MAX_VALUE,超过这个值,JVM不会分配内存空间
  3. 新增时可以有null;
  4. 源码扩容时,有数组大小溢出意识
删除:
  • 值删除

        public boolean remove(Object o) {
            if (o == null) { //判断是否为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])) { //equals要看具体实现
                        fastRemove(index);
                        return true;
                    }
            }
            return false;
        }
    
        private void fastRemove(int index) {
            modCount++; //版本+1
            int numMoved = size - index - 1;
            if (numMoved > 0) //底层还是调用System.arraycopy
                System.arraycopy(elementData, index+1, elementData, index,
                                 numMoved);
            elementData[--size] = null; // clear to let GC do its work,最后一个位置为null帮助GC
        }
    
  • 下标删除

        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;
        }
    
  • 批量删除

迭代器
  • hasNext

    public boolean hasNext() {
     return cursor != size;//cursor 表示下一个元素的位置,size 表示实际大小,如果两者相等,说明已经没有元素可以迭代了,如果不等,说明还可以迭代
    }
    
  • next

    public E next() {
     //迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
     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();
    }
    
    
  • remove

    public void remove() {
     // 如果上一次操作时,数组的位置已经小于 0 了,说明数组已经被删除完了
     if (lastRet < 0)
     throw new IllegalStateException();
     //迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
     checkForComodification();
     try {
     ArrayList.this.remove(lastRet);
     cursor = lastRet;
     // -1 表示元素已经被删除,这里也防止重复删除
     lastRet = -1;
     // 删除元素时 modCount 的值已经发生变化,在此赋值给 expectedModCount
     // 这样下次迭代时,两者的值是一致的了
     expectedModCount = modCount;
     } catch (IndexOutOfBoundsException ex) {
     throw new ConcurrentModificationException();
     }
    }
    
线程安全

作为共享变量时是不安全的,使用SynchronizedList或vector,可以用CopyOnWriteArrayList保证线程安全,每个方法加了Synchronized

Vector

方法与ArrayList一样,加了synchronized修饰

扩容

是两倍不是1.5倍了

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);
}
ArrayList不同

vector是同步的,开销比ArrayList大,速度慢

LinkedList

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VNWYCLqf-1630569077534)(pic\image-20210810200813271.png)]

  • 每个节点叫node,有prev和next,first的prev为空,last的next为空
  • 没有数据时,last和first一个节点,前后都空
add
  • add&addLast都是从尾部插入

        void linkLast(E e) {
            final Node<E> l = last;
            final Node<E> newNode = new Node<>(l, e, null);
            last = newNode;
            if (l == null) //如果last为空表示目前链表里没东西
                first = newNode;
            else
                l.next = newNode;
            size++;
            modCount++;
        }
    
        void linkBefore(E e, Node<E> succ) {
            // assert succ != null;
            final Node<E> pred = succ.prev;
            final Node<E> newNode = new Node<>(pred, e, succ);
            succ.prev = newNode;
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            size++;
            modCount++;
        }
    
  • addFirst

        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++;
        }
    
remove

remove默认removeFirst还有removeLast

get
    Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) { //分成两部分 从first开始或者从last开始
            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;
        }
    }
迭代器

Iterator只支持从头到尾的访问,还有个迭代接口ListIterator,向前和向后的访问

  • 从头到尾:next,hasNext,nextIndex
  • 从尾到头:previous,hasPrevious,previousIndex

Map源码

HashMap

数组+链表+红黑树,链表长度>8,并且hashmap容量>64,才会转化为红黑树。红黑树大小<6时转化为链表,初始容量16,影响因子0.75,modcount会快速失败

数组查找复杂度O(1),链表O(n),红黑树O(log(n))

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E2jTs6FK-1630569077538)(pic\image-20210812173313513.png)]

类注释
  • 允许null
  • 很多数据时建议固定容量
  • 影响因子,均衡空间时间,值越大扩容减少,但是hash冲突增加

扩容有两种情况

  • 初始化时给定数组大小,通过tableSizeFor计算,数组大小永远接近2的幂次,给定19则实际初始化32
  • resize扩容则大小 = 数组容量*0.75
新增
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果hashmap为空或长度为0则resize
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
    //如果数组(buket)对应的下标内容为空直接赋值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
       //如果不为空,有对应的值
        else {
            Node<K,V> e; K k;
            //如果数组内容有值且key等于内容的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就只有是链表类型了
            else {
                for (int binCount = 0; ; ++binCount) {
                    //如果下一个为空则插入
                    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向后移
                    p = e;
                }
            }
            //如果e不为空,则说明已经新增位置已经找到
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //当onlyIfAbsent为false时才覆盖
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                //返回旧值
                return oldValue;
            }
        }
    //版本++
        ++modCount;
    //如果实际容量大于扩容门槛,开始扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
链表新增

链表长度>8,并且hashmap容量>64才会转化为红黑树

为什么是8,泊松分布,当链表长度为8的时候,出现的概率为 0.00000006,不到千万分之一

  1. 首先判断新增的节点在红黑树上是不是已经存在,判断手段有如下两种:

​ 1.1. 如果节点没有实现 Comparable 接口,使用 equals 进行判断;

​ 1.2. 如果节点自己实现了 Comparable 接口,使用 compareTo 进行判断。

  1. 新增的节点如果已经在红黑树上,直接返回;不在的话,判断新增节点是在当前节点的左边 还是右边,左边值小,右边值大;
  2. 自旋递归 1 和 2 步,直到当前节点的左边或者右边的节点为空时,停止自旋,当前节点即为 我们新增节点的父节点;
  3. 把新增节点放到当前节点的左边或右边为空的地方,并于当前节点建立父子节点关系;
  4. 进行着色和旋转,结束。
查找
  • 根据hash算法定位数组索引,,equals判断
  • 当前节点有没有next,有的话是链表还是红黑树
  • 走链表or红黑树查找方法

链表查找–自旋

do {
 // 如果当前节点 hash 等于 key 的 hash,并且 equals 相等,当前节点就是我们要找的节点
 // 当 hash 冲突时,同一个 hash 值上是一个链表的时候,我们是通过 equals 方法来比较
key 是否相等的
 if (e.hash == hash &&
 ((k = e.key) == key || (key != null && key.equals(k))))
 return e;
 // 否则,把当前节点的下一个节点拿出来继续寻找
} while ((e = e.next) != null);

红黑树查找

  • 根节点递归查找
  • hashcode比较左右节点,左小右大
  • 判断 有无定位,无的话重复23
  • 自旋定位

WeakHashMap

Entry继承WeakReference,被WeakReference 关联的对象在下一次垃圾回收时会被回收。

WeakHashMap 主要实现缓存,通过使用WeakHashMap 引用缓存对象,由JVM回收。

ConcurrentCache

Tomcat的ConcurrentCache就是WeakHashMap 实现

TreeMap

TreeMap底层数据结构就是红黑树

比较器,如果有外部传进来的Comparator比较器优先使用外部;如果外部比较器为空,则使用key自己实现的Comparable的compareTo方法

containsKey,get,put,remove方法时间复杂度都是log(n)

put
  public V put(K key, V value) {
        Entry<K,V> t = root;
      //判断根节点是否为空
        if (t == null) {
            //compare保证key不为null
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths,如果自带Comparator
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            //自旋,不断地找父节点,直到父节点为空
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                //如果相等,直接覆盖
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            //插入节点不能为空
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
            //自身的Comparable
                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);
        }
        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;
    }
  1. 新增节点时,利用红黑树的左小右大特性,从根节点往下查找,直到节点是null
  2. 查找时发现key已经存在直接覆盖
  3. TreeMap禁止key是null
get

一样的逻辑,先看比较器,再自旋查找

    public V get(Object key) {
        Entry<K,V> p = getEntry(key);
        return (p==null ? null : p.value);
    }
    
        final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        Entry<K,V> p = root;
        while (p != null) {
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
        return null;
    }
remove

之后补充

LinkedHashMap

HashMap无序的,TreeMap按照key排序,LinkedHashMap维护插入顺序

本身继承HashMap,还有两大特性:

  • 按照插入顺序进行访问
  • 实现访问最少最先删除,把很久没有访问的key自动删除
结构
    static class Entry<K,V> extends HashMap.Node<K,V> {
        //为node添加一个before和after
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

    private static final long serialVersionUID = 3801124242820219131L;

    /**
     * The head (eldest) of the doubly linked list.
     * 链表头
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * The tail (youngest) of the doubly linked list.
     * 链表尾
     */
    transient LinkedHashMap.Entry<K,V> tail;
   /**
     * 控制两种访问模式字段,默认false
     * true按照访问顺序,把经常访问的key放到队尾
     * false按插入顺序访问
     */
    final boolean accessOrder;
新增

新增继承hashmap的put,但是重写了newNode/newTreeNode方法。使新增节点加到链表尾部

    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        //如果last为空,链表为空,首尾节点相等
        if (last == null)
            head = p;
        //有数据建立新增与上个节点的前后关系  
        else {
            p.before = last;
            last.after = p;
        }
    }
按顺序访问

LinkedHashMap只提供单向访问,按照插入的顺序从头到尾。

通过迭代器访问,entrySet.Iterator;keySet.Iterator;valueSet.Iterator

LRU策略

Least recently used最近最少使用;经常访问的元素被追加到队尾,不常访问的数据靠近队头;通过设置删除策略,把头结点删除

//简单实现
public static void testAccessOrder() {
        // 新建 LinkedHashMap
        LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer,
                Integer>(4, 0.75f, true) {
            {
                put(10, 10);
                put(9, 9);
                put(20, 20);
                put(1, 1);
            }

            @Override
            protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
                return size() > 3;
            }
        };

        log.info("初始化:{}",JSON.toJSONString(map));
        Assert.requireNonEmpty(map.get(9));
        log.info("map.get(9):{}",JSON.toJSONString(map));
        Assert.requireNonEmpty(map.get(20));
        log.info("map.get(20):{}",JSON.toJSONString(map));
    }
//结果
14:14:24.126 [main] INFO  Demo - 初始化:{9:9,20:20,1:1}
14:14:24.130 [main] INFO  Demo - map.get(9){20:20,1:1,9:9}
14:14:24.130 [main] INFO  Demo - map.get(20){1:1,9:9,20:20}
实现方法

get方法中有一个是否开启LRU判断,get,getOrDefault,computeIfAbsent,merge,computeIfPresent

    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        //是否开启LRU,将当前key移到队尾
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }
删除策略

LinkedHashMap本身没有put方法,调用HashMap的put,有一个afterNodeInsertion实现删除

// 删除很少被访问的元素,被 HashMap 的 put 方法所调用
void afterNodeInsertion(boolean evict) { 
 // 得到元素头节点
 LinkedHashMap.Entry<K,V> first;
 // removeEldestEntry 来控制删除策略,如果队列不为空,并且删除策略允许删除的情况下,删除头节点
 if (evict && (first = head) != null && removeEldestEntry(first)) {
 K key = first.key;
 // removeNode 删除头节点
 removeNode(hash(key), key, null, false, true);
 }
}

Map的hash算法

    static final int hash(Object key) {
        int h;
        //先使用底层hashcode算法,计算key的hash值,然后将h向右移动16位使hash值更分散,再和异或时高16位和低16位都能参与计算
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

//元素下标位置,n是数组长度,一半都是2的n次
tab[i = (n - 1) & hash]
//数学上有个公式,b是2的幂次时 a%b = a & (b-1),而且&是本地方法

问题关于Map问题

  1. 如果在put时,数组中已经有key,不想替换value/取值时如果calue是空想返回默认值怎么办

    如果有key不想覆盖value,选择putIfAbsent,有一个onlyIfAbsent判断,一般put是false允许覆盖;

    返回默认就是getOrDefault

  2. 自定义对象作为Map的key时需要注意

    DTO是数据载体,如果是Hashmap,LinkedHashMap时要重写equals和hashcode方法

    如果是Treemap,需要实现Comparable接口

Set源码

HashSet

类注释
  • 底层基于hashmap,迭代时不能保证插入顺序
  • add/remove,contains,size等方法的耗时性能不会随着数据量的增加而增加,时间复杂度是O(1)
  • 线程不安全
  • 快速失败

234为List,Set,Map共同点

初始化
    private transient HashMap<E,Object> map;

    // map的value都是PRESENT
    private static final Object PRESENT = new Object();

    public HashSet(Collection<? extends E> c) {
        //容量计算,默认16,+1是确定期望值比扩容的阈值大1就不会扩容
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }

TreeSet

底层是TreeMap复用的两种思路

复用1

Treeset的add方法

//直接拿来用
public boolean add(E e) {
 return m.put(e, PRESENT)==null;
}
复用2

TreeSet定义想要的api,让TreeMap去实现
返回默认就是getOrDefault

  1. 自定义对象作为Map的key时需要注意

    DTO是数据载体,如果是Hashmap,LinkedHashMap时要重写equals和hashcode方法

    如果是Treemap,需要实现Comparable接口

Set源码

HashSet

类注释
  • 底层基于hashmap,迭代时不能保证插入顺序
  • add/remove,contains,size等方法的耗时性能不会随着数据量的增加而增加,时间复杂度是O(1)
  • 线程不安全
  • 快速失败

234为List,Set,Map共同点

初始化
    private transient HashMap<E,Object> map;

    // map的value都是PRESENT
    private static final Object PRESENT = new Object();

    public HashSet(Collection<? extends E> c) {
        //容量计算,默认16,+1是确定期望值比扩容的阈值大1就不会扩容
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }

TreeSet

底层是TreeMap复用的两种思路

复用1

Treeset的add方法

//直接拿来用
public boolean add(E e) {
 return m.put(e, PRESENT)==null;
}
复用2

TreeSet定义想要的api,让TreeMap去实现

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值