java基础学习 集合框架 Map

6 篇文章 0 订阅
本文介绍了Java Map接口及其主要实现类HashMap、LinkedHashMap和TreeMap的特性,包括它们的结构、添加机制、排序方式和应用场景。此外,还涉及了Properties的简单使用和Collections工具类的实用技巧。
摘要由CSDN通过智能技术生成

Map接口

HashMap 简要说明

package com.example.demo.day02;

import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.Properties;
import java.util.Map;


/**
 * 1、Map接口以及实现类
 * |-- Map:双列数据,存储 key-value 键值对的数据
 *     |--- HashMap:作为Map的主要实现类;线程不安全的,效率高;可以存储key或value为null的元素。
 *          |------ LinkedHashMap:HashMap的子类,能够保证在遍历元素时,可以按照添加的顺序实现遍历。
 *                                 原因:在原有HashMap的基础上,添加了一对"指针",指向前一个和后一个元素。
 *                                 对于频繁的遍历操作,LinkedHashMap执行效率高于HashMap。
 *     |--- TreeMap: 按照添加的 Key-Value 键值对进行排序,实现排序遍历。
 *                   此时考虑key的自然排序或定制排序。底层使用的是红黑树。
 *     |--- Hashtable: 作为Map的古老实现类;线程安全的,效率低;不能存储key或value为null
 *          |------ Properties: Hashtable的子类常用来处理配置文件,Key和Value都是String类型
 *
 *  2、key-value 键值对的理解 Map中的key是无序的不可重复的 value 是无序的可重复的。
 *      ---> 要求Key所在的类,必须重写equals() 和 hashcode() 方法 -- HashMap
 *        一个 key-value 键值对 构成了一个Entry对象,Entry也是无序的不可重复的。
 *
 *
 *      HashMap的底层:数组+链表 JDK7及以前
 *                    数组+链表+红黑树 JDK8
 *
 *
 */
public class MapTest {


