Java集合

概述

  • 面向对象语言对事物的提现是以对象的形式,为了方便对多个对象的操作,就要对对象进行储存。但JAVA当中的存储对象方面的数组存在弊端,针对此问题Java提供了另一种容器(集合),可以动态的把多个对象的引用放入容器。

  • Java数组的特点:

    • 初始化长度确定
    • 需要指定元素类型
    • 对于添加、删除、插入数据等操作,非常不便,同时效率不高。
    • 有序、可重复,对于无序、不重复的需求,无法满足。
  • 集合优点:

    • 解决数组的弊端

Collection 接口

  • 描述: 单列集合,用来储存对象

  • 子类(实现类):

    • List(接口): 有序的,可重复的数据

      • ArrayList、LinkedList、Vector
    • Set(接口): 无序的,不可重复的数据

      • HashSet、LinkedHashSet、TreeSet

常用方法

    int size(); // 返回集合长度

    boolean isEmpty();  // 判断集合是否为空

    boolean contains(Object obj); // 当前集合是否包含obj

    boolean containsAll(Collection<?> c); // 判断c中的所有元素是否都在当前集合中

    Object[] toArray(); // 将集合转换为数组对象

    boolean add(E e);  // 向集合中添加新对象

    boolean addAll(Collection<? extends E> c); // 添加另一个集合

    void clear(); // 删除集合中所有的元素

    boolean remove(Object o); // 从集合中删除指定的对象

    boolean removeAll(Collection<?> c); // 从当前集合中删除与c集合相同的所有元素(差集)

    boolean retainAll(Collection<?> c); // 从当前集合中找出找到c集合相同的元素(交集)

    boolean equals(Object o); // 判断集合是否相等(如何有序的话,顺序也必须一致)

    int hashCode(); // 得到当前对象的hashCode值
    Iterator<E> iterator(); // 返回Iterator实例,用于集合遍历

注意事项

contains(Object obj)方法:
contains(Object obj)是否包含对象obj,判断方式取决于obj对象是否重写equals()方法,如果重写,则比较的为内容,如果没有重写,则比较的是地址值

集合当中如果需要遍历当中删除:iterator(迭代器)的remove方法。在删除之前需要先调用iterator.next()方法,将指针下移才能使用remove方法不然会抛异常

面试题

	@Test
	public void test3 () {
		int[] a = {1,2,3,4,5,6,7,8,9};
		for (int i = 0; i < a.length; i++) {
			a[i] = 0;
		}
		
		for (int i = 0; i < a.length; i++) {
			System.out.print(a[i]); // {000000000}
		}
		
	}
	
	@Test
	public void test4() {
        int[] a = {1,2,3,4,5,6,7,8,9};
		for (int i : a) {
			i = 1;
		}
		
		for (int i = 0; i < a.length; i++) {
			System.out.print(a[i]);{123456789}
		}
	}

for循环会改变原有的数组。
forEach则不会。

List接口

概述:有序的,可重复的数据。可用于替换原有的数组

经典面试题:

ArrayList、LinkedList、Vector三者的异同?

  • 同: 三个类都是实现了List接口,存储数据的特点相同: 储存有序的、可重复的数据

  • 异:

    • ArrayList: 线程不安全,执行效率高;底层使用Object[]存储

    • LinkedList: 对于频繁的插入、删除操作,使用此类的效率比ArrayList高;底层使用双向链表储存

    • Vector: 线程安全,执行效率低;底层使用Object[]存储

常用方法:

List接口继承自Collection,Collection的方法都有,这里就不重复介绍了

    void add (int index,Object obj); // 在index位置插入元素 obj

    boolean addAll(int index,Collection eles); //从index位置开始将eles中的所有元素添加进来

    Object get(int index); // 获取指定index位置的元素

    int indexOf(Object obj); // 返回obj在当前集合中首次出现的位置

    int lastIndexOf(Object obj); // 返回ovj在当前集合中最后一次出现的位置

    Object remove(int index); // 移除指定index位置的元素,并返此元素

    Object set(int index,Object ele); // 设置指定index位置的元素为ele

    List subList(int formIndex,int toIndex); // 返回从fromIndex到toIndex位置的子集合 

ArrayList

底层使用Object[]存储
源码:

jdk7:

