简单java-Map

吗 Map

   public interface Map<K,V>  两个泛型     ,一个键,一个值 
    将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射到一个值。 
     
     Collection<E>            Map<K,v>
      单列集合                  双列集合
      每次只能保存一个值         每次存储一对值 
       
                              键不能重复 ,值可以重复  

方法

           V put(K key,V value) 将指定的值与此映射中的指定键关联(可选操作)。 
             如果该键存在,则替换值,返回旧的value值,没有则返回null, 
          
           V get(Object key)返回指定键所映射的值;如果此映射不包含该键的映射关系,则返 
              回 null。 
               
           V remove(Object key)如果存在一个键的映射关系,则将其从此映射中移除并返回值 
                                                   不存在则返回null
           void clear()从此映射中移除所有映射关系  
            
           boolean containsKey(Object key) 如果此映射包含指定键的映射关系,则返回 true  
           boolean containsValue(Object value) 试是否存在于此映射中的值  
                  如果此映射将一个或多个键映射到指定值,则返回 true  
           
           boolean isEmpty()如果此映射未包含键-值映射关系,则返回 true。 
           int size() 返回键值对的数量 

遍历

           Set<K> keySet()返回此映射中包含的键的 Set 视图。  
              将所有的key取出,放到Set集合里,然后就可以通过Set来获取值了 
              
           Set<Map.Entry<K,V>> entrySet()  返回此映射中包含的映射关系的 Set 视图 
           
           内部接口,类似内部类
            public static interface Map.Entry<K,V>  Map.entrySet 方法返回映射 collection 视图 
            返回的Set集合里面有Map.Entry<K,V>集合
            Map.Entry<K,V>:在Map接口中有一个内部接口Entry 
            作用:当map集合一创建,那么就会在Map集合中创建一个Entry对象,用来记录 
            键与值(键值对对象,键与值的映射关系)
           
           而Map.Entry接口里面提供获取键值对的方法 
           
           K getKey()  返回与此项对应的键。
           V getValue()  返回与此项对应的值 

