HashTable实现原理以及源码解析

HashTable实现原理以及源码解析


1、HashTable
和HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射。
Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。

Hashtable中的映射不是有序的。

Hashtable 的实例有两个参数影响其性能:初始容量 和 加载因子。
容量 是哈希表中桶 的数量,初始容量 就是哈希表创建时的容量。
在发生“哈希冲突”的情况下,单个桶会存储多个条目,这些条目必须按顺序搜索。
加载因子 是对哈希表在其容量自动增加之前可以达到多满的一个尺度。初始容量和加载因子这两个参数只是对该实现的提示。
通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。
加载因子过高虽然减少了空间开销,但同时也增加了查找某个条目的时间。

2、Hashtable的构造函数:
	public Hashtable(int initialCapacity, float loadFactor) {//可指定初始容量和加载因子  
        if (initialCapacity < 0)  
            throw new IllegalArgumentException("Illegal Capacity: "+  
                                               initialCapacity);  
        if (loadFactor <= 0 || Float.isNaN(loadFactor))  
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);  
        if (initialCapacity==0)  
            initialCapacity = 1;//初始容量最小值为1  
        this.loadFactor = loadFactor;  
        table = new Entry[initialCapacity];//创建桶数组  
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//初始化容量阈值  
        useAltHashing = sun.misc.VM.isBooted() &&  
                (initialCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);  
    }  
    /** 
     * Constructs a new, empty hashtable with the specified initial capacity 
     * and default load factor (0.75). 
     */  
    public Hashtable(int initialCapacity) {  
        this(initialCapacity, 0.75f);//默认负载因子为0.75  
    }  
    public Hashtable() {  
        this(11, 0.75f);//默认容量为11,负载因子为0.75  
    }  
    /** 
     * Constructs a new hashtable with the same mappings as the given 
     * Map.  The hashtable is created with an initial capacity sufficient to 
     * hold the mappings in the given Map and a default load factor (0.75). 
     */  
    public Hashtable(Map<? extends K, ? extends V> t) {  
        this(Math.max(2*t.size(), 11), 0.75f);  
        putAll(t);  
    }  
需注意的点:
1.Hashtable的默认容量为11,默认负载因子为0.75.(HashMap默认容量为16,默认负载因子也是0.75)
2.Hashtable的容量可以为任意整数,最小值为1,而HashMap的容量始终为2的n次方。
3.为避免扩容带来的性能问题,建议指定合理容量。
4.跟HashMap一样,Hashtable内部也有一个静态类叫Entry,其实是个键值对对象,保存了键和值的引用。
5.HashMap和Hashtable存储的是键值对对象,而不是单独的键或值。

Hashtable的API:

synchronized void                clear()
synchronized Object              clone()
boolean             contains(Object value)
synchronized boolean             containsKey(Object key)
synchronized boolean             containsValue(Object value)
synchronized Enumeration<V>      elements()
synchronized Set<Entry<K, V>>    entrySet()
synchronized boolean             equals(Object object)
synchronized V                   get(Object key)
synchronized int                 hashCode()
synchronized boolean             isEmpty()
synchronized Set<K>              keySet()
synchronized Enumeration<K>      keys()
synchronized V                   put(K key, V value)
synchronized void                putAll(Map<? extends K, ? extends V> map)
synchronized V                   remove(Object key)
synchronized int                 size()
synchronized String              toString()
synchronized Collection<V>       values()

3、Hashtable数据结构:

    
Hashtable的继承关系:
	java.lang.Object
	   ↳     java.util.Dictionary<K, V>
			 ↳     java.util.Hashtable<K, V>

	public class Hashtable<K,V> extends Dictionary<K,V>
		implements Map<K,V>, Cloneable, java.io.Serializable { }
1)Hashtable继承于Dictionary类,实现了Map接口。
Map是"key-value键值对"接口,Dictionary是声明了操作"键值对"函数接口的抽象类。
Dictionary是个被废弃的抽象类。
2)Hashtable是通过"拉链法"实现的哈希表。
它包括几个重要的成员变量:table, count, threshold, loadFactor, modCount。
table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。 
count是Hashtable的大小,它是Hashtable保存的键值对的数量。 
threshold是Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值="容量*加载因子"。
loadFactor就是加载因子。 
modCount是用来实现fail-fast机制的

