java集合看这一篇就够了!我说的

Collection结构概览

在这里插入图片描述

Collection

list和set接口都是继承了collection接口,当然可以使用父类的一些方法
这里就探讨下collection里一些常用的方法
1、添加

add(Object obj)
addAll(Collection coll)

2、获取有效元素的个数

 int size()

3、清空集合

 void clear()

4、是否是空集合

 boolean isEmpty()

5、是否包含某个元素
boolean contains(Object obj):是通过元素的equals方法来判断是否
是同一个对象
boolean containsAll(Collection c):也是调用元素的equals方法来比
较的。拿两个集合的元素挨个比较。
6、删除
boolean remove(Object obj) :通过元素的equals方法判断是否是
要删除的那个元素。只会删除找到的第一个元素
boolean removeAll(Collection coll):取当前集合的差集
7、取两个集合的交集
boolean retainAll(Collection c):把交集的结果存在当前集合中,不
影响c
8、集合是否相等

boolean equals(Object obj)

9、转成对象数组

Object[] toArray()

10、获取集合对象的哈希值

hashCode()

11、遍历
iterator():返回迭代器对象,用于集合遍历

collection集合的遍历方式

collection是个接口没法实例化我们用多态的形式,通过它的子类arrarylist来展示collection的遍历方式

  • 迭代器
    public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        Collections.addAll(list,5,3,1,4);
        Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()){
            System.out.print(iterator.next());
        }
    }
  • 增强for循环
    public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        Collections.addAll(list,5,3,1,4);
        for(Integer num:list){
            System.out.print(num);
        }
    }

lambda表达式

    public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        Collections.addAll(list,5,3,1,4);
        list.forEach(System.out::println);
    }

iterator迭代器接口

  • Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。
  • Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
  • 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
    对于iterator迭代器
iterator迭代器的方法

boolean hasNext()如果迭代具有更多的元素,则返回true 。 (换句话说,如果next()返回一个元素而不是抛出一个异常,则返回true )
E next()返回迭代中的下一个元素。
结果
迭代中的下一个元素
异常
NoSuchElementException - 如果迭代没有更多的元素
可能对于迭代器的指针有很多的看法,大致分为,指针是在第一个元素前边,或者在第一个元素上

我认为的是指针是在第一个元素之前,根据上边的iterator 的两个方法可以看出,在调用it.next()方法之前必须要调用it.hasNext()进行检测。若不调用,且
下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常,如果此时iterator的指针是在第一个元素的时候,集合的第一个元素可能就无法被遍历

default void remove()

Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方法,不是集合对象的remove方法。如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,再调用remove都会报IllegalStateException。

  1. remove()将会删除上次调用next()时返回的元素,也就是说先调用next()方法,再调用remove方法才会删除元素。next()和romove方法具有依赖性,必须先用next,再使用romove。如果先用remove方法会出现IllegalStateException异常。
  2. 使用remove()方法必须紧跟在next()之后执行,如果在remove和next中间,集合出现了结构性变化(删除或者是增加)则会出现异常IllegalStateException。
List系列
List系列集合特点
  • ArrayList、LinekdList:有序,可重复,有索引。
  • 有序:存储和取出的元素顺序一致
  • 有索引:可以通过索引操作元素
  • 可重复:存储的元素可以重复
List的实现类的底层原理
  • ArrayList 的底层实现
    它是一个动态数组,实现了 List 接口以及 list相关的所有方法,它允许所有元素的插入,包括 null。
  1. 属性
//默认容量的大小
private static final int DEFAULT_CAPACITY = 10;
//空数组常量
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认的空数组常量
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMEN
TDATA = {};
//存放元素的数组,从这可以发现 ArrayList 的底层实现就是一个 Object
数组
transient Object[] elementData;
//数组中包含的元素个数
private int size;
//数组的最大上限
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALU
E - 8;