    public void test(){
        // HashMap的底层实现原理简单说明 以JDK7 为例
        HashMap<String, String> map = new HashMap<>();
        // 在实例化以后,底层创建了长度为16的一维Entry[]数组
        // ... 可能已经执行过多次put...
        // 首先,调用key 所在类的 hashCode() 计算出 key的哈希值,此哈希值经过算法计算以后,得到在Entry数组中的存放位置。
        // 如果此位置没有数据 -- 此时 key-value 键值对添加成功。---- 情况1
        // 如果此位置已经存在数据(意味着此位置上存在一个或者多个数据,以链表的形式存在),比较key和已经存在的一个或者多个数据的哈希值
        //     如果key的哈希值与已经存在的数据的哈希值都不相同,-- 此时 key-value 键值对添加成功。---- 情况2
        //     如果key的哈希值与已经存在的数据中的某一个哈希值相同,继续比较,调用key所在类的 equals()方法,
        //            equals()方法返回false key-value 键值对添加成功 -------情况3
        //            equals()方法返回true,使用value 替换原有key对应的值
        // 补充:关于情况2和情况3:
        //         此时key-value和原来的数据以链表的方式存储。
        //         在不断添加的过程中,会涉及到扩容问题,当超出临界值时,要进行扩容。
        //         默认的扩容方式中,扩容为原来的2倍,并将原有的数据复制过来(重新计算元素在数组中的位置)
        // JDK8 相较于 JDK7  底层实现方面略有却别
        // 1、new HashMap<>(),底层没有直接创建长度为16的数组,并且数组名称变为 Node[] 而不是Entry[]
        // 2、首次调用put方法时,才创建长度为16的 Node[] 数组
        // 3、JDK7底层结构只有:数组+链表,JDK8底层结构:数组+链表+红黑树,当数组的某一个索引位置上的元素,
        //          以链表形式存在的个数大于8 且当前底层数组的长度还大于64,此时索引位置上的所有数据改为使用红黑树存储。
        //
        // HashMap源码中的重要常量
        // DEFAULT_INITIAL_CAPACITY = 1 << 4  :HashMap的默认容量 16
        // MAXIMUM_CAPACITY = 1 << 30 : HashMap中最大支持的容量 2^30
        // DEFAULT_LOAD_FACTOR = 0.75f :HashMap的默认加载因子是0.75,HashMap 底层数组的扩容时,
        //      并不像ArrayList一样,等到满了才扩容,默认情况下而是到 16 * 0.75 存储键值对的数量大于12进行扩容
        //      这样做的好处是:key的经过算法计算出位置正好填满数组的概率低,会造成链表长度过长,导致性能下降,提前扩容
        //      可以一定程度上减少链表过长的问题
        // TREEIFY_THRESHOLD = 8 :Bucket(桶)中的链表长度大于该默认值,转化为红黑树
        // UNTREEIFY_THRESHOLD = 6 :Bucket(桶)中红黑树存储的Node小于该默认值,转化为链表
        // MIN_TREEIFY_CAPACITY = 64 Bucket(桶)中的Node被树化时最小的hash表容量(当Bucket(桶)中Node的数量大到需要转变为红黑树时
        //      ,若hash表容量小于MIN_TREEIFY_CAPACITY,此时执行resize()扩容操作,这个MIN_TREEIFY_CAPACITY的值至少是
        //      TREEIFY_THRESHOLD的4倍。)
        // HashMap源码中的 属性
        // Node<K,V>[] table : 存储元素的数组,总是 2的n次幂
        // size : HashMap中存储的键值对的数量
        // modCount : HashMap扩容和结构改变的次数
        // entrySet: 存储具体元素的集合
        // threshold : 扩充的临界值 = 容量 * 加载因子
        // loadFactor : 加载因子
        map.put("key","value");
        // 底层put过程
        // 无参数初始化
        // public HashMap() {
        //    // 仅将默认加载因子赋值给 loadFactor
        //    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
        // }
        //
        // public V put(K key, V value) {
        //          hash(key) 拿到一个Hash值
        //        return putVal(hash(key), key, value, false, true);
        //    }

        // 获取hash值的方法
        //static final int hash(Object key) {
        //        int h;
        //        key的hashCode 异或 key.hashCode() 右移16位 直接说使用hashCode是不准确的
        //        >>表示右移,如果该数为正,则高位补0,若为负数,则高位补1;
        //        >>>表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0。
        //        参考 https://blog.csdn.net/cobbwho/article/details/54907203
        //        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        //    }


        // map 的 put方法 针对首次调用 put 方法
//        final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
//        boolean evict) {
            //
//            HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
                // 首次调用时 table未初始化 当时是null 直接进入resize()方法进行初始化 -> 进入resize() 方法查看
//            if ((tab = table) == null || (n = tab.length) == 0)
//                n = (tab = resize()).length;
//            if ((p = tab[i = (n - 1) & hash]) == null) // 通过 & 运算 的方式确定在tab 当前数组中存在的位置
//                tab[i] = newNode(hash, key, value, null); // 如果当前位置是null 添加成功
//            else { // 当前位置已经存在数据
//                HashMap.Node<K,V> e; K k;
//                if (p.hash == hash &&
//                        ((k = p.key) == key || (key != null && key.equals(k)))) // 比较 hash值,调用equals() 方法
//                    e = p; // key相同 当前数组p 存进 e中
//                else if (p instanceof HashMap.TreeNode) // 首次调用先不关心这个 因为还不是红黑树节点
//                    e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//                else {
//                    for (int binCount = 0; ; ++binCount) { // 直接进入for循环
//                        if ((e = p.next) == null) { // 当前数组节点上存在链表,直接比较下一个 因为已经与数组上的元素比较过了
//                            p.next = newNode(hash, key, value, null); // 链表中不存在下一个元素,新造Node元素准备新增
                                                                        // 并且作为 p的下一个元素
//                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 当超过8 准备转换成红黑树结构存储
//                                treeifyBin(tab, hash);
//                            break;
//                        }
//                        if (e.hash == hash &&
//                                ((k = e.key) == key || (key != null && key.equals(k)))) 存在相同的Key 终止循环
//                            break;
//                        p = e; // 将e赋值给p 返回for循环继续比较
//                    }
//                }
//                if (e != null) { // existing mapping for key 存在 key 替换 value
//                    V oldValue = e.value;
//                    if (!onlyIfAbsent || oldValue == null)
//                        e.value = value;
//                    afterNodeAccess(e);
//                    return oldValue;
//                }
//            }
//            ++modCount;
//            if (++size > threshold) // size是map存放键值对的数量 默认大于12 进行扩容
//                resize();
//            afterNodeInsertion(evict);
//            return null;
//        }

          // 扩容方法
//        final HashMap.Node<K,V>[] resize() {
//            HashMap.Node<K,V>[] oldTab = table;  //  首次调用 table 是null oldCap
//            int oldCap = (oldTab == null) ? 0 : oldTab.length; //首次调用oldCap =0
//            int oldThr = threshold; // 首次调用threshold 临界值还未赋值 初始化时仅赋值了加载因子
//            int newCap, newThr = 0;
//            if (oldCap > 0) {
//                if (oldCap >= MAXIMUM_CAPACITY) {
//                    threshold = Integer.MAX_VALUE;
//                    return oldTab;
//                }
//                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
//                        oldCap >= DEFAULT_INITIAL_CAPACITY) // 不是首次调用时进入这个判读
//                    newThr = oldThr << 1; // double threshold //临界值将会变成原来的2倍
//            }
//            else if (oldThr > 0) // initial capacity was placed in threshold
//                newCap = oldThr;
//            else {               // zero initial threshold signifies using defaults
                                   // 首次调用走到这里
//                newCap = DEFAULT_INITIAL_CAPACITY; // 首次调用 默认值时 16
//                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //首次调用 临界值12 出现
//            }
//            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"})
//            HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap]; // 首次调用数组出现,
//                                                                                        // 并且初始化一个长度16 HashMap.Node[]数组
//            table = newTab; // HashMap.Node[]类型的数组被初始化
//            if (oldTab != null) { // 首次调用不用关心这个 扩容时会重新计算hash位置会改变
//                for (int j = 0; j < oldCap; ++j) {
//                    HashMap.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 HashMap.TreeNode)
//                            ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//                        else { // preserve order
//                            HashMap.Node<K,V> loHead = null, loTail = null;
//                            HashMap.Node<K,V> hiHead = null, hiTail = null;
//                            HashMap.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; HashMap.Node<K,V> e;
//            if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // 当前数组长度小于64不转成红黑树而是数组扩容
//                resize();
//            else if ((e = tab[index = (n - 1) & hash]) != null) {
//                HashMap.TreeNode<K,V> hd = null, tl = null;
//                do {
//                    HashMap.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);
//            }
//        }
    }
}