HashMap<K,V>

    子类  
       基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作 
       此类不保证映射的顺序 ,无序的
 
       (除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)
       注意,此实现不是同步的(多线称)
       默认的初始化大小为16,每次扩充,容量变为原来的2倍     
        
        为什么容量必须是2的幂 , 
        
        jdk  1.7
          首先我们要知道,当前我们创建对象时是不会去创建桶的,也就是数组声明了但没有初始化,
          当我们去put的时候,桶数组会去和一个共享的空初始数组比较,当为true,说明桶的空间还没有开辟,
          这时他就会调用一个方法,将传入的对象的大小进行计算,如果不是2的幂,就将容量向上提升为2的幂
          如,当为17的时候,就变为32,向上升一个幂,如果超过最大值们就用最大值
           
           将hashcode存放在桶中,如何存放 
           hashcode是int修饰的,int 4个字节, 40多亿个数
           如果初始桶为16,那么如何存放
               大家想到的是取模  hashcode%16 
               那么缺点是什么呢: 
                     负数%整数 时为负数   
                     较慢 
                     
                  hashmap里使用的是(jdk1.7)先算哈希值,然后通过一个方法计算这个对象的索引下标,
                  那么这个方法内部做了什么呢
                     他使用了数组的长度减1之后也就是(length-1)和哈希值进行了按位与的操作,
                     并且是分布均匀的,但是如果初始容lenth不是2的幂呢
                       首先我们要知道,2的幂的二进制是  10,100,1000,10000...等,对它进行减1的话,得到的是一个全是1111的数
                       但如果不是2的幂的话,二进制就会有0的存在,此时无论和谁做按位与运算,结果都是0,那么问题就来了
                       这样子有一些桶就永远都是空的,因为不会被获取到下标被存入值
                       然后获取到下标后,将对应下标的数组元素赋值给一个Entry<k,v> ,并且判断是否为null,不为null的话
                       就进循环找,然后判断,通过一些什么哈希值,值得比较等,来判断是否存在,存在则覆盖返回,
                       不存在,则Entry<k,v>进行下一次迭代, 因为entry是一个map,是链表,所以,就这个地址一直迭代去找,就是在链表中找
                       最后该桶的链表查找完了,依然不存在,则添加一个在桶里,插在链表的前面,再返回null 
                      
                        
                        那么扩容是怎么扩容的    ,
                          举例: 
                          根据容量,默认初始16  ,乘于负载因子默认0.75    
                           也就是说当存储的元素超过了12,那么就扩容,扩容为原来的2倍,扩容后需要重新计算哈希值,重新调整元素的位置
                                    因为桶的数量变了,变为原来的2倍,创建新了的entry哈希桶,长度为原来的2倍
                        
                        然后调用transfer方法,将原来的桶一个个的遍历出来赋值到新的桶里,注意,由于是往头插数据,所以现在就出现顺序反了的问题
                      问题的根源在于这个方法   
                           问题:  
                               容易遇到死锁      原因再多线称的情况下很有可能出现环形链表   , 
                               就是在做数据迁移的时候,没有考虑到迁移后的链表元素的顺序,前后元素发生改变,就是节点的next指向了原本的前一个,现在是后一个
                               因为顺序调换了,那么死锁问题就来了
                            可以通过精心构造的恶意请求引发dos
                        
              1 .8
                     改进  ,将数组加链表改为了    数组+链表(红黑树)      
                     改进了扩容时的插入顺序问题   。使用的是尾插法
                     链表变成红黑树的时候是,符合泊松分布,那么是什么时候变为红黑树的呢,当每个桶里的元素个数分别是,0,1,2,3,4,5,6,7,8的概率 
                     当为8的时候且桶总量超过64时,才会转红黑树,概率已经非常的小了, (桶的数量必须大于64,小于64的时候只会扩容)
                       
                       hash 的实现和1.7的不同
                        是通过 hashCode() 的高 16 位异或低 16 位实现的:
                        为什么要用异或运算符?
                             保证了对象的 hashCode 的 32 位值只要有一位发生改变,整个 hash() 返回值就会改变。尽可能的减少碰撞。
                     
                            扩容时,调用 resize() 方法,数组长度大于数组的阈值时,将 table 长度变为原来的两倍
                      
                      HashMap中put方法的过程?
                              。调用哈希函数获取Key对应的hash值,再计算其数组下标
                              。 如果没有出现哈希冲突,则直接放入数组;如果出现哈希冲突,则以链表的方式放在链表后面
                              。如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表;
                              。如果结点的key已经存在,则替换其value即可;
                              。如果集合中的键值对大于数组阀值,调用resize方法进行数组扩容
                 
                   数组扩容的过程 
                              创建一个新的数组,其容量为旧数组的两倍,并重新计算旧数组中结点的存储位置。结点在新数组中的位置只有两种,原下标位置或原下标+旧
                              数组的大小。因为数组的扩容是 *2
                               
                     使用场景:在 Map 中插入、删除和定位元素时
                      
                      你知道HashMap的哈希函数怎么设计的
                          hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashcode的高16位和低16位进行异或操作。
                          尽可能降低hash碰撞,越分散越好;
                      为什么采用hashcode的高16位和低16位异或能降低hash碰撞?hash函数能不能直接用key的hashcode?
                              因为key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。int值范围为**-2147483648~2147483647**,前后加起来大
                              概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。
                              源码中模运算就是把散列值和数组长度-1做一个"与"操作,位运算比%运算要快。
                              这也正好解释了为什么HashMap的数组长度要取2的整数幂。因为这样(数组长度-1)正好相当于一个“低位掩码”。“与”操作的结果就是散列值
                              的高位全部归零,只保留低位值,用来做数组下标访问。
                              
                              注意,在第一次put时也就是数组还为空时,数组的大小设置为了阈值的大小,阈值是在我们构造中传入的,不传默认16  
                              但这里相信会有一点迷糊,那么容量那么大的话,为什么getsize却不是那么多,主要是内部设置了getsize是返回键值对的数目,而不是数组容
                              量,所以我们这里有可能会有点迷糊, 、构造有参数,则使用构造的,无参则使用默认的,反正就是传了容量大小,则计算的向上2次幂做数
                              组大小,没有则使用默认的16,则默认阈值为12
                              
                              
                             
                              版本优化     
                                  扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
                                  也就是正好是扩容2倍,1.8通过移位的方式来扩容2倍
                                  在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;  
                                  链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的
                                  后继节点,1.8遍历链表,将元素放置到链表的最后;
                                   
                             
                                              防止发生hash冲突,链表长度过长,将时间复杂度由O(n)降为O(logn); 
                                              因为1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;
                                              A线程在插入节点B,B线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,后遍历到的B节点放入了头部,
                                              这样形成了环