ArrayList 的属性非常少,就只有这些。其中最重要的莫过于 elementData 了,ArrayList所有的方法都是建立在 elementData 之上。接下来,我们就来看一下一些主要的方法吧。

  1. 构造方法
    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 Capacit
y: "+initialCapacity);
 }
}
public ArrayList() {
 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

可以看出当使用空参构造器的时候就会初始化一个空的object数组,当调用有参构造器的时候就会初始化为我们参数的大小

  1. get方法
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];
 }

ArrayList的底层使用数组结构来实现的,所以get方法特别简单,先判断索引是否越界,没有越界的话直接通过索引获取就好了

  1. add方法
public boolean add(E e) {
 ensureCapacityInternal(size + 1); // Increments modCou
nt!!
 elementData[size++] = e;
 return true;
}
public void add(int index, E element) {
 rangeCheckForAdd(index);
 ensureCapacityInternal(size + 1); // Increments modCou
nt!!
 //调用一个 native 的复制方法,把 index 位置开始的元素都往后挪一位
 System.arraycopy(elementData, index, elementData, inde
x + 1, size - index);
 elementData[index] = element;
 size++;
}
private void ensureCapacityInternal(int minCapacity) {
 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacit
y);
 }
 ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
 modCount++;
 if (minCapacity - elementData.length > 0)
 grow(minCapacity);
}

ArrayList 的 add 方法也很好理解,在插入元素之前,它会先检查是否需要扩容,然后再把元素添加到数组中最后一个元素的后面ensureCapacityInternal 方法中,我们可以看见,如果当 elementData 为空数组时,它会使用默认的大小去扩容。所以说,通过无参构造方法来创建 ArrayList 时,它的大小其实是为 0 的,只有在使用到的时候,才会通过 grow 方法去创建一个大小为 10 的数组。第一个 add 方法的复杂度为 O(1),虽然有时候会涉及到扩容的操作,但是扩容的次数是非常少的,所以这一部分的时间可以忽略不计。如果使用的是带指定下标的 add方法,则复杂度为 O(n),因为涉及到对数组中元素的移动,这一操作是非常耗时的。

  1. set方法
public E set(int index, E element) {
 rangeCheck(index);
 E oldValue = elementData(index);
  elementData[index] = element;
 return oldValue;
}

这个方法也比较简单,先去检查索引是否越界,然后将旧值保存起来,将新值赋给旧值,在将旧值返回

  1. remove方法
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 wo
rk
 return oldValue;
}

同样的也是根据索引将数组别的元素移动,然后将末尾的元素置空

  1. 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 wi
n:
 elementData = Arrays.copyOf(elementData, newCapacity);
}

grow 方法是在数组进行扩容的时候用到的,从中我们可以看见,ArrayList 每次扩容都是扩 1.5 倍,然后调用 Arrays 类的 copyOf 方法,把元素重新拷贝到一个新的数组
中去。

  1. indexof() lastindexof()
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;
}
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;
 }

即为遍历数组需寻找,返回索引值,lastindexof就是从后往前寻找罢了

  • LinkedList
    linkedlist底层是双向链表,链表不同于数组,数组通过索引查找元素很快,插入删除操作会速度略差,因为涉及到元素的移动,链表的插入删除效率就好点,且在头尾结点都有一个指针,所以对头尾结点的操作很快,很适合用来模拟队列或者栈
List集合特有的方法

在这里插入图片描述

List集合的遍历

由于list集合是支持索引的所以对于list集合的遍历,除了collection中的遍历方式,也可以通过for循环

public static void main(String[] args) {
    List<Integer> list=new ArrayList<>();
    Collections.addAll(list,5,3,1,4);
    for (int i = 0; i < list.size(); i++) {
        System.out.print(list.get(i));
    }
}
LinkedList的特点

在这里插入图片描述
双向链表即一个结点中会有前一个结点的地址,data,后一个结点的地址,头尾的操作是很快的,这里就不在赘述

并发修改异常问题