HashMap 总结

    HashMap的存储结构简要说明 JDK1.8之前
    HashMap的内部存储结构其实是数组和链表的组合,当实例化一个HashMap时,默认情况下JDK会自动创建一个长度为capacity 16的Entry数组,这个长度在哈希表中被称为容量(capacity),在这个数组中可以存放元素的位置称为“桶”(backet),每个桶都有自己的索引,JDK可以根据索引快速查找到桶中的元素,
    每一个backet中存储一个元素,即一个Entry对象,但是每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此在一个backet中,是可以生成一个entry链。而且新添加的元素作为链表的head。
    添加元素的过程简要说明
    向HashMap中添加一个entry(key,value),首先需要计算entry中的key的哈希值(根据key所在类的hashcode()方法计算得到),此哈希值经过处理之后得到底层Entry[]数组中需要存储的位置i,如果位置i上没有元素,entry直接添加,如果位置i上已经存在元素(可能是以链表形式存在多个),需要通过循环的方法,依次比较,如果entry与其他元素的哈希值均不相同,则添加成功(以链表形式存储,且作为链表的头部)。如果哈希值相同,会调用key所在类的equals()方法,返回值为true,使用entry中的value替换原有元素的value,遍历完成后equals()方法返回false,entry依旧添加成功,以链表形式存储,且作为链表的头部。
    HashMap的扩容
    当HashMap中的元素逐渐增加,哈希冲突的机率将会增大,因为数组的长度是固定的,为了提高查询的效率,HashMap将会对数组进行扩容,而在HashMap的数组扩容之后,原有数组中的数据必须重新计算其在新数组中的位置,并且添加进去,这是比较消耗性能的。HashMap的扩容时机,当HashMap中的元素个数超过临界值(数组中的长度length * 设定的加载因子 loadFactor),就会对数组进行扩容,默认的加载因子是 0.75、数组长度是16,也就是当HashMap的元素个数超过12(临界值),HashMap就会触发扩容方法,将数组的大小扩容为原来的2倍,也就是32,然后重新计算每个元素在数组的位置,所以当我们大致确定HashMap中元素的个数,那么预设元素的个数能够有效提高HashMap的性能(尽量减少扩容次数)。
    JDK1.8
    HashMap的存储结构修改为数组+链表+红黑树的结合,默认情况下,当实例化一个HashMap时,JDK仅初始化了加载因子,当在第一次调用put方法时,JDK调用扩容方法,初始化一个长度为16的Node数组,这个长度在哈希表中被称为容量(capacity),在这个数组中可以存放元素的位置称为“桶”(backet),每个桶都有自己的索引,JDK可以根据索引快速查找到桶中的元素,
    每一个backet中存储一个元素,即一个Node对象,但是每一个Node对象可以带一个引用变量(next),用于指向下一个元素,因此在一个backet中,是可以生成一个Node链,也可能是一个个的TreeNode对象,每一个TreeNode可以有两个叶子节点,left和right,所以在一个桶中,可以生成一个TreeNode树,而新添加的元素作为链表的last,或者树的叶子节点。
    HashMap的扩容时机,当HashMap中的元素个数超过临界值(数组中的长度length * 设定的加载因子 loadFactor),就会对数组进行扩容,默认的加载因子是 0.75、数组长度是16,也就是当HashMap的元素个数超过12(临界值),HashMap就会触发扩容方法,将数组的大小扩容为原来的2倍,也就是32,然后重新计算每个元素在数组的位置,所以当我们大致确定HashMap中元素的个数,那么预设元素的个数能够有效提高HashMap的性能(尽量减少扩容次数)。到这里基本与JDK8之前一致,在JDK8中,当一个链表的元素的个数已经达到了8个,此时如果底层数组的长度没有超过64,HashMap会先扩容解决,如果已经达到了64,那么当前链表会转成红黑树存储,节点类型会由Node转为TreeNode类型。当然,如果当前映射关系被移除后,下次调用扩容方法,判断树的节点个数低于6个,也会把树转成链表存储。
    
    

