Java集合

目录

集合的继承结构图

Collection

集合遍历/迭代

list

set

Collections

泛型

Map


1、集合概述

  1.1、什么是集合?有什么用?

      数组其实就是一个集合。集合实际上就是一一个容器。可以来容纳其它类型的数据。

      集合为什么说在开发中使用较多?
          集合是一个容器,是一个载体,可以一次容纳多个对象。
          在实际开发中,假设连接数据库,数据库当中有10条记录,那么假设把这10条记录查询出来,在java程序中会将10条数据封装成10个java对象,然后将10个java对象放到某一个集合当中,将集合传到前端,然后遍历集合,将一个数据一个数据展现出来。
  1.2、集合不能直接存储基本数据类型,另外集合也不能直接存储java对象,集合当中存储的都是java对象的内存地址。( 或者说集合中存储的是引用。)

【注意】:
        集合在java中本身是一个容器,是一个对象。
        集合中任何时候存储的都是“引用"。
  1.3、在Java中每一一个不同的集合,底层会对应不同的数据结构。往不同的集合中存储元素,等于将数据放到了不同的数据结构当中。

    什么是数据结构?
        数据存储的结构就是数据结构。不同的数据结构,数据存储方式不同。

        例如:
          数组、二叉树、链表、哈希表..  (这些都是常见的数据结构。)
  1.4、集合在java JDK哪个包下?
        java.util.*;所有的集合类和集合接口都在java.util包下。

集合的继承结构图

   在java中集合分为两大类:

      一类是单个方式存储元素:
        单个方式存储元素,这一类集合中超级父接口: java.util.Collection;

      一类是以键值对的方式存储元素:
        以键值对的方式存储元素,这一类集合中超级父接口: java.util.Map;

Collection

1、Collection中能存放什么元素?
        Collection中什么都能存,只要是object的子类型就行。(集合中不能查接存储基本数据类型,也不能存java对象,只是存储java对象的内存地址。)

2、Collection中的常用方法:
        boolean add(Object e) 向集合中添加元素,默认都是向集合末尾添加元素

Collection c = new ArrayList();
c.add(1200); 
c.add(3.14); 
c.add(new Object());
c.add(true); 

解析:因为Collection接口是抽象的,无法实例化。所以使用多态创建对象。这里存在自动装箱机制。
        int size()  获取集合中的元素的个数

System.out.println("集合中的元素个数为:" + c.size()); // 4

        void clear()  清空集合

c.clear();
System.out.println("集合中的元素个数为:" + c.size()); // 0

        boolean contains(Object o) 判断当前集合中是否包含元素o,包含返回true,不包含返回false

                 底层调用了equals方法进行比对。

boolean flag = c.contains("1200");
System.out.println(flag); // true

        boolean remove(Object o)  删除集合中的某个元素。

                底层调用了equals方法进行比对。

c.remove(1200);
System.out.println("集合中的元素个数为:" + c.size()); // 3

【重点】:迭代集合元素的过程中,不能调用集合对象的remove方法,删除元素:c.remove(o); 迭代过程中不会这样。会出现异常:java.util.ConcurrentModificationException。一定要使用迭代器Iterator的remove方法,删除元素,不能使用集合自带的remove方法删除元素。

        boolean isEmpty()  判断该集合中的元素的个数是否为0

System.out.println(c.isEmpty());  // false

        Object[] toArray() 调用这个方法可以把集合转换为数组。【作为了解,使用不多。】

Object[] objects = c.toArray();
// 遍历数组
for (int i = 0; i < objects.length; i++) {
    Object o = objects[i];
    System.out.println(o);
}

集合遍历/迭代

【注意】:以下遍历/迭代方式,是所有Collection以及子类通用的一种方式。Map集合不能使用。

迭代的步骤:

        第一步:获取集合对象的迭代器对象Iterator   

Iterator it = c.iterator();

【重点】:集合的结构发生改变时,迭代器必须重新获取,如果还使用以前老的迭代器,会出现异常:java.util.ConcurrentModificationException

        第二步:通过迭代器对象方法来迭代/遍历集合。
                boolean hasNext() 如果有元素可以迭代,则返回true
                Object next() 返回迭代的下一个元素。

                       next()方法  存进去是什么类型,取出来还是什么类型。