构造器:

    // 初始化一个空数组,长度为10的Object[] elementData
    public ArrayList () {
        this (10);
    }

    public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0) 
            throw new IllegalArgumentException("Illegal Capacity:" + initialCapacity);
        this.elementData = new Object[initialCapacity];
        
    }

主要方法:

    public boolean add (E e) {
        ensureCapacotyInternal(size + 1); // 判断长度是否够用
        elementData[size++] = e;
        return true;
    }

    private void ensureCapacotyInternal (int minCapacity) {
        modCount++;
        if (minCapacity - elementData.length > 0) { // 长度比较,现有长度是否够用
            grow(minCapacity)
        }
    }

    private void grow (int minCapacity) {
        int oldCapacity = elementData.length; // 记录本身的长度
        int newCapacity = oldCapacity + (oldCapacity >> 1); //新的长度(默认情况下扩容为1.5倍)
        if (newCapacity - minCapacity < 0) 
            // 如果长度还是不够,那么扩容长度直接为需求的长度
            newCapacity = minCapacity
        
        if (newCapacity - MAX_ARRAY_SIZE > 0) 
            // 如果长度达到了预设的最大值
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity); // 复制
    }

    /**
    * 如果长度大于预定最大值则返回整型数的最大值
    **/
    private static int hugeCapacity (int minCapacity) {
        if (minCapacity < 0) 
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ? Interger.MAX_VALUE : MAX_ARRAY_SIZE;
    }
    

总结:

ArrayList list = new ArrayList();

底层创建默认长度为10的Object[]数组ElementData。

list.add(“test”);

如果本次的添加导致底层elementData数组容量不够,则扩容。

默认情况下,扩容为原来容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。

结论: 建议开发中如果能够确定长度,推荐使用带参的构造器可提高效率:

public ArrayList(int initialCapacity) {
}

jdk 8

  • 构造器:
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
        public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }
    

初始化时,不再给长度只申明一个空的数组。

  • add方法:

        public boolean add (E e) {
        ensureCapacotyInternal(size + 1); // 判断长度是否够用
        elementData[size++] = e;
        return true;
        }
    
        private void ensureCapacotyInternal (int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA ) { 
                minCapacity = Math.max (DEFAULT_CAPACITY,minCapacity); 
            }
            ensureExplicitCapacity(minCapacity);
        }
        private void ensureExplicitCapacity (int minCapacity) {
            modCount++;
            if (minCapacity - elementData.length > 0) {
                grow(minCapacity)
            }
        }
        private void grow (int minCapacity) {
            int oldCapacity = elementData.length; // 记录本身的长度
            int newCapacity = oldCapacity + (oldCapacity >> 1); //新的长度(默认情况下扩容为1.5倍)
            if (newCapacity - minCapacity < 0) 
                // 如果长度还是不够,那么扩容长度直接为需求的长度
                newCapacity = minCapacity;
            
            if (newCapacity - MAX_ARRAY_SIZE > 0) 
                // 如果长度达到了预设的最大值
                newCapacity = hugeCapacity(minCapacity);
            elementData = Arrays.copyOf(elementData, newCapacity); // 复制
        }
    

总结

ArrayList list = new ArrayList();

底层并没有创建为十长度的Object[],elementData初始化为{}

list.add(“test”)

当一次调用add方法时,底层才创建了长度10的数组。
其余操作与7无异。

总结:

  1. jdk7中的ArrayList初始化类似于单例模式饿汉式,初始化则给长度。
  2. jdk8中的ArrayList初始化类似于单例模式中懒汉式,初始化并没有给定长度,而是当调用的添加时才会给定长度

面试题

    @Test
    public void test(){
        List list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        updateList(list);
        System.out.println(list);
    }

    public void updateList(List list) {
        list.remove(2);
    }

结果为:[1,2]

此题考查对于list.remove()方法,放入数字参数时,是按照下标还是按照值来删除。
在添加时List会自动装箱,但删除的时候直接输入数字却不会自动装箱,需要我们手动来进行。

LinkedList

LinkedList存储结构为链式,一个数据划分为三部分:

  1. 上一个的值 (如果是第一个则为null)
  2. 本数组的值 (即我们储存的值)
  3. 下一个的值