4、Hashtable存取数据:
存数据(put):
	public synchronized V put(K key, V value) {//向哈希表中添加键值对  
        // Make sure the value is not null  
        if (value == null) {//确保值不能为空  
            throw new NullPointerException();  
        }  
        // Makes sure the key is not already in the hashtable.  
        Entry tab[] = table;  
        int hash = hash(key);//根据键生成hash值---->若key为null,此方法会抛异常  
        int index = (hash & 0x7FFFFFFF) % tab.length;//通过hash值找到其存储位置  
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {/遍历链表  
            if ((e.hash == hash) && e.key.equals(key)) {//若键相同,则新值覆盖旧值  
                V old = e.value;  
                e.value = value;  
                return old;  
            }  
        }  
        modCount++;  
        if (count >= threshold) {//当前容量超过阈值。需要扩容  
            // Rehash the table if the threshold is exceeded  
            rehash();//重新构建桶数组,并对数组中所有键值对重哈希,耗时!  
            tab = table;  
            hash = hash(key);  
            index = (hash & 0x7FFFFFFF) % tab.length;//这里是取摸运算  
        }  
        // Creates the new entry.  
        Entry<K,V> e = tab[index];  
        //将新结点插到链表首部  
        tab[index] = new Entry<>(hash, key, value, e);//生成一个新结点  
        count++;  
        return null;  
    }
1.Hasbtable并不允许值和键为空(null),若为空,会抛空指针。
2.HashMap计算索引的方式是h&(length-1),而Hashtable用的是模运算,效率上是低于HashMap的。
3.另外Hashtable计算索引时将hash值先与上0x7FFFFFFF,这是为了保证hash值始终为正数。
4.特别需要注意的是这个方法包括下面要讲的若干方法都加了synchronized关键字,也就意味着这个Hashtable是个线程安全的类,这也是它和HashMap最大的不同点.

Hashtable扩容方法rehash:
	protected void rehash() {  
        int oldCapacity = table.length;//记录旧容量  
        Entry<K,V>[] oldMap = table;//记录旧的桶数组  
        // overflow-conscious code  
        int newCapacity = (oldCapacity << 1) + 1;//新容量为老容量的2倍加1  
        if (newCapacity - MAX_ARRAY_SIZE > 0) {  
            if (oldCapacity == MAX_ARRAY_SIZE)//容量不得超过约定的最大值  
                // Keep running with MAX_ARRAY_SIZE buckets  
                return;  
            newCapacity = MAX_ARRAY_SIZE;  
        }  
        Entry<K,V>[] newMap = new Entry[newCapacity];//创建新的数组  
        modCount++;  
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);  
        boolean currentAltHashing = useAltHashing;  
        useAltHashing = sun.misc.VM.isBooted() &&  
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);  
        boolean rehash = currentAltHashing ^ useAltHashing;  
        table = newMap;  
        for (int i = oldCapacity ; i-- > 0 ;) {//转移键值对到新数组  
            for (Entry<K,V> old = oldMap[i] ; old != null ; ) {  
                Entry<K,V> e = old;  
                old = old.next;  
                if (rehash) {  
                    e.hash = hash(e.key);  
                }  
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;  
                e.next = newMap[index];  
                newMap[index] = e;  
            }  
        }  
    }  	
Hashtable每次扩容,容量都为原来的2倍加1,而HashMap为原来的2倍。

取数据(get):
        public synchronized V get(Object key) {//根据键取出对应索引  
      Entry tab[] = table;  
      int hash = hash(key);//先根据key计算hash值  
      int index = (hash & 0x7FFFFFFF) % tab.length;//再根据hash值找到索引  
      for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {//遍历entry链  
          if ((e.hash == hash) && e.key.equals(key)) {//若找到该键  
              return e.value;//返回对应的值  
          }  
      }  
      return null;//否则返回null  
	}
当然,如果你传的参数为null,是会抛空指针的。

5、Hashtable的主要对外接口:
1.clear() 的作用是清空Hashtable。它是将Hashtable的table数组的值全部设为null.
		public synchronized void clear() {
			Entry tab[] = table;
			modCount++;
			for (int index = tab.length; --index >= 0; )
				tab[index] = null;
			count = 0;
		}
2.contains() 和 containsValue() 的作用都是判断Hashtable是否包含“值(value)”
		public boolean containsValue(Object value) {
			return contains(value);
		}

		public synchronized boolean contains(Object value) {
			// Hashtable中“键值对”的value不能是null,
			// 若是null的话,抛出异常!
			if (value == null) {
				throw new NullPointerException();
			}


			// 从后向前遍历table数组中的元素(Entry)
			// 对于每个Entry(单向链表),逐个遍历,判断节点的值是否等于value
			Entry tab[] = table;
			for (int i = tab.length ; i-- > 0 ;) {
				for (Entry<K,V> e = tab[i] ; e != null ; e = e.next) {
					if (e.value.equals(value)) {
						return true;
					}
				}
			}
			return false;
		}