Collection c = new ArrayList();
c.add("abc");
c.add("def");
c.add(100);
c.add(new Object());
Iterator it = c.iterator();
while (it.hasNext()) {
    Object obj = it.next();
    System.out.println(obj);
}

在遍历的时候,下面方式是错误的,会报异常java.util.NoSuchElementException

while (true) {
    Object obj = it.next();
    System.out.println(obj);
}

迭代原理图

 【注意】:集合迭代的时候删除元素

下面这种方式会报异常的java.util.ConcurrentModificationException

while (it.hasNext()) {
    Object obj = it.next();
    c.remove(100);
    System.out.println(obj);
}

解析:删除元素之后,集合的结构发生了变化,应该重新去获取迭代器,但是,循环下一次的时候并没有重新获取迭代器,所以会出现异常:java.util.ConcurrentModificationException,出异常根本原因是:集合中元素删除了,但是没有更新迭代器

应该使用迭代器对象删除元素

while (it.hasNext()) {
    Object obj = it.next();
    it.remove();
    System.out.println(obj);
}

解析:使用迭代器去删除时,会自动更新迭代器,并且更新集合(删除集合中的元素)。

【重点】:1、集合的结构发生改变时,迭代器必须重新获取,如果还使用以前老的迭代器,会出现异常:java.util.ConcurrentModificationException
        2、迭代集合元素的过程中,不能调用集合对象的remove方法,删除元素:c.remove(o); 迭代过程中不会这样。会出现异常:java.util.ConcurrentModificationException
        3、在迭代元素的过程中,一定要使用迭代器Iterator的remove方法,删除元素,不能使用集合自带的remove方法删除元素。

list

1、List集合存储元素特点:有序可重复
                有序:LIst集合中的元素有下标。从0开始,以1递增。存进去什么顺序,取出来就是什么顺序。
                可重复:存储元素可重复。

2、List是Collection接口的子接口,List接口有自己“特色”的方法:
        以下是List接口特有的常用的方法:
                void add(int index, E element) 在列表的指定位置插入指定元素(第一个参数是下标)

List myList = new ArrayList();
myList.add("A");  // 默认都是向集合末尾添加元素。
myList.add("B");
myList.add("C");
myList.add("C");
myList.add(1, "KING");

注意:这个方法使用不多,因为对于ArrayList集合来说效率比较低。

                Object set(int index, Object element) 修改指定位置的元素

myList.set(2, "Soft");

                Object get(int index) 根据下标获取元素

Object firstObj = myList.get(0);
System.out.println(firstObj);

                int indexOf(Object o) 获取指定对象第一次出现处的索引

System.out.println(myList.indexOf("C")); // 3

                int lastIndexOf(Object o) 获取指定对象最后一次出现处的索引

System.out.println(myList.lastIndexOf("C")); // 4

                Object remove(int index) 删除指定下标位置的元素

myList.remove(0);
System.out.println(myList.size()); // 4

遍历list集合,因为有下标,所以List集合有特殊的遍历方式,Set没有。

for (int i = 0; i < myList.size(); i++) {
    Object obj = myList.get(i);
    System.out.println(obj);
}

ArrayList

1、默认初始化容量10(底层先创建了一个长度为10的数组,当添加第一个元素的时候,初始化容量10.)
2、集合底层是一个Object[]数组。
3、构造方法:

new ArrayList();
new ArrayList(20);

List list1 = new ArrayList();
System.out.println(list1.size()); // 0

集合的size()方法是获取当前集合中元素的个数,不是获取集合的容量。

List list2 = new ArrayList(20);
System.out.println(list2.size()); // 0

通过这个构造方法就可以将HashSet集合转换成List集合。

Collection c = new HashSet();
List myList3 = new ArrayList(c);

4、ArrayList集合的扩容:增长到原容量的1.5倍。(根据源码看出)

int newCapacity = oldCapacity + (oldCapacity >> 1);

解析:新数 = 原数  + 位运算右移1位(原数除以2)
        原先是4,现在增长:2,增长后是6,增长之后的容量是之前容量的:1.5倍。

        ArrayList集合底层是数组,怎么优化?
                尽可能少的扩容。因为数组扩容效率比较低,建议在使用ArrayList集合的时候预估元素的个数,给定一个初始化容量。