源码:

    public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable
    {
        transient int size = 0;

        /**
        * Pointer to first node.
        */
        transient Node<E> first;

        /**
        * Pointer to last node.
        */
        transient Node<E> last;
        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;
            }
        }
    }

添加方法:

    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); // 参数(上一个元素,需要添加的元素,下一个元素),由于为添加方法,所以最后一部分为null。
        last = newNode; // 最后一个元素等于当前的元素
        if (l == null) 
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

总结

LinkedList list = new LinkedList();

内部声明了Node类型的first和last属性,默认值为null

list.add(123); // 将123封装到Node中,创建了Node对象

Node定义为: 体现了双向列表的说法

    Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next; // 上一个
            this.prev = prev; // 下一个
    }
Vector

通过构造器创建时,创建了长度为10的数组。扩容长度为原来的2倍。


Set接口

特点:

  1. 无序:不等于随机性。存储数据在底层数据中的顺序并非按照数组索引的先后顺序,而是按照数据的哈希值决定。

  2. 不可重复:保证添加的元素按照equals()判断时,返回值不为true

子类:

  1. HashSet:

    • 线程不安全
    • 初始化长度为16
    • 可以存储null值
    1. 子类LinkedHashSet

方法与Collection接口一致

添加过程:

首先调用添加元素的hashCode方法得到hash值,通过某种算法计算出在HashSet底层数组中的存放位置。

判断数组此位置是否有元素,如果此位置上没有元素,则元素直接添加成功。

如果有其它元素(或以链表形式存在的多个元素),则比较元素与其他元素的hash值:

如果Hash值不相同,则直接添加

如果hash值相同,equals()方法,如果都为false,则添加成功,如果一个为true,则添加失败

注意: 向Set中添加的数据,需要重写equals与hashCode方法。

面试题

去重复

    public static void main(String[] args){
        List list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(4);
        Set set = new HashSet();
        set.addAll(list);
        list = new ArrayList(set);
    }

利用了set当中的不可重复性来达到目的,如果去除的是自定义类,那么要求自定义类当中重写hashCode与equals方法

    @Test
    public void test() {
        HashSet set = new HashSet():
        Person p1 = new Person(1001,"AA");
        Person p2 = new Person(1002,"BB");
        set.add(p1);
        set.add(p2);
        System.out.println(set); // [{1001,"AA"},{1002,"BB"}]
        
        p1.name = "CC";
        set.remove(p1);
        System.out.println(p1); // [{1001,"CC"},{1002,"BB"}]

        set.add(new Person(1001,"CC")); // [{1001,"CC"},{1002,"BB"},{1001,"CC"}]

        set.add(new Person(1001,"AA")); //  [{1001,"CC"},{1002,"BB"},{1001,"CC"},{1001,"AA"}]
    }

解释

当p1.name修改为CC时,只是将p1.name修改为CC

当remove方法被执行时,那么set会重新计算hashCode值,而set集合根据对象的hashCode值来确定位置.
那么remove方法实际上寻找的对象为{10001,“CC”}这个对象算出来的hashCode值位置。而我们set集合当中现有的{10001,“CC”}的hashCode值为{1001,“AA”}算出来的地址值,所以会找不到这个地址值。