在这里插入图片描述
list集合在进行删除操作的时候会伴随着数组的移动操作,所以才会出现这样的错误,所以在删除的时候我们可以用上文所诉的iterator的remove方法,或者倒叙遍历删除,同样可以避免这个异常问题。
底层是list在删除的时候会检测size,如果变动的话就会报这个错误的,底层源码这里就不在论述了

Set系列
Set系列集合特点:
  • 无序:存取顺序不一致
  • 不重复:可以去除重复
Set集合实现类特点:
  • HashSet:无序、不重复、无索引
  • LinkedHashSet:有序、不重复、无索引
    *TreeSet:排序、不重复、无索引
hashset底层原理

hashset底层是基于哈希表来实现的
在jdk1.8之前哈希表是数组加链表的形式
1.8之后变为数组+链表+红黑树的形式 当链表长度为8时自动转换为红黑树
红黑树是avl树的一种变体,也有研究过avl树的代码,增删改查的效率很高,红黑树自然不会差,相比于链表来说必然是很强的
附上哈希表的初始化步骤吧:
在这里插入图片描述
再有不懂的俺的数据结构专栏有哈希表的简单用法,虽然根本不能和jdk的比,但还是可以感受下简单的哈希表,是用数组加链表实现的

LinkedHashSet

在这里插入图片描述
大致相同于hashset的底层原理,不同得到是linkedhashset根据元素的插入顺序会有指针指向,用来记录插入的顺序
即它是有序的、不重复的、同样的也没有索引

TreeSet

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
一般来说使用有参构造器多一点,如果去将类实现comparable接口,会将类的比较固化。
这里放下有参构造器的示例代码

public static void main(String[] args) {
    //默认是将数字从小到大排序,因为integer类实现了comperable接口
    Set<Integer> list=new TreeSet<>(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2-o1;
        }
    });
    Collections.addAll(list,5,3,1,4);
    System.out.println(list);//5413

    }
使用场景的大概总结

在这里插入图片描述
但排序的话其实还是很少用treeset的,collections工具类的方法同样可以实现对list的排序,也是比较常用的

Map接口

在这里插入图片描述

map集合的概述

在这里插入图片描述

map集合体系特点

在这里插入图片描述

map集合常用api

在这里插入图片描述

map集合的遍历

在这里插入图片描述

public static void main(String[] args) {
    Map<String,Integer> map=new HashMap<>();
    map.put("cxf",18);
    map.put("xx",19);
    map.put("ff",20);
     //方式一
    Set<String> strings = map.keySet();
    for (String key:strings){
        System.out.println(key+"="+map.get(key));
    }
    //方式二
    Set<Map.Entry<String, Integer>> entries = map.entrySet();
    for (Map.Entry<String, Integer> entry : entries) {
        System.out.println(entry.getKey()+"="+entry.getValue());
    }
    //方式三
    map.forEach((k,v)-> System.out.println(k+"="+v));

}
实现类hashmap

在这里插入图片描述
在这里插入图片描述
HashMap 的大致结构如下图所示,其中哈希表是一个数组,我们经常把数组中的每一个节点称为一个桶,哈希表中的每个节点都用来存储一个键值对。在插入元素时,如果发生冲突(即多个键值对映射到同一个桶上)的话,就会通过链表的形式来解决冲突。因为一个桶上可能存在多个键值对,所以在查找的时候,会先通过 key 的哈希值先定位到桶,再遍历桶上的所有键值对,找出 key 相等的键值对,从而来获取 value。同样的jdk8以前和jdk8以后如果过一个桶中出现多个结点,挂载方式也是有所不同的,jdk8及以前是将旧元素挂载在新元素的下边,而jdk8及以后是将新元素,挂载在旧元素的下边,当链表的长度超过8时,链表转换为红黑树
底层的一些源码

  1. 属性