5、数组的优点:
        检索效率比较高。(每个元素占用空间大小相同,内存地址是连续的,知道首元素内存地址,然后知道下标,通过数学表达式计算出元素的内存地址,所以检索效率最高。)
6、数组缺点:
        随机增删元素效率比较低。另外数组无法存储大数据量。(很难找到一块非常巨大的连续的内存空间。)
7、向数组末尾添加元素,效率很高,不受影响。
8、面试官经常问的一个问题?
        这么多的集合中,你用哪种集合最多?
    答:ArrayList集合。因为往数组末尾添加元素,效率不受影响。 另外,我们检索/查找某个元素的操作比较多。
9、ArrayList集合是非线程安全的。(不是线程安全的集合。)

LinkedList

1、链表的优点:
        由于链表上的元素在空间存储上内存地址不连续。所以随机增删元素的时候不会有大量元素位移,因此随机增删效事较高。在以后的开发中,如果遇到随机增删集合中元素的业务比较多时,建议使用LinkedList

2、链表的缺点:
        不能通过数学表达式计算被查找元素的内存地址,每一次查找都是从头节点开始遍历,直到找到为止。所以LinkedList集合检索/查找的效率较低

3、ArrayList和LinkedList对比
        ArrayList:把检索发挥到极致。(末尾添加元素效率还是很高的。)
        LinkedList:把随机增删发挥到极致。
        加元素都是往末尾添加,所以ArrayList用的比LinkedList多。

4、LinkedList集合没有初始化容量。

双向链表示意图

Vector

1、底层也是一个数组。
2、初始化容量:10

List vector = new Vector();

3、怎么扩容呢?
        扩容之后是原容量的2倍。
        10 --> 20 --> 40 --> 80

int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);

4、ArrayList集合扩容特点:
        ArrayList集合扩容是原容量1.5倍。
5、Vector中所有的方法都是线程同步的,都带有synchronized关键字,是线程安全的。效率比较低,使用较少了。
6、怎么将一个线程不安全的ArrayList集合转换成线程安全的呢?
        使用集合工具类:
                java,util,Collections;
                java,util,Collection 是集合接口。
                java,util,Collections 是集合工具类。

List myList = new ArrayList(); // 非线程安全的。
Collections.synchronizedList(myList);

解析:将ArrayList集合由非线程安全变成线程安全的。

set

List集合存储元素特点:无序不可重复

        无序:存进去的顺序,取出来不一定是这个顺序。并且没有下标。

        不可重复:元素不可重复。

HashSet

底层实际上是HashMap,放到HashSet集合中的元素实际上放到HashMap集合的key部分了。

Set strs = new HashSet();
strs.add("hello3");
strs.add("hello4");
strs.add("hello1");
strs.add("hello2");
strs.add("hello3");

遍历集合

hello1
hello4
hello2
hello3

TreeSet

底层实际上是TreeMap,TreeMap集合底层是一个二叉树。

1、无序不可重复的,但是存储的元素可以自动按照大小顺序排序!称为:可排序集合。
2、无序:这里的无序值得是存进去的顺序和取出来的顺序不同。并且没有下标。

Set strs = new TreeSet();
strs.add("A");
strs.add("Z");
strs.add("B");
strs.add("Z");
strs.add("Y");

遍历集合(可以使用foreach语句,不需要下标,遍历集合全部元素)

for (Object s : strs) {
    System.out.println(s);
}

A
B
Y
Z

3、对于自定义的类型来说,TreeSet不可以排序,因为没有指定Person对象之间的比较规则,会出现了这个异常:java.lang.ClassCastException:

方法一:自定义类实现Comparable,并实现compareTo方法,这个compareTo用来制定集合元素的比较规则的。

public class Customer implements Comparable<Customer>{
    int age;
    public Customer(int age) {
        this.age = age;
    }
    @Override
    public int compareTo(Customer o) {  // c1.compareTo(c2);
        return this.age - o.age;  // >0 <0 =0  从小到大排序
    }
}

解析:compareTo方法的返回值很重要:
        返回0表示相同,value会覆盖。
        返回>0,会继续在右子树上找。【10 - 9 = 1, 1 > 0的说明左边这个数字比较大,所以在右子树上找。】
        返回<0,会继续在左子树上找。