add(new Person(1001,“CC”);由于当前集合中的{10001,“CC”}位置是由{1001,“AA”}的hashCode值得到的,与{10001,“CC”}的hashCode值很大概率会不同,那么得到的位置也会不同,所以直接放入。

set.add(new Person(1001,“AA”)); 由于当前集合中的{10001,“CC”}位置是由{1001,“AA”}的hashCode值得到的,那么两个元素的hashCode值相同,调用equlas比较内容,显然是false,所以可以插入

  1. LinkedHashSet

    优点: 与LinkedList一致,对于频繁的插入、删除操作效率高于HashSet。底层采用双向链表结构。


TreeSet

特点:

  1. 可以按照添加对象的指定属性,进行排序

  2. 向TreeSet中添加的数据,要求是相同类的对象

  3. TreeSet判断是否重复不再使用equals、hashCode方法,而是ComparaTo方法的返回值,如果返回0(两个对象某个值相同)那么则认为这个对象相同。

  4. 排序方式可以使用自然排序与定制排序

Map

特点:

  1. 双列数据,储存key-value对的数据

  2. key不可重复,使用Set储存所有的key

  3. value无序的、可重复,使用Collection储存所有的value

  4. 底层使用Entry对象存储key-value,Entry也是无序的、不可重复的

HashMap

特点:

  1. Map主要实现类
  2. 线程不安全,效率高
  3. 可以储存null的key与value
  4. 底层:
    • 数组+链表 jdk7
    • 数组+链表+红黑树 jdk8
  5. key要求所在的类要重写equals()和hashCode(),value所在类要重写equals()

主要方法:

    V put(K key, V value); // 将指定key-value 添加到(或修改)当前map对象中
    void putAll(Map<? extends K, ? extends V> m); // 将m中所有的key-value对,并返回value
    V remove(Object key); // 删除指定key的key-value对,并返回value
    void clear(); // 清空当前map中的所有数据
    V get(Object key); // 获取指定key对应的value
    boolean containsKey(Object key); // 是否包含指定的key
    boolean containsValue(Object value); // 是否包含指定的value
    Set<K> keySet(); // 返回所有key构成的Set集合
    Collection<V> values(); // 返回所有value构成的 Collection集合
    Set<Map.Entry<K, V>> entrySet(); // 返回所有key-value对构成的Set集合

面试题:

  1. HashMap的底层实现原理
  • jdk7:
    • 在实例化以后,底层创建了长度是16的一维数组Entry[] table

    • 调用key1所在类的hashCode方法计算key1哈希值,此哈希值经过某种算法计算以后,得到Entry数组中的存放位置

      • 如果此位置上的数据为空,此时的key1-value1添加成功
      • 如果此位置上的数据不为空(此位置存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个的哈希值:
        • 如果key1的哈希值与已经存在的哈希值都不相同,此时key1-value1添加成功
        • 如果keu1的哈希值和已经存在的某一个数据的哈希值相同,继续比较:
          • 调用key1所在类的equals()方法,比较:
            • 如果equals返回false:则添加成功
            • 如果equals返回true:使用value1替换相同key的value值
    • 如超出临界值(且存放的位置为空时)默认扩容为原来的2倍,并将原有的数据复制

    HashMap map = new HashMap();
    map.put(key1,value1);
  • jdk8:

    不同:

    1. new HashMap():底层没有创建一个长度为16的数组
    2. 底层的数组是: Node[],而非Entry[]
    3. 首次调用put()方法,底层创建长度为16的数组
    4. jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树
      • 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度>64时,此时此索引位置上的所有数据改为使用红黑树存储

负载因子(加载因子)的大小,对HashMap有什么影响?

  • 负载因子的大小决定了HashMap的数据密度。
  • 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
  • 负载因子越小,就越容易出发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。经常扩容也会影响性能,建议初始化预设大一点的空间。
  • 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7-0.75,此时平均检索长度接近与常数

源码分析:

HashMap源码中的重要常量:

名称含义
DEFAULT_INITIAL_CAPACITYHashMap的默认容量,16
MAXIMUM_CAPACITYHashMap的最大支持容量,2^30
DEFAULT_LOAD_FACTORHashMap的默认加载因子,0.75
TREEIFY_THRESHOLDBucket中链表长度大于该默认值,转换为红黑树
UNTREEIFY_THRESHOLDBucket中红黑树储存的Node小于该默认值,转化为链表
MIN_TREEIFY_CAPACITY桶中的Node被树化时最小的hash表容量:64(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)
table存储元素的数组,总是2的n次幂
entrySet存储具体元素的集
sizeHashMap中存储的键值对的数量
modCountHashMap扩容和结构改变的次数
threshold扩容的临界值,=容量 * 填充因子:16 * 0.75
loadFactor填充因子

jdk7:

 /**
   * The default initial capacity - MUST be a power of two.
   */
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

 /**
   * The load factor used when none specified in constructor.
   */
 static final float DEFAULT_LOAD_FACTOR = 0.75f;
 /**
   * The maximum capacity, used if a higher value is implicitly specified
   * by either of the constructors with arguments.
   * MUST be a power of two <= 1<<30.
   */
 static final int MAXIMUM_CAPACITY = 1 << 30;
 /**
   * The next size value at which to resize (capacity * load factor).
   * @serial
   */
 // If table == EMPTY_TABLE then this is the initial capacity at which the
 // table will be created when inflated.
 int threshold;

 /**
   * The table, resized as necessary. Length MUST Always be a power of two.
   */
 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
 public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
 }

 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)Math.min(capacity * loadFactor,MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();

 }

 public V put(K key,V value) {
     if (key == null) 
         return putForNullkey(value);
     int hash = hash(key);
     int i = indexFor(hash,table.length);
     for(Entry[K,V] e = table[i];e != null; e = e.next){
         Object k;
         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
             V oldValue = e.value;
             e.value = value;
             e.recordAccess(this);
             return oldValue;
         }
     }
     modCount++;
     addEntry(hash,key,value,i);
     return null;
 }