//默认的初始容量为 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大的容量上限为 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的负载因子为 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//变成树型结构的临界值为 8
static final int TREEIFY_THRESHOLD = 8;
//恢复链式结构的临界值为 6
static final int UNTREEIFY_THRESHOLD = 6;
//哈希表
transient Node<K,V>[] table;
//哈希表中键值对的个数
transient int size;
//哈希表被修改的次数
transient int modCount;
//它是通过 capacity*load factor 计算出来的,当 size 到达这个值时,
就会进行扩容操作
int threshold;
//负载因子
final float loadFactor;
//当哈希表的大小超过这个阈值,才会把链式结构转化成树型结构,否则仅采
取扩容来尝试减少冲突
static final int MIN_TREEIFY_CAPACITY = 64;

即初始数组的长度是16,默认的加载因子是0.75,变成树形结构的临界值时8

  1. Node定义
    node是hashmap的一个静态内部类,属性中有hash key value 和next指针
    内部代码也是一些常规代码放下边了就
static class Node<K,V> implements Map.Entry<K,V> {
 final int hash;
 final K key;
 V value;
 Node<K,V> next;
 Node(int hash, K key, V value, Node<K,V> next) {
 this.hash = hash;
 this.key = key;
 this.value = value;
 this.next = next;
 }
 public final K getKey() { return key; }
 public final V getValue() { return value; }
  public final String toString() { return key + "=" + valu
e; }
 public final int hashCode() {
 return Objects.hashCode(key) ^ Objects.hashCode(val
ue);
 }
 public final V setValue(V newValue) {
 V oldValue = value;
 value = newValue;
 return oldValue;
 }
 public final boolean equals(Object o) {
 if (o == this)
 return true;
 if (o instanceof Map.Entry) {
 Map.Entry<?,?> e = (Map.Entry<?,?>)o;
 if (Objects.equals(key, e.getKey()) &&
 Objects.equals(value, e.getValue()))
 return true;
 }
 return false;
 }
 }
  1. get方法
    get 方法主要调用的是 getNode 方法,所以重点要看 getNode 方法的
    实现
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(key)) == null ? null : e.value;
    }

    /**
     * Implements Map.get and related methods.
     *
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & (hash = hash(key))]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

实现步骤大致如下:
1、通过 hash 值获取该 key 映射到的桶。
2、桶上的 key 就是要查找的 key,则直接命中。
3、桶上的 key 不是要查找的 key,则查看后续节点:
(1)如果后续节点是树节点,通过调用树的方法查找该 key。
(2)如果后续节点是链式节点,则通过循环遍历链查找该 key。
简单点的就是通过hash值定位到,该结点在数组中的存储位置,然后通过树或者链表本身的getnode方法来进行查找,找到就返回。

  1. put方法
//put 方法的具体实现也是在 putVal 方法中,所以我们重点看下面的 pu
tVal 方法
 public V put(K key, V value) {
 return putVal(hash(key), key, value, false, true);
 }
 final V putVal(int hash, K key, V value, boolean onlyIf
Absent,
 boolean evict) {
 Node<K,V>[] tab; Node<K,V> p; int n, i;
 //如果哈希表为空,则先创建一个哈希表
 if ((tab = table) == null || (n = tab.length) == 0)
 n = (tab = resize()).length;
 //如果当前桶没有碰撞冲突,则直接把键值对插入,完事
 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.equ
als(k))))
 e = p;
 //如果是采用红黑树的方式处理冲突,则通过红黑树的 putTree
Val 方法去插入这个键值对
 else if (p instanceof TreeNode)
 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, 
hash, key, value);
 //否则就是传统的链式结构
 else {
 //采用循环遍历的方式,判断链中是否有重复的 key
 for (int binCount = 0; ; ++binCount) {
 //到了链尾还没找到重复的 key,则说明 HashMap 没有
包含该键
 if ((e = p.next) == null) { 
 
 //创建一个新节点插入到尾部
 p.next = newNode(hash, key, value, nul
l);
 //如果链的长度大于 TREEIFY_THRESHOLD 这个
临界值,则把链变为红黑树
 if (binCount >= TREEIFY_THRESHOLD - 1)
// -1 for 1st
 treeifyBin(tab, hash);
 break;
 }
 //找到了重复的 key
 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;
 }

put 方法比较复杂,实现步骤大致如下:
1、先通过 hash 值计算出 key 映射到哪个桶。
2、如果桶上没有碰撞冲突,则直接插入。
3、如果出现碰撞冲突了,则需要处理冲突:
(1)如果该桶使用红黑树处理冲突,则调用红黑树的方法插入。
(2)否则采用传统的链式方法插入。如果链的长度到达临界值,则把链转变为红黑树。
4、如果桶中存在重复的键,则为该键替换新值。
5、如果 size 大于阈值,则进行扩容。

  1. remove方法
//remove 方法的具体实现在 removeNode 方法中,所以我们重点看下面的 r
emoveNode 方法
public V remove(Object key) {
 Node<K,V> e;
 return (e = removeNode(hash(key), key, null, false, tru
e)) == null ?
 null : e.value;
 }
final Node<K,V> removeNode(int hash, Object key, Object va
lue,
 boolean matchValue, boolean movabl
e) {
 Node<K,V>[] tab; Node<K,V> p; int n, index;
 //如果当前 key 映射到的桶不为空
 if ((tab = table) != null && (n = tab.length) > 0 &&
 (p = tab[index = (n - 1) & hash]) != null) {
 Node<K,V> node = null, e; K k; V v;
 //如果桶上的节点就是要找的 key,则直接命中
 if (p.hash == hash && ((k = p.key) == key || (key != 
null && key.equals(k))))
 node = p;
 else if ((e = p.next) != null) {
 //如果是以红黑树处理冲突,则构建一个树节点
 if (p instanceof TreeNode)
 node = ((TreeNode<K,V>)p).getTreeNode(hash, 
key);
 //如果是以链式的方式处理冲突,则通过遍历链表来寻找节点
 else {
 do {
  if (e.hash == hash && ((k = e.key) == key 
|| (key != null && key.equals(k)))) {
 node = e;
 break;
 }
 p = e;
 } while ((e = e.next) != null);
 }
 }
 //比对找到的 key 的 value 跟要删除的是否匹配
 if (node != null && (!matchValue || (v = node.value)
== value ||
 (value != null && value.equals
(v)))) {
 //通过调用红黑树的方法来删除节点
 if (node instanceof TreeNode)
 ((TreeNode<K,V>)node).removeTreeNode(this, t
ab, movable);
 //使用链表的操作来删除节点
 else if (node == p)
 tab[index] = node.next;
 else
 p.next = node.next;
 ++modCount;
  --size;
 afterNodeRemoval(node);
 return node;
 }
 }
 return null;
}

步骤也很简单
1.先判断当前key映射的桶是否为空
2.如果桶上的结点就是要找的结点,则直接命中
3.判断是链式冲突,还是红黑树冲突,红黑树冲突的话要构建一个红黑树结点
4.如果是链式的就调用链式的查找方式去查找
5.通过equals判断key和value是否相同
6.如果是红黑树就调用红黑树的remove方法
7.如果是链表就调用链表的删除的方法

总结

1.数组加链表的方式当链表过长时对效率影响很大
2.数组加链表加红黑树 的方式大大提高了效率,属于用空间换时间了属于是。

实现类linkedhashmap

在这里插入图片描述
linkedhashmap大体上继承自hashmap多了些对链表的操作

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

相比于hashmap的node 它这里多了记录插入顺序的前、后指针
仔细看过hashmap源码的话你会发现有这三个空方法
同时

final boolean accessOrder;

该属性的打开或者关闭也表示linkefhashmap是否实现最近最少使用的算法

// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

实这三个方法表示的是在访问、插入、删除某个节点之后,进行一些处理,它们在 LinkedHashMap 都有各自的实现。LinkedHashMap 正是通过重写这三个方法来保证链表的插入、删除的有序性。
这里不在详细展开说

实现类treemap

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Quare_feifei

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值