因为TreeMap底层是二叉树,所以结合二叉树图

方法二: 使用比较器的方式。

创建TreeSet集合的时候,给构造方法传递一个比较器。

可以单独写一个比较器类实现Comparator接口

public class WuGuiComparator implements Comparator<WuGui> {
    @Override
    public int compare(WuGui o1, WuGui o2) {
        // 指定比较规则,按照年龄排序
        return o1.age - o2.age;
    }
}

通过构造方法传入比较器

TreeSet<WuGui> wuGuis = new TreeSet<>(new WuGuiComparator());

也可以使用匿名内部类的方式,传入比较器

TreeSet<WuGui> wuGuis =new TreeSet<>(new Comparator<WuGui>() {
    @Override
    public int compare(WuGui o1, WuGui o2) {
        return o1.age - o2.age;
    }
});

【最终结论】:
        放到TreeSet或者TreeMap集合key部分的元素要想做到排序,包括两种方式:
                第一种:放在集合的元素实现java.lang.Comparable接口。
                第二种:在构造TreeSet或者TreeMap集合的时候给它传递一个比较器对象。
Comparable和Comparator怎么选择呢?
        当比较规则不会发生改变的时候,或者说当比较规则只有1个的时候,建议实现Comparable接口。
        如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用Comparator接口。
        Comparator接口的设计符合OTC原则。

Collections

java.util.Collection 集合接口
java.util.Collections 集合工具类,方便集合的操作。

1、Collections.synchronizedList(); 非线程安全变成线程安全的

List<String> list = new ArrayList<>();
Collections.synchronizedList(list);

2、Collections.sort();  排序

Collections.sort(list);

【注意】:对List集合中元素排序,需要保证List集合中的元素实现了:Comparable接口。

还有对Set集合排序,需要将Set集合转换成List集合

Set<String> sets = new HashSet<>();
List<String> myList = new ArrayList<>(sets);

泛型

1、JDK5.0之后推出的新特性:泛型
2、泛型这种语法机制,只在程序编译阶段起作用,只能给编译器参考的。(运行阶段泛型没用!!)
3、使用了泛型好处是什么?
        第一:集合中存储的元素类型统一了。
        第二:从集合中取出的元素类型是泛型指定的类型,不需要进行大量的“向下转型”!
4、泛型的缺点是什么?
        导致集合中存储的元素缺乏多样性!
大多数业务中,集合中元素的类型还是统一的,所以这种泛型特性被大家所认可。

创建一个Animal类为父类,Cat类为子类继承Animal,Bird继承Animal

List<Animal> myList = new ArrayList<Animal>();

解析:使用泛型List<Animal>之后,表示List集合中只能允许存储Animal类型的数据。

使用迭代器迭代的类型也是Animal类型

Iterator<Animal> it = myList.iterator();
while (it.hasNext()) {
    Animal a = it.next();
}

指定List集合中只能存储Animal,那么存储String就编译报错了。这样用了泛型之后,集合中元素的数据类型更加统一了。

myList.add("abc");

5、JDK8之后引入了:自动类型推断机制。(又被称为“钻石表达式”)

List<Animal> myList = new ArrayList<>();

6、自定义泛型
        自定义泛型的时候,<> 尖括号中的是一个标识符,随便写。
java源代码中经常出现的是:<E> 和 <T>
        E是Element单词首字母。T是Type单词首字母。

public class MyIterator<T> {
    public T get() {
        return null;
    }
}

测试

MyIterator<String> mi = new MyIterator<>();
String s1 = mi.get();
MyIterator<Animal> mi2 = new MyIterator<>();
Animal s2 = mi2.get();

Map

1、Map和Collection没有继承关系。
2、Map集合以Key和Value的方式存储数据:键值对
        Key和Value都是引用数据类型,存储的都是对象的内存地址。
3、Map接口中常用的方法:
        V put(K key, V value) 向Map集合中添加键值对

Map<Integer, String> map = new HashMap<>();
map.put(1, "zhangsan"); // 1 在这里进行了自动装箱。
map.put(2, "lisi");
map.put(3, "wangwu");

        V get(Object key) 通过key获取value

String value = map.get(1);
System.out.println(value); // zhangsan

        void clear() 清空Map集合

