Java集合之HashMap

  HashMap基于哈希表的Map接口的实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。并且该类是非线程安全的。

 HashMap的底层是一个数组加链表的集合。它的数据结构如下:

         

  HashMap底层是一个table数组,数组的长度(table.length)始终要求是2的N次幂。每次在该HashMap中在新增一个元素时(内部会将该元素的信息封装成Entry<K,V>),会先对该元素的键通过内部的一个函数(散列函数)求一个散列码(int型),然后将该散列码与(数组的长度-1)进行与运算,得出table数组的下标,存在该处。如果该处已经有元素(不同的键的散列码可能相等),就在这个新Entry<K,V>中的next属性保存上一次的Entry<K,V>的引用,依次,就在该数组的某项组成了链表。

  其中table中的每一项都称为一个桶位。数组的长度要求是2的N次幂,可以保证在减1后所对应的二进制位均为1,并且(length-1)&(散列码)之后,会落在0-length之间,这样就能保证生成的数组下标不越界。

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{//JDK1.7    java.util
	 //默认初始容量,必须为2的指数倍,因为可能使用该值初始化数组长度时。
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    //最大容量,必须为2的指数倍,因为可能使用该值初始化数组长度时。
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //加载因子默认值。0.75是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize 后的 HashMap 容量是容量的两倍
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    static final Entry<?,?>[] EMPTY_TABLE = {};//无数据时的空数组
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;//存储数据的Entry数组
    transient int size;//HashMap中实际键值对的数量
    
    //此 loadFactor和 capacity下允许的最大元素数目,超过这个数目就重新 resize,以降低实际的负载因子。threshold = capacity*loadFactor
    int threshold;
    final float loadFactor;//加载因子。loadFactor = size/capacity
    transient int modCount;//修改次数
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;//0x7fffffff
    
    public HashMap(int initialCapacity, float loadFactor) {//构造一个带指定初始容量和加载因子的空 HashMap
        if (initialCapacity < 0)
        	//初始容量校验
            throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;//指定容量不能超过MAXIMUM_CAPACITY:1 << 30
         //加载因子校验
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        //赋值
        this.loadFactor = loadFactor;
        threshold = initialCapacity;//初始时候直接将容量赋值给threshold。
        init();
    }
    void init() {//空方法
    }
    //构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    //构造一个具有默认初始容量 (16)和默认加载因子 (0.75)的空 HashMap
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
    //构造一个映射关系与指定 Map相同的新 HashMap
    public HashMap(Map<? extends K, ? extends V> m) { 
    	//容量为m的元素个数/0.75 和 默认初始容量中的较大值
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, 
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        inflateTable(threshold);

        putAllForCreate(m);
    }
    private void putAllForCreate(Map<? extends K, ? extends V> m) {
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            putForCreate(e.getKey(), e.getValue());//遍历存放键值对
    }
    private void putForCreate(K key, V value) {
        int hash = null == key ? 0 : hash(key);//散列key
        int i = indexFor(hash, table.length);//获取底层数组的索引
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //判断键相等的两个条件。散列码和key
            if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                e.value = value;
                return;
            }
        }
        createEntry(hash, key, value, i);
    }
    void createEntry(int hash, K key, V value, int bucketIndex) {
    	
    	//获取原来此处已经存放的所有Entry<K,V>
        Entry<K,V> e = table[bucketIndex];
        
        //添加新的Entry<>,并且将新的Entry<>里的next属性指向上一个Entry<K,V>。依次递推,这样每次添加新的,都与原来的形成了一个链表。
        table[bucketIndex] = new Entry<>(hash, key, value, e);//table[i]数组里的元素永远是最后添加进去的Entry<>
        size++;//数量加1
    }
    private void inflateTable(int toSize) {
    	//2的N次方。见下面函数
        int capacity = roundUpToPowerOf2(toSize);
        //初始化threshold的值
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];//初始化数组
        initHashSeedAsNeeded(capacity);//初始化HashSeed值
    }
    private static int roundUpToPowerOf2(int number) {
        //Integer.highestOneBit(int i)返回的则是跟它最靠近的比它小的2的N次方
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }
    
    
    transient int hashSeed = 0;//哈希因子。用于计算key的hash值
    final boolean initHashSeedAsNeeded(int capacity) {//初始化哈希因子hashSeed,32位,高16位始终为0
        boolean currentAltHashing = hashSeed != 0;
        boolean useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean switching = currentAltHashing ^ useAltHashing;
        if (switching) {
        	//对hashSeed赋值
            hashSeed = useAltHashing
                ? sun.misc.Hashing.randomHashSeed(this)
                : 0;
        }
        return switching;
    }
    private static class Holder {
        static final int ALTERNATIVE_HASHING_THRESHOLD;
        static {
            String altThreshold = java.security.AccessController.doPrivileged(
                new sun.security.action.GetPropertyAction(
                    "jdk.map.althashing.threshold"));

            int threshold;
            try {
                threshold = (null != altThreshold)
                        ? Integer.parseInt(altThreshold)
                        : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;

                // disable alternative hashing if -1
                if (threshold == -1) {
                    threshold = Integer.MAX_VALUE;
                }

                if (threshold < 0) {
                    throw new IllegalArgumentException("value must be positive integer.");
                }
            } catch(IllegalArgumentException failed) {
                throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
            }

            ALTERNATIVE_HASHING_THRESHOLD = threshold;
        }
    }
    
    
    final int hash(Object k) {//散列函数。根据 key求散列码
        int h = hashSeed;
        if (0 != h && k instanceof String) {//单独计算String的hash值
            return sun.misc.Hashing.stringHash32((String) k);
        }
        //计算算法
        h ^= k.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);//异或
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    
    public int size() {//返回键值对的数量
        return size;
    }

    public boolean isEmpty() {//是否为空
        return size == 0;
    }

    public V get(Object key) {//返回指定键所映射的值
        if (key == null)
            return getForNullKey();//允许键为null
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }

    private V getForNullKey() {//当键为null时
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;//返回键为null时的value
        }
        return null;//没有则返回null
    }

    public boolean containsKey(Object key) {//是否包含对于指定键的映射关系
        return getEntry(key) != null;
    }

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;//map为空则返回null
        }
        int hash = (key == null) ? 0 : hash(key);//根据key求散列码
        for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
            Object k;
            //判断键相等的两个条件。散列码和key
            if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

    static int indexFor(int h, int length) {
        return h & (length-1);//初始化时始终保证length为2的N次幂,减1后,与h求与运算始终在区间[0,length)中,不会越界
    }
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);//此时会初始化底层数组,数组长度将是2的N次幂
        }
        if (key == null)
            return putForNullKey(value);//允许键值为null
        int hash = hash(key);//对key求散列码
        int i = indexFor(hash, table.length);//找到table的索引
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //hash值判断和键需要同时判断
            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;
    }
    private V putForNullKey(V value) {//允许键值为null
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;//若存在该键,则更新值
                e.value = value;
                e.recordAccess(this);//空方法
                return oldValue;//返回旧值
            }
        }
        //若不存在该键
        modCount++;//修改次数+1
        addEntry(0, null, value, 0);
        return null;
    }
    void addEntry(int hash, K key, V value, int bucketIndex) {
    	//元素是否超过阈值,该桶位是否存放了元素
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);//扩容,重新散列,即重新分布数组
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);//重新计算散列码(桶位的索引)
        }
        //元素太多超过阈值并且该桶位还没有存放元素
        createEntry(hash, key, value, bucketIndex);//增加键值对
    }
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;//原来的长度为默认最大值时
            return;
        }

        Entry[] newTable = new Entry[newCapacity];//新建数组
        transfer(newTable, initHashSeedAsNeeded(newCapacity));//重新初始化哈希因子hashSeed
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {//遍历table
            while(null != e) {
            	//遍历数组里的Entry
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);//计算散列码
                }
                int i = indexFor(e.hash, newCapacity);
                //将原来数据存放在心的数组中
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
    //将指定映射的所有映射关系复制到此映射中
    public void putAll(Map<? extends K, ? extends V> m) {
        int numKeysToBeAdded = m.size();
        if (numKeysToBeAdded == 0)
            return;
        if (table == EMPTY_TABLE) {//是否为空,为空需要进行初始化
            inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));
        }
        if (numKeysToBeAdded > threshold) {//判断是否超过了阈值
            int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
            if (targetCapacity > MAXIMUM_CAPACITY)
                targetCapacity = MAXIMUM_CAPACITY;
            int newCapacity = table.length;
            while (newCapacity < targetCapacity)
                newCapacity <<= 1;//一直乘以2,保证是2的N次幂
            if (newCapacity > table.length)
                resize(newCapacity);//重新散列
        }

        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            put(e.getKey(), e.getValue());//存放元素
    }
    
    public V remove(Object key) {//从此映射中移除指定键的映射关系
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }
    final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;//当map为空时,直接null
        }
        int hash = (key == null) ? 0 : hash(key);//获取散列码
        int i = indexFor(hash, table.length);//获取索引
        Entry<K,V> prev = table[i];//拿到桶位中的Entry<K,V>
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;//获取上一个Entry<K,V>
            Object k;
            //hash和key必须都相等
            if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;//修改次数+1
                size--;//元素个数减1
                if (prev == e)
                	//如果prev == e,即要删除的键值对是最晚添加进去的,此时需要把上一个Entry复制给table[i]数组里的值
                    table[i] = next;
                else
                	//如果要删除的键值对不是最晚添加进去的,此时需要把上一个Entry复制给下一个Entry里的next
                    prev.next = next;
                e.recordRemoval(this);//空方法
                return e;
            }
            prev = e;
            e = next;//继续遍历
        }

        return e;
    }

    final Entry<K,V> removeMapping(Object o) {
        if (size == 0 || !(o instanceof Map.Entry))
            return null;
        Map.Entry<K,V> entry = (Map.Entry<K,V>) o;
        Object key = entry.getKey();
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            if (e.hash == hash && e.equals(entry)) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }
    
    public void clear() {
        modCount++;//修改次数+1
        Arrays.fill(table, null);//全部赋值为null,方便回收
        size = 0;//size为0
    }

    public boolean containsValue(Object value) {//如果此映射包含指定值,则返回 true
        if (value == null)
            return containsNullValue();//允许值为null

        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)//遍历数组
            for (Entry e = tab[i] ; e != null ; e = e.next)//遍历数组里的Entry
                if (value.equals(e.value))
                    return true;
        return false;
    }
    private boolean containsNullValue() {//值为null时
        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)//遍历数组
            for (Entry e = tab[i] ; e != null ; e = e.next)//遍历数组里的Entry
                if (e.value == null)
                    return true;
        return false;
    }

    public Object clone() {//浅复制
        HashMap<K,V> result = null;
        try {
            result = (HashMap<K,V>)super.clone();
        } catch (CloneNotSupportedException e) {
            // assert false;
        }
        if (result.table != EMPTY_TABLE) {
            result.inflateTable(Math.min(
                (int) Math.min(
                    size * Math.min(1 / loadFactor, 4.0f),
                    // we have limits...
                    HashMap.MAXIMUM_CAPACITY),
               table.length));
        }
        result.entrySet = null;
        result.modCount = 0;
        result.size = 0;
        result.init();
        result.putAllForCreate(this);//存放所有键值对

        return result;
    }

    //静态类。实现了Map.Entry<K,V>接口
    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;//存放当前键值对的key
        V value;//存放当前键值对的value
        Entry<K,V> next;//指向上一个Entry<K,V>
        int hash;//存放当前所处桶位的散列码
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {//获取key
            return key;
        }

        public final V getValue() {//获取value
            return value;
        }

        public final V setValue(V newValue) {//修改value
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {//equals
            if (!(o instanceof Map.Entry))
                return false;//必须是Map.Entry或其子类
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        public final int hashCode() {//哈希值,键与值异或
            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
        }

        public final String toString() {//toString
            return getKey() + "=" + getValue();
        }

        void recordAccess(HashMap<K,V> m) {//空方法
        }

        void recordRemoval(HashMap<K,V> m) {//空方法
        }
    }

    public Set<K> keySet() {//返回key的set集合
        Set<K> ks = keySet;//继承自父类的keySet,键集合
        return (ks != null ? ks : (keySet = new KeySet()));
    }

    private final class KeySet extends AbstractSet<K> {
        public Iterator<K> iterator() {//迭代器
            return newKeyIterator();
        }
        public int size() {//元素个数
            return size;//父类的size
        }
        public boolean contains(Object o) {
            return containsKey(o);//是否包含某个key
        }
        public boolean remove(Object o) {//移除
            return HashMap.this.removeEntryForKey(o) != null;//直接调用外部类的移除
        }
        public void clear() {//清空
            HashMap.this.clear();//直接调用外部类的清空
        }
    }
    Iterator<K> newKeyIterator() {
        return new KeyIterator();
    }
    private final class KeyIterator extends HashIterator<K> {
        public K next() {
            return nextEntry().getKey();
        }
    }
    private abstract class HashIterator<E> implements Iterator<E> {
        Entry<K,V> next;        //下一次迭代时返回的Entry
        int expectedModCount;   // 期望修改次数  fast-fail机制
        int index;              // 当前桶位索引
        Entry<K,V> current;     // 当前的Entry

        HashIterator() {
            expectedModCount = modCount;//初始化期望修改次数,此时等于实际修改次数
            if (size > 0) { 
                Entry[] t = table;
                //获取第一个不为null的桶并赋值给next。此为迭代的起点。没有元素时,next为null
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        }
        //是否可以继续迭代
        public final boolean hasNext() {
            return next != null;
        }

        final Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)//实际修改次数与期望修改次数比较
                throw new ConcurrentModificationException();
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();//此时next即为null,没有可迭代的元素

            if ((next = e.next) == null) {
                Entry[] t = table;
                //为下一次迭代做准备,越过数组中的null项
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
            current = e;
            return e;
        }

        public void remove() {//移除
            if (current == null)
                throw new IllegalStateException();//此时没有元素移除
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();//实际修改次数与期望修改次数比较
            Object k = current.key;//拿到当前键值对的key
            current = null;//清空
            HashMap.this.removeEntryForKey(k);//调用外部类的删除
            expectedModCount = modCount;//删除时,修改次数会+1,重新赋值
        }
    }
    
    public Collection<V> values() {//值的集合
        Collection<V> vs = values;//继承自父类
        return (vs != null ? vs : (values = new Values()));
    }
    private final class Values extends AbstractCollection<V> {
        public Iterator<V> iterator() {
            return newValueIterator();//返回迭代器
        }
        public int size() {//元素个数
            return size;//重复值算不同的值
        }
        public boolean contains(Object o) {//是否包含某个值
            return containsValue(o);
        }
        public void clear() {//清空
            HashMap.this.clear();
        }
    }
    Iterator<V> newValueIterator() {
        return new ValueIterator();//迭代器
    }
    private final class ValueIterator extends HashIterator<V> {//迭代器内部实现
        public V next() {
            return nextEntry().value;//迭代值
        }
    }
    

    private transient Set<Map.Entry<K,V>> entrySet = null;//存放键值对
    
    public Set<Map.Entry<K,V>> entrySet() {
        return entrySet0();
    }
    private Set<Map.Entry<K,V>> entrySet0() {//内部实现
        Set<Map.Entry<K,V>> es = entrySet;
        return es != null ? es : (entrySet = new EntrySet());
    }
    private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public Iterator<Map.Entry<K,V>> iterator() {//迭代器
            return newEntryIterator();
        }
        //
        public boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;//必须是Map.Entry或其子类
            Map.Entry<K,V> e = (Map.Entry<K,V>) o;
            Entry<K,V> candidate = getEntry(e.getKey());
            return candidate != null && candidate.equals(e);//Entry的比较
        }
        public boolean remove(Object o) {
            return removeMapping(o) != null;//移除
        }
        public int size() {
            return size;//大小
        }
        public void clear() {
            HashMap.this.clear();//清空
        }
    }
    Iterator<Map.Entry<K,V>> newEntryIterator()   {
        return new EntryIterator();
    }
    //EntryIterator内部实现
    private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
        public Map.Entry<K,V> next() {
            return nextEntry();
        }
    }

    private static final long serialVersionUID = 362498820763181265L;//版本号
    
    //序列化
    private void writeObject(java.io.ObjectOutputStream s) throws IOException{
        s.defaultWriteObject();
        if (table==EMPTY_TABLE) {
            s.writeInt(roundUpToPowerOf2(threshold));//写入threshold
        } else {
           s.writeInt(table.length);//写入length
        }
        s.writeInt(size);//写入size
        if (size > 0) {
            for(Map.Entry<K,V> e : entrySet0()) {
                s.writeObject(e.getKey());//写入键
                s.writeObject(e.getValue());//写入值
            }
        }
    }
    //反序列化
    private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException{
        s.defaultReadObject();
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            throw new InvalidObjectException("Illegal load factor: " +
                                               loadFactor);
        }
        table = (Entry<K,V>[]) EMPTY_TABLE;
        s.readInt(); // ignored.
        int mappings = s.readInt();
        if (mappings < 0)
            throw new InvalidObjectException("Illegal mappings count: " +
                                               mappings);

        // capacity chosen by number of mappings and desired load (if >= 0.25)
        int capacity = (int) Math.min(mappings * Math.min(1 / loadFactor, 4.0f), HashMap.MAXIMUM_CAPACITY);
        if (mappings > 0) {
            inflateTable(capacity);
        } else {
            threshold = capacity;
        }
        init(); 
        for (int i = 0; i < mappings; i++) {
            K key = (K) s.readObject();
            V value = (V) s.readObject();
            putForCreate(key, value);
        }
    }
    int   capacity()     { return table.length; }//获取长度
    float loadFactor()   { return loadFactor;   }//获取加载因子
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值