3.containsKey() 的作用是判断Hashtable是否包含key
		public synchronized boolean containsKey(Object key) {
			Entry tab[] = table;
			int hash = key.hashCode();
			// 计算索引值,
			// % tab.length 的目的是防止数据越界
			int index = (hash & 0x7FFFFFFF) % tab.length;
			// 找到“key对应的Entry(链表)”,然后在链表中找出“哈希值”和“键值”与key都相等的元素
			for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
				if ((e.hash == hash) && e.key.equals(key)) {
					return true;
				}
			}
			return false;
		}
4.elements() 的作用是返回“所有value”的枚举对象
		public synchronized Enumeration<V> elements() {
			return this.<V>getEnumeration(VALUES);
		}

		// 获取Hashtable的枚举类对象
		private <T> Enumeration<T> getEnumeration(int type) {
			if (count == 0) {
				return (Enumeration<T>)emptyEnumerator;
			} else {
				return new Enumerator<T>(type, false);
			}
		}
若Hashtable的实际大小为0,则返回“空枚举类”对象emptyEnumerator;
否则,返回正常的Enumerator的对象。

EmptyEnumerator对象是如何实现的:
	private static Enumeration emptyEnumerator = new EmptyEnumerator();

	// 空枚举类
	// 当Hashtable的实际大小为0;此时,又要通过Enumeration遍历Hashtable时,返回的是“空枚举类”的对象。
	private static class EmptyEnumerator implements Enumeration<Object> {


		EmptyEnumerator() {
		}


		// 空枚举类的hasMoreElements() 始终返回false
		public boolean hasMoreElements() {
			return false;
		}


		// 空枚举类的nextElement() 抛出异常
		public Object nextElement() {
			throw new NoSuchElementException("Hashtable Enumerator");
		}
	}
Enumerator的作用是提供了“通过elements()遍历Hashtable的接口” 和 “通过entrySet()遍历Hashtable的接口”。
	private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
		// 指向Hashtable的table
		Entry[] table = Hashtable.this.table;
		// Hashtable的总的大小
		int index = table.length;
		Entry<K,V> entry = null;
		Entry<K,V> lastReturned = null;
		int type;


		// Enumerator是 “迭代器(Iterator)” 还是 “枚举类(Enumeration)”的标志
		// iterator为true,表示它是迭代器;否则,是枚举类。
		boolean iterator;


		// 在将Enumerator当作迭代器使用时会用到,用来实现fail-fast机制。
		protected int expectedModCount = modCount;


		Enumerator(int type, boolean iterator) {
			this.type = type;
			this.iterator = iterator;
		}


		// 从遍历table的数组的末尾向前查找,直到找到不为null的Entry。
		public boolean hasMoreElements() {
			Entry<K,V> e = entry;
			int i = index;
			Entry[] t = table;
			/* Use locals for faster loop iteration */
			while (e == null && i > 0) {
				e = t[--i];
			}
			entry = e;
			index = i;
			return e != null;
		}
		//获取下一个元素
		// 注意:从hasMoreElements() 和nextElement() 可以看出“Hashtable的elements()遍历方式”
		// 首先,从后向前的遍历table数组。table数组的每个节点都是一个单向链表(Entry)。
		// 然后,依次向后遍历单向链表Entry。
		public T nextElement() {
			Entry<K,V> et = entry;
			int i = index;
			Entry[] t = table;
			/* Use locals for faster loop iteration */
			while (et == null && i > 0) {
				et = t[--i];
			}
			entry = et;
			index = i;
			if (et != null) {
				Entry<K,V> e = lastReturned = entry;
				entry = e.next;
				return type == KEYS ? (T)e.key : (type == VALUES ? (T)e.value : (T)e);
			}
			throw new NoSuchElementException("Hashtable Enumerator");
		}


		// 迭代器Iterator的判断是否存在下一个元素
		// 实际上,它是调用的hasMoreElements()
		public boolean hasNext() {
			return hasMoreElements();
		}


		// 迭代器获取下一个元素
		// 实际上,它是调用的nextElement()
		public T next() {
			if (modCount != expectedModCount)
				throw new ConcurrentModificationException();
			return nextElement();
		}


		// 迭代器的remove()接口。
		// 首先,它在table数组中找出要删除元素所在的Entry,
		// 然后,删除单向链表Entry中的元素。
		public void remove() {
			if (!iterator)
				throw new UnsupportedOperationException();
			if (lastReturned == null)
				throw new IllegalStateException("Hashtable Enumerator");
			if (modCount != expectedModCount)
				throw new ConcurrentModificationException();


			synchronized(Hashtable.this) {
				Entry[] tab = Hashtable.this.table;
				int index = (lastReturned.hash & 0x7FFFFFFF) % tab.length;


				for (Entry<K,V> e = tab[index], prev = null; e != null;
					 prev = e, e = e.next) {
					if (e == lastReturned) {
						modCount++;
						expectedModCount++;
						if (prev == null)
							tab[index] = e.next;
						else
							prev.next = e.next;
						count--;
						lastReturned = null;
						return;
					}
				}
				throw new ConcurrentModificationException();
			}
		}
	}