map.clear();
System.out.println("键值对的数量:" + map.size()); // 0

        boolean containsKey(Object key) 判断Map集合中是否包括某个key

System.out.println(map.containsKey(3)); // true

  底层调用的都是equals进行比对的,所以自定义的类型需要重写equals方法。
        boolean containsValue(Object Value) 判断Map集合中是否包括某个value

System.out.println(map.containsValue("wangwu")); // true

        boolean isEmpty() 判断Map集合中元素个数是否为0

System.out.println(map.isEmpty()); // false

        V remove(Object key) 通过key删除键值对

map.remove(2);
System.out.println("键值对的数量:" + map.size()); // 2

        int size() 获取Map集合中键值对的个数

int i = map.size();
System.out.println("键值对的数量:" + i); // 3

        Collection<V> values() 获取Map集合中所有的value,返回一个Collection

Collection<String> values = map.values();

        Set<K> keySet() 获取Map集合中所有的key(所有的键是一个set集合)

Set<Integer> keys = map.keySet();

        Set<Map.Entry<K, V>> entrySet()    将Map集合转换成Set集合。

Set<Map.Entry<Integer, String>> set = map.entrySet();

解析:set集合对象
                1=zhangsan 【注意:Map集合通过entrySet()方法转换成的这个Set集合,Set集合中元素的类型是Map.Entry<K, V>】
                2=lisi    【Map.Entry和String一样,都是一种类型的名字,只不过:Map.Entry是静态内部类,是Map中的静态内部类】
                3=wangwu

4、Map集合的遍历

Map<Integer, String> map = new HashMap<>();
map.put(1, "zhangsan");
map.put(2, "lisi");
map.put(3, "wangwu");

方法一:获取Map结合所有的key,通过key获取value

Set<Integer> keys = map.keySet();
Iterator<Integer> it = keys.iterator();
while (it.hasNext()) {
    Integer key = it.next();
    String value = map.get(key);
}

使用foreach也可以,都是获取key

for (Integer key : keys) {
    String value = map.get(key);
    System.out.println(key + "-->" + value);
}

方法二:使用Set<Map.Entry<K, V>> entrySet()方法,把Map集合直接全部转换成Set集合。

Set集合中元素的类型是:Map.Entry

Set<Map.Entry<Integer, String>> set = map.entrySet();
Iterator<Map.Entry<Integer, String>> it2 = set.iterator();
while (it2.hasNext()) {
    Map.Entry<Integer, String> node = it2.next();
    Integer key = node.getKey();
    String value = node.getValue();
    System.out.println(key + "=" + value);
}

也可以使用foreach

for (Map.Entry<Integer, String> node : set) {
    System.out.println(node.getKey() + "-->" + node.getValue());
}

HashMap

1、HashMap集合底层是哈希表/散列表的数据结构。
2、哈希表是一个怎样的数据结构呢?
        哈希表是一个数组和单向链表的结合体。
        数组:在查询方面效率比较高,随机增删方面效率很低。
        单向链表:在随机增删方面效率较高,在查询方面效率很低。
        哈希表将以上的两种数据结构融合一起,充分发挥它们各自的优点。
3、HashMap集合底层:

public class HashMap {
    // HashMap底层实际上就是一个数组。(一维数组)
    Node<K, v> table;
    // 静态的内部类HashMap.Node
    static class Node<K, v> {
        // 哈希值(哈希值是key的hashCode()方法的执行结果。hash值通过哈希函数/算法,可以转换存储成数组的下标。)
        final int hash;
        final K key;  // 存储到Map集合中的那个key
        V value;  // 存储daoMap集合中的那个value
        Node<K,V> next;  // 下一个节点的内存地址。
    }
}

哈希表/散列表:一维数组,这个数组中每一个元素是一个单向链表。(数组和链表的结合体。)

哈希表数据结构图

 4、HashMap集合的默认初始化容量是16,默认加载因子是0.75
        这个默认加载因子是当HashMap集合底层数组的容量达到75%的时候,数组开始扩容。