jdk8:


public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
}

public V put(K key,V value){
    return putVal(hash(key),key,value,false,true);
}

static final int hash (Object key) {
    int h ;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
}
  1. HashMap和Hashtable的异同

  2. CurrentHashMap与Hashtable的异同

LinkedHashMap

特点:

  1. 在遍历Map元素时,可以按照添加的顺序实现遍历

    • 在原有的HashMap底层结构基础上,添加了一对指针,指向前一个与后一个
  2. 对于频繁的遍历操作,此类执行效率高于HashMap

  3. 底层实现原理

    • LinkedHashMap底层使用的结构与HashMap相同,因为LinkedHashMap区别就在于:LinkedHashMap内部提供了Entry,替换HashMap中的Node.

TreeMap

特点:

  1. 按照添加的key-value对进行排序
  2. 按照key进行自然排序或定制排序
  3. 底层使用红黑树

HashTable

特点:

  1. 线程安全的,效率低
  2. 不能储存null的key和value

Properties

特点:

  1. 常用于处理配置文件
  2. key和value都是String类型

Collections

操作Collection和Map的工具类

面试题:
Collections与Collection的区别

  1. Collections是Collection和Map的工具类
  2. Collections是类,Collection是接口

常用方法:

 public static void reverse(List<?> list)  //反转list中元素的顺序
 public static void shuffle(List<?> list) // 对list集合元素进行随机排序
 public static <T extends Comparable<? super T>> void sort(List<T> list) // 根据元素的自然排序对指定list集合元素按升序排序
 public static <T> void sort(List<T> list, Comparator<? super T> c) // 根据指定的Comparator产生的顺序对list集合进行定制排序
 public static void swap(List<?> list, int i, int j)  // 将指定list集合中的i处元素和j处元素进行交换
 public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) // 根据元素的自然顺序,返回给定集合中的最大元素

 public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp) // 根据Comparator指定的顺序,返回给定集合中的最大值。

 public static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> coll) // 根据元素的自然顺序,返回给定集合中的最小元素
 public static <T> T min(Collection<? extends T> coll, Comparator<? super T> comp) // 根据Comparator指定的顺序,返回给定集合中的最小值
public static int frequency(Collection<?> c, Object o)  // 返回指定集合中指定元素的出现次数
public static <T> void copy(List<? super T> dest, List<? extends T> src) // 将src的内容复制到dest中
public static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)

    package com.pyw;

import org.junit.Test;

import java.lang.reflect.Array;
import java.util.*;

public class CollectionsTest {

    @Test
    public void test(){
        List<Integer> list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add(789);
        list.add(753);
        list.add(498);
        list.add(785);
        list.add(642);
        Collections.reverse(list);
      //  System.out.println(list); // [642, 785, 498, 753, 789, 456, 123]

       // Collections.shuffle(list);
       // System.out.println(list); // [456, 498, 642, 785, 123, 789, 753] 由于随机所以每次都会不一样

       // Collections.sort(list);
       // System.out.println(list); // [123, 456, 498, 642, 753, 785, 789] 由于类当中实现了Comparable接口默认从小到大

        Collections.sort(list, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return -Integer.compare(o1,o2);
            }
        });
      //  System.out.println(list); // [789, 785, 753, 642, 498, 456, 123] 取反按照从大到小

        List list2= new ArrayList<>();
        list2 = Arrays.asList(new Object[list.size()]);
        Collections.copy(list2,list);
        System.out.println(list2); // [789, 785, 753, 642, 498, 456, 123]
    }
}

Collections常用方法:同步控制
Collections.SynchronizedXXX(XX); // 返回一个线程安全的list或Map集合

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值