5.get() 的作用就是获取key对应的value,没有的话返回null
public synchronized V get(Object key) {
Entry tab[] = table;
int hash = key.hashCode();
// 计算索引值,
int index = (hash & 0x7FFFFFFF) % tab.length;
// 找到“key对应的Entry(链表)”,然后在链表中找出“哈希值”和“键值”与key都相等的元素
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return e.value;
}
}
return null;
}

6.put() 的作用是对外提供接口,让Hashtable对象可以通过put()将“key-value”添加到Hashtable中。
	public synchronized V put(K key, V value) {
		// Hashtable中不能插入value为null的元素!!!
		if (value == null) {
			throw new NullPointerException();
		}

		// 若“Hashtable中已存在键为key的键值对”,
		// 则用“新的value”替换“旧的value”
		Entry tab[] = table;
		int hash = key.hashCode();
		int index = (hash & 0x7FFFFFFF) % tab.length;
		for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
			if ((e.hash == hash) && e.key.equals(key)) {
				V old = e.value;
				e.value = value;
				return old;
				}
		}

		// 若“Hashtable中不存在键为key的键值对”,
		// (01) 将“修改统计数”+1
		modCount++;
		// (02) 若“Hashtable实际容量” > “阈值”(阈值=总的容量 * 加载因子)
		//  则调整Hashtable的大小
		if (count >= threshold) {
			// Rehash the table if the threshold is exceeded
			rehash();
			tab = table;
			index = (hash & 0x7FFFFFFF) % tab.length;
		}

		// (03) 将“Hashtable中index”位置的Entry(链表)保存到e中
		Entry<K,V> e = tab[index];
		// (04) 创建“新的Entry节点”,并将“新的Entry”插入“Hashtable的index位置”,并设置e为“新的Entry”的下一个元素(即“新Entry”为链表表头)。        
		tab[index] = new Entry<K,V>(hash, key, value, e);
		// (05) 将“Hashtable的实际容量”+1
		count++;
		return null;
	}
7.putAll() 的作用是将“Map(t)”的中全部元素逐一添加到Hashtable中
public synchronized void putAll(Map<? extends K, ? extends V> t) {
for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
put(e.getKey(), e.getValue());
}

8.remove() 的作用就是删除Hashtable中键为key的元素
		public synchronized V remove(Object key) {
			Entry tab[] = table;
			int hash = key.hashCode();
			int index = (hash & 0x7FFFFFFF) % tab.length;
			// 找到“key对应的Entry(链表)”
			// 然后在链表中找出要删除的节点,并删除该节点。
			for (Entry<K,V> e = tab[index], prev = null ; e != null ; prev = e, e = e.next) {
				if ((e.hash == hash) && e.key.equals(key)) {
					modCount++;
					if (prev != null) {
						prev.next = e.next;
					} else {
						tab[index] = e.next;
					}
					count--;
					V oldValue = e.value;
					e.value = null;
					return oldValue;
				}
			}
			return null;
		}
6、Hashtable实现的Cloneable接口
Hashtable实现了Cloneable接口,即实现了clone()方法。
clone()方法的作用很简单,就是克隆一个Hashtable对象并返回。

// 克隆一个Hashtable,并以Object的形式返回。
public synchronized Object clone() {
try {
Hashtable<K,V> t = (Hashtable<K,V>) super.clone();
t.table = new Entry[table.length];
for (int i = table.length ; i-- > 0 ; ) {
t.table[i] = (table[i] != null)
? (Entry<K,V>) table[i].clone() : null;
}
t.keySet = null;
t.entrySet = null;
t.values = null;
t.modCount = 0;
return t;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError();
}
}