【重点】:HashMap集合初始化容量必须是2的倍数,这也是官方推荐的,这是因为达到散列均匀,为了提高HashMap集合的存取效率,所必须的。
5、HashMap集合的key部分特点:无序,不可重复。
        为什么无序? 因为不一定挂到那个单向链表上。
        不可重复是怎么保证的?

                equals方法保证HashMap集合的key不可重复。如果key重复了,value会覆盖。

Map<Integer, String> map = new HashMap<>();
map.put(1111, "zhangsan");
map.put(6666, "lisi");
map.put(7777, "wangwu");
map.put(2222, "zhaoliu"); // key重复的时候,value会自动覆盖。
map.put(2222, "king");
System.out.println(map.size()); // 4

放在HashMap集合key部分的元素其实就是放在HashMap集合中了。所以HashMap集合中的元素也需要同时重写equals()+hashCode()方法.
6、哈希表HashMap使用不当时无法发挥性能!
        假设将所有的hashCode()方法返回值固定为某个值,那么会导致底层哈希表变成了纯单向链表。这种情况我们成为:散列分布不均匀。
        什么是散列分布均匀?
                假设有100个元素,10个单向鲢丧,那么每个单向链表上有10个节点,这是最好的,是散列分布均匀的。
        假设将所有的hashCode()方法返回值都设定为不一样的值,可以吗,有什么问题?
                不行,因为这样的话导致底层哈希表就成为一维数组了,没有链表的概念了。也是散列分布不均匀。
        散列分布均匀需要重写hashCode()方法时有一定的技巧。

7、向Map集合中存,以及从Map集合中取,都是先调用key的hashCode方法,然后再调用equals方法!equals方法有可能调用,也有可能不调用。
        拿put(k,v)举例,什么时候equals不会调用?
                k.hashCode()方法返回哈希值,哈希表经过哈希算法转成数组下标。

                数组下标位置上如果是null,equals不需要执行。
        拿get(k)举例,什么时候equals不会调用?
                k.hashCode()方法返回哈希值,哈希表经过哈希算法转成数组下标。
                数组下标位置上如果是null,equals不需要执行。
【注意】:如果一个类的equals方法重写了,那么hashCode()方法必须重写。并且equals方法返回如果是true,hashCode()方法返回的值必须一样。equals方法返回true表示两个对象相同,在同一个单向链表上比较。那么对于同一个单向链表上的节点来说,他们的哈希值都是相同的。所以hashCode()方法的返回值也应该相同。
8、【重点】:放在HashMap集合key部分的元素,以及放在HashSet集合中的元素,需要同时重写hashCode和equals方法。

public int hashCode() {
    return Objects.hash(name);
}

解析:根据name属性获取哈希值

9、对于哈希表数据结构来说:
         如果o1和o2的hash值相同,一定是放在同一个单向链表上。
        当然如果o1和o2的hash值不同,但由于哈希算法执行结束之后转换的数组下标可能相同,此时会发生“哈希碰撞“。

10、主要掌握的方法:
        map.put(k, v)  添加key为k,value为v的元素
        v = map.get(k)  获取key为k的元素

11、HashMap集合key部分允许null,但是注意:HashMap集合的key,null值只有一个

Map map = new HashMap();
map.put(null, null);
System.out.println(map.size());  // 1
map.put(null, 100);
System.out.println(map.size());  // 1

Hashtable

1、Hashtable的key可以为null么?
        Hashtable的key和value都是不能为null的
        HashMap集合的key和value都是可以为null的。

Map map = new Hashtable();
map.put(null, "123");
map.put(100, null);

上面代码,会抛出异常java.lang.NullPointerException
2、Hashtable方法都是带有synchronize:线程安全的
        线程安全有其他的方案,这个Hashtable对线程的处理导致效率较低,使用较少了。
3、Hashtable和HashMap一样,底层都是哈希表数据结构。
4、Hashtable的初始化容量是11,默认加载因子是:0.75f
5、Hashtable的扩容是:原容量 * 2 + 1

Properties

1、Properties继承Hashtable,Properties的key和value都是String类型。
2、Properties被称为属性类对象。
3、Properties是线程安全的。

setProperty(k,v)   存入key为k,value为v的元素

getProperty(k)  通过key获取value

Properties pro = new Properties();
pro.setProperty("username", "root");
pro.setProperty("password", "123");
String username = pro.getProperty("username");
String password = pro.getProperty("password");
System.out.println(username); //root
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值