LinkedHashMap 简要说明

@Test
    public void testLinkedHashMap(){
        // LinkedHashMap 是 HashMap的子类 没有重写hashMap的put方法
        // 但是声明了一个 HashMap.Node<K,V>的一个子类 Entry<K,V>
        // static class Entry<K,V> extends HashMap.Node<K,V> {
        //        // 除了 HashMap.Node之外还有两个属性 before 和 after 也就是
                  // 当前元素记录了上一个和下一个元素。
        //        Entry<K,V> before, after;
        //        Entry(int hash, K key, V value, Node<K,V> next) {
        //            super(hash, key, value, next);
        //        }
        //    }
        //
        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        // 在 newNode(hash, key, value, null); 时 调用了LinkedHashMap中的 Entry
        // 添加的同时记录了上一个元素和下一个元素
        map.put("key","value");
        System.out.println(map);
    }
@Test // 遍历map方式
    public void test4() {
        Map<String, String> map = new HashMap<String, String>() {{
            put("k1", "v1");
            put("k2", "v2");
            put("k3", "v3");
        }};
        map.forEach((k, v) -> System.out.println(k + "   " + v));
        System.out.println("---------------------");
        // 获取所有的key
        Set<String> keys = map.keySet();
        for (String k : keys) {
            System.out.println(k + "   " + map.get(k));
        }
        // 获取所有的value
        System.out.println("---------------------");
        Collection<String> values = map.values();
        System.out.println(values); 
        System.out.println("---------------------");
        // 获取所有的键值对
        Set<Map.Entry<String, String>> entries = map.entrySet();
        Iterator<Map.Entry<String, String>> iterator = entries.iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, String> entry = iterator.next();
            System.out.println(entry.getKey() + "    " + entry.getValue());
        }
    }

TreeMap的两种添加方式

@Test
    public void treeMapTest() {
        // TreeMap 向TreeMap中添加 key-value键值对,要求key必须是由一个类创建的对象 原有是要按照key进行排序
        // TreeMap 的两种排序方式:自然排序 自定义排序
        // 自然排序
        TreeMap<String, Integer> map = new TreeMap<String, Integer>() {{
            put("AA", 60);
            put("CC", 70);
            put("BB", 90);
            put("DD", 80);
        }};
        map.forEach((k, v) -> System.out.println(k + "   " + v));

        System.out.println("---------------------");
        // 定制排序
        TreeMap<String, Integer> map1 = new TreeMap<String, Integer>(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return -Integer.compare(o1.hashCode(), o2.hashCode());
            }
        }) {{
            put("AA", 60);
            put("CC", 70);
            put("BB", 90);
            put("DD", 80);
        }};
        map1.forEach((k, v) -> System.out.println(k + "   " + v));

        System.out.println("---------------------");

        TreeMap<String, Integer> map2 = new TreeMap<String, Integer>((o1, o2) -> -Integer.compare(o1.hashCode(), o2.hashCode())) {{
            put("AA", 60);
            put("CC", 70);
            put("BB", 90);
            put("DD", 80);
        }};
        map2.forEach((k, v) -> System.out.println(k + "   " + v));
    }