7、Hashtable实现的Serializable接口
Hashtable实现java.io.Serializable,分别实现了串行读取、写入功能。
串行写入函数就是将Hashtable的“总的容量,实际容量,所有的Entry”都写入到输出流中
串行读取函数:根据写入方式读出将Hashtable的“总的容量,实际容量,所有的Entry”依次读出
	private synchronized void writeObject(java.io.ObjectOutputStream s)
		throws IOException
	{
		// Write out the length, threshold, loadfactor
		s.defaultWriteObject();


		// Write out length, count of elements and then the key/value objects
		s.writeInt(table.length);
		s.writeInt(count);
		for (int index = table.length-1; index >= 0; index--) {
			Entry entry = table[index];


			while (entry != null) {
			s.writeObject(entry.key);
			s.writeObject(entry.value);
			entry = entry.next;
			}
		}
	}


	private void readObject(java.io.ObjectInputStream s)
		 throws IOException, ClassNotFoundException
	{
		// Read in the length, threshold, and loadfactor
		s.defaultReadObject();


		// Read the original length of the array and number of elements
		int origlength = s.readInt();
		int elements = s.readInt();


		// Compute new size with a bit of room 5% to grow but
		// no larger than the original size.  Make the length
		// odd if it's large enough, this helps distribute the entries.
		// Guard against the length ending up zero, that's not valid.
		int length = (int)(elements * loadFactor) + (elements / 20) + 3;
		if (length > elements && (length & 1) == 0)
			length--;
		if (origlength > 0 && length > origlength)
			length = origlength;


		Entry[] table = new Entry[length];
		count = 0;


		// Read the number of elements and then all the key/value objects
		for (; elements > 0; elements--) {
			K key = (K)s.readObject();
			V value = (V)s.readObject();
				// synch could be eliminated for performance
				reconstitutionPut(table, key, value);
		}
		this.table = table;
	}
8、遍历Hashtable的键值对(获取键值集)
第一步:根据entrySet()获取Hashtable的“键值对”的Set集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。
// 假设table是Hashtable对象
// table中的key是String类型,value是Integer类型
Integer integ = null;
Iterator iter = table.entrySet().iterator();
while(iter.hasNext()) {
Map.Entry entry = (Map.Entry)iter.next();
// 获取key
key = (String)entry.getKey();
// 获取value
integ = (Integer)entry.getValue();
}

9、通过Iterator遍历Hashtable的键(获取键集)
第一步:根据keySet()获取Hashtable的“键”的Set集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。

// 假设table是Hashtable对象
// table中的key是String类型,value是Integer类型
String key = null;
Integer integ = null;
Iterator iter = table.keySet().iterator();
while (iter.hasNext()) {
// 获取key
key = (String)iter.next();
// 根据key,获取value
integ = (Integer)table.get(key);
}

10、通过Iterator遍历Hashtable的值(获取值集)
第一步:根据value()获取Hashtable的“值”的集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。
// 假设table是Hashtable对象
// table中的key是String类型,value是Integer类型
Integer value = null;
Collection c = table.values();
Iterator iter= c.iterator();
while (iter.hasNext()) {
value = (Integer)iter.next();
}

11、通过Enumeration遍历Hashtable的键(获取键集)
第一步:根据keys()获取Hashtable的集合。
第二步:通过Enumeration遍历“第一步”得到的集合。
Enumeration enu = table.keys();
while(enu.hasMoreElements()) {
System.out.println(enu.nextElement());
}   

12、通过Enumeration遍历Hashtable的值(获取值集)
第一步:根据elements()获取Hashtable的集合。
第二步:通过Enumeration遍历“第一步”得到的集合。
Enumeration enu = table.elements();
while(enu.hasMoreElements()) {
System.out.println(enu.nextElement());
}

13、总结:
1.Hashtable是个线程安全的类(HashMap线程安全);
2.Hasbtable并不允许值和键为空(null),若为空,会抛空指针(HashMap可以);
3.Hashtable不允许键重复,若键重复,则新插入的值会覆盖旧值(同HashMap);
4.Hashtable同样是通过链表法解决冲突;
5.Hashtable根据hashcode计算索引时将hashcode值先与上0x7FFFFFFF,这是为了保证hash值始终为正数;
6.Hashtable的容量为任意正数(最小为1),而HashMap的容量始终为2的n次方。Hashtable默认容量为11,HashMap默认容量为16;
7.Hashtable每次扩容,新容量为旧容量的2倍加1,而HashMap为旧容量的2倍;
8.Hashtable和HashMap默认负载因子都为0.75;



参考资料:
JDK API HashMap
HashTable 源代码  
http://www.cnblogs.com/skywang12345/p/3310887.html


每天努力一点,每天都在进步。
  • 8
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

powerfuler

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值