特点

 底层是哈希表,查询的速度特别快   
       1.8前:数组+单向链表 
       1.8后:数组+单向链表/红黑树(链表长度大于8)“提高查询速度
       而且是无序的,指的是存储和取出的顺序 
        

注意:之所以Map集合可以过滤相同的键,是因为key的类型要重写hashcode和equals方法
          我们平常使用的包装类都是已经覆写过的,所有可以直接使用,过滤才有效
          当我们要存储自定义的数据类型是,一定要覆写这两个方法,否则达不到储存效果
         
         扩容的情况
         1、 存放新值的时候当前已有元素的个数必须大于等于阈值
        2、 存放新值的时候当前存放数据发生hash碰撞



       数组的特点:查询效率高,插入,删除效率低。

       链表的特点:查询效率低,插入删除效率高。
       在HashMap底层使用数组加(链表或红黑树)的结构完美的解决了数组和链表的问题,使得查询和插入,删除的效率都很高。
  
   该类的构造  4public HashMap() 无参的形式   该类内部默认的使用了,容量为16,负载因子为0.75f
    public HashMap(int initialCapacity)指定初始容量,且负载因子默认的为0.75f,因为是类定义的默认值,他会调用下面的构造
    public HashMap(int initialCapacity, float loadFactor)  指定容量,指定负载因子
   
   
       ConcurrentHashMap 类(是 Java并发包 java.util.concurrent 中提供的一个线程安全且高效的 HashMap 实现)。
       而针对 ConcurrentHashMap,在 JDK 1.7 中采用 分段锁的方式;ConcurrentHashMap中的分段锁称为Segment,
       Segment继承了ReentrantLock),当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,
       然后对这个分 段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
       
       JDK 1.8 中直接采用了CAS(无锁算法)+ synchronized。
       HashMap & ConcurrentHashMap 的区别?
        除了加锁,原理上无太大区别。另外,HashMap 的键值对允许有null,但是ConCurrentHashMap 都不允许。 

LinkedHashMap

           HashMap的子类
 特点   
        底层是哈希表+链表(保证迭代的顺序) 
        是一个有序的,存储和取出的顺序一样  
        其他和父类一样,也是不同步的
        使用场景:在需要输出的顺序和输入的顺序相同的情况下 
         
         是通过双链表表的结构来维护节点的顺序的
          
           可以存null值。null键
            
            那么如何实现呢  我们需要重写这个方法removeEldestEntry(Map.Entry<K,V> eldest)
            这个方法是linkedhashmap的,默认返回的是false ,当我们去添加值得
            覆写了afterNodeInsertion方法,在put后会去调用removeEldestEntry方法查看返回的布尔值,为true,则按照访问顺序排序,
            超出了设置的缓存最大值,则删除最旧的节点对象,首节点,就一直这样,然后访问了数据后,会将数据的引用被最后面的引用,保证了一种访问顺序,
             afterNodeInsertion方法是父类hashmap的
                我们可以用一个类来继承该类,然后重写该方法,设置缓存的数量,当存储的数量为多少时,就开始删除老的数据
                LinkedHashMap中的get方法与父类HashMap处理逻辑相似,不同之处在于增加了一处链表更新的逻辑。如果LinkedHashMap中存在要寻找的节点,那
                么判断如果设置了accessOrder,则在返回值之前,将该节点移动到对应桶中链表的尾部。也就是我们说的访问顺序 
                 
                 或者使用构造的方式也可以,第一个参数是容器大小,第二个是加载因子,第三个则是布尔是,true为访问顺序
                 
                 其他的和hashmap一致,只不过多了个双向链表
                
          
          他得内部有一个类Entry<t,v>继承了HashMap.Node<k,v>内部类
           且在该类里面声明了Entry<t,v> 当前类得属性  before和after,相当于两个指针
            
            LinkedHsahmap类也定义了这个内部类的属性  
            transient LinkedHashMap.Entry<K,V> head;
            transient LinkedHashMap.Entry<K,V> tail;
             
             构造 