Properties 的简单使用

@Test
    public void testProperties()  {
        //Properties 常用来处理配置文件,Key和Value都是String类型 是 Hashtable的子类
        Properties properties = new Properties();
        FileInputStream inputStream = null;
        try {
            inputStream = new FileInputStream("/Users/zkt/project/demo/src/main/resources/jdbc.properties");
            properties.load(inputStream);// 加载流对应的文件
            System.out.println(properties.getProperty("name"));
            System.out.println(properties.getProperty("password"));

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (null != inputStream){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

Collections工具类的简单使用

     Collections 是一个操作 Set、List和Map等集合的工具类
     Collections 中提供了一系列的静态方法对集合元素进行排序,查询和修改等操作,
     还提供了对集合对象设置不可变、将集合对象实现同步控制等方法。

@Test
    public void testCollections() {
        // Collections 是一个操作 Set、List和Map等集合的工具类
        // Collections 中提供了一系列的静态方法对集合元素进行排序,查询和修改等操作,
        // 还提供了对集合对象设置不可变,对集合对象实现同步控制等方法。
        List<Integer> list = new ArrayList<Integer>() {{
            add(1);
            add(85);
            add(8);
            add(11);
            add(9);
            add(22);
            add(1);
        }};
        System.out.println(list);
        // 排序操作
        // Collections.reverse(List); 反转List中的元素
        Collections.reverse(list);
        System.out.println(list);
        // Collections.shuffle(List<?> list); 对List集合中的元素进行随机排序
        Collections.shuffle(list);
        System.out.println(list);
        // Collections.sort(List<T> list); 根据元素的自然顺序对指定List集合元素进行升序排序
        Collections.sort(list);
        System.out.println(list);
        // Collections.sort(List<T> list, Comparator<? super T> c); 根据指定的Comparator产生的顺序对List集合元素进行排序
        Collections.sort(list, (o1, o2) -> Integer.compare(o2, o1));
        System.out.println(list);
        // Collections.swap(List<?> list, int i, int j); 将指定的list集合中的i处元素与j处元素进行交换
        Collections.swap(list, 0, 4);
        System.out.println(list);
        // 查找替换
        // Collections.max(Collection<? extends T> coll) 根据元素的自然顺序 返回集合中的最大元素
        Integer max = Collections.max(list);
        System.out.println(max);
        // Collections.max(Collection<? extends T> coll, Comparator<? super T> comp) 根据指定的Comparator指定的顺序 返回集合中的最大元素
        Integer max1 = Collections.max(list, (Integer::compare));
        System.out.println(max1);
        // Collections.min(Collection<? extends T> coll) 根据元素的自然顺序 返回集合中的最小元素
        Integer min = Collections.min(list);
        System.out.println(min);
        // Collections.min(Collection<? extends T> coll, Comparator<? super T> comp) 根据指定的Comparator指定的顺序 返回集合中的最小元素
        Integer min1 = Collections.min(list, (Integer::compare));
        System.out.println(min1);
        // Collections.frequency(Collection<?> c, Object o) 返回集合中指定的元素出现的次数
        int frequency = Collections.frequency(list, 1);
        System.out.println(frequency);
        // Collections.copy(List<? super T> dest, List<? extends T> src);  将src中的内容复制到dest中
        // List<Integer> dest = new ArrayList<>();    错误写法     // java.lang.IndexOutOfBoundsException: Source does not fit in dest
        List<Integer> dest = Arrays.asList(new Integer[list.size()]);
        System.out.println(dest);
        Collections.copy(dest, list);
        System.out.println("------------");
        System.out.println(dest);
        // Collections.replaceAll(List<T> list, T oldVal, T newVal) 使用新的值替换List对象中所有旧的值
        boolean b = Collections.replaceAll(dest, 1, 100);
        System.out.println(b);
        System.out.println(dest);
        // Collections.synchronizedXXX 该方法将指定的集合包装成线程同步的集合,一定程度上解决多线程并发访问集合时的线程安全问题
        System.out.println("------------");
        List<Integer> syncList = Collections.synchronizedList(dest);
        System.out.println(syncList);

    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值