public LinkedHashMap() {
        super();
        accessOrder = false;
    }

  public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }
 public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
 public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

     可以自定义容量大小和负载因子
       还有一个参数是     accessOrder = false;      
       它的作用是什么呢 ,
       当为false时获取的顺序则就是插入时的顺序,如果是true的话,是访问的顺序
       就是访问哪个就使哪个在前面 
        
        可以用作缓存
        

HashTable

    该类是线程安全的,内部使用的是synchronized
    此类实现一个哈希表 ,类似hashMap,      哈希表也叫散列表 
     核心是基于哈希值得桶和列表
    该哈希表将键映射到相应的值。任何非 null 对象都可以用作键或值
    也就是说,键和值都不可以为空
    而是hashTable是同步,说明是单线程,速度慢 ,该类是1.0版本就有的 
    是最早的双列集合,其他的map都是在1.2版本后才出现的
      
      hashtable默认的初始大小为11,之后的每次扩充,容量变为u按了的2n+1,
      
      
      Hashtable:底层是一个哈希表,是一个线程安全的集合  
      HashTable 是使用 synchronize 关键字加锁的原理(就是对对象加锁);
  

   


      
      我们之前学的所有集合都可以存储null键,null值  
      但hashtable不可以 ,,注意注意,是所有的集合,list,set等,其他的map及子类
     
     Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。  由该方法rehash内进行的扩容,但要到数组大小超过了阈值时会扩容
         负载因子为0.75 ,

    创建时,如果给定了容量初始值,那么Hashtable会直接使用你给定的大小,HashMap会将其向上扩充为2的幂。也就是说Hashtable会尽量使用素数、奇
    数。而HashMap则总是使用2的幂作为哈希表的大小。
    扩容是key不存在时,先判断是否需要扩容,则先扩容,在插入值
    
    之所以会有这样的不同,是因为Hashtable和HashMap设计时的侧重点不同,
    hashtable的侧重点是哈希的结果更加均匀,使得哈希冲突减少,当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀
    而HashMap则更加关注hash的计算效率问题,HashMap为了加快hash的速度,将哈希表的大小固定为了2的幂。当然这引入了哈希分布不均匀的问题
    所以HashMap为解决这问题,又对hash算法做了一些改动。 这从而导致了Hashtable和HashMap的计算hash值的方法不同
    
  Hashtable直接使用对象的hashCode。hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后在对数组的长度取模。获取下标 
  但这种方式每一次都要做取模运算运算,效率不太好

  Hashtable在计算元素的位置时需要进行一次除法运算,而除法运算是比较耗时的。
  HashMap为了提高计算效率,将哈希表的大小固定为了2的幂,这样去数组下标时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。
  HashMap的效率虽然提高了,但是hash冲突却也增加了。因为它得出的hash值的低位相同的概率比较高
   
   
   
          
     与hashmap相比提供了 
      Enumeration elements = t.elements();提供了这种迭代的方式,   
      这种是比较老的,
       
       Hashtable和vectir集合一样,都被其他的集合取代了 
       但是Hashtable的子类Properties依然很活跃
       它是唯一一个和io流结合的集合
        
        
      为啥Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null? 
        
        这是因为Hashtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据。
        如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,
        ConcurrentHashMap同理。         
         
         fail-safe是什么 
             快速失败(fail—fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修
             改),则会抛出Concurrent Modification Exception。
          原理是啥?
          迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。
          集合在被遍历期间如果内容发生变化,就会改变modCount的值。
          每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount是否为expectedmodCount值,是的话就返回遍历;否抛出异常,终止遍历。
          这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件

最后:

           那你平常怎么解决这个线程不安全的问题?
           
           java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap可以实现线程安全的Map。
           而且该collentions工具类里,来提供了synchronizedList,synchronizedMap,synchronizedCollection,  支持同步的,而这些都是内部类
           
           HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大,
           Collections.synchronizedMap是使用Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法内通过
                对象锁实现;
           ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高。(1.7)
           
                     
                     ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成,HashEntry则用于存储键值对数据,
                     一个ConcurrentHashMap里包含一个Segment数组,一个Segment里包含一个HashEntry,hashentry是链表
                     
                     ConcurrentHashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。变成了红黑树
                     Segment 是 ConcurrentHashMap 的一个内部类,其中 Segment 继承于 ReentrantLock。
                     每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
                     Segment ,它的初始化容量是16
                     就是说如果容量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。
                     是怎么做到线程安全得呢
                        put逻辑

                         Put方法首先定位到Segment,然后在Segment里进行插入。插入操作需要经历两个步骤0,第一步判断是否需要对Segment里的HashEntry 数组
                        进行扩容,第二步定位添加元素的位置然后放在HashEntry数组里。
                        
                        判断是否初始化,无就初始化
                        进行第一次key的hash来定位Segment的位置,
                        找到相应  的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承
                        ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,
                        那当前线程会以自旋的方式,去继续的调用tryLock()方法去获取锁,超过指定次数就挂起
                  
                        
             
                   size
                          因为并发操作,计算size的时候,还在并发的插入数据,可能会导致size和实际的size有相差,两种方案
                          1、第一种方案他会使用不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,比较前后两次计算的结果,结果一致就认为当前没
                               有元素加入,计算的结果是准确的
                         2.  第二种方案是如果第一种方案不符合,他就会给每个Segment加上锁,然后计算ConcurrentHashMap的size返回(美团面试官的问题,多个线程下
                             如何确定size)
                         
                   get
                         get过程不需要加锁,除非读到的值是空的才会加锁重读为什么不需要加锁,因为使用了volatile
                         只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
                         由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
                         
                         扩容类似hashmap 1.7 ,因为现在的concurr是1.7版的
                         
                         你有没有发现1.7虽然可以支持每个Segment并发访问,但是还是存在一些问题?
                             是的,因为基本上还是数组加链表的方式,我们去查询的时候,还得遍历链表,会导致效率很低
                             这个跟jdk1.7的HashMap是存在的一样问题

                       jdk1.7使用得是segment,1.8使用得是CAS和synchronized

                       1.8
                       ConcurrentHashMap成员变量使用volatile 修饰,免除了指令重排序,同时保证内存可见性,另外使用CAS操作和synchronized结合实现赋值操作,
                       多线程操作只会锁住当前操作索引的节点。 
                       当然这种方式,键和值都不能为空

                        put
                                根据 key 计算出 hashcode 。
                                判断是否需要进行初始化。 
                                
                                1.key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
                                2.然后判断是否扩容
                                3.在接着使用synchronized写入数据
                                这三个是else if的关系
                                 
                                 判断是否需要变成红黑树
                       get
                               根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
                               如果是红黑树那就按照树的方式获取值。
                               就不满足那就按照链表的方式遍历获取值。
                         
                      他内部对节点的值和下一个节点使用了volatile ,也就是保证了可见性
                      并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)。
                      
                     当我们在构造里指定容量时    他会先右移一位然后再加上自己再加1,然后再向上取2的幂,无参,容量默认是16,阈值也是12 
                     
                    
                      
                  
                  
           
           有没有有序的Map?
                   LinkedHashMap 和 TreeMap
                   LinkedHashMap内部维护了一个双向单链表,有头尾节点,同时LinkedHashMap节点Entry内部除了继承HashMap的Node属性,还有before 和 after
                   用于
                   标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。      

        讲讲TreeMap怎么实现有序的?
                  TreeMap是按照Key的自然顺序或者Comprator的顺序进行排序,内部是通过红黑树来实现。所以要么key所属的类实现Comparable接口,或者自定义
                  一个实现了Comparator接口的比较器,传给TreeMap用户key的比较。

TreeMap

public class TreeMap<K,V>extends AbstractMap<K,V>implements NavigableMap<K,V>, Cloneable, Serializable
基于红黑树,,此实现不是同步的
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值