Google Guava 常用特性总结

一、Objects方法

1.equals

当一个对象中的字段可以为null时,实现Object.equals方法会很痛苦,因为不得不分别对它们进行null检查。使用Objects.equal帮助你执行null敏感的equals判断,从而避免抛出NullPointerException。例如:

Objects.equal("a", "a"); // returns true
Objects.equal(null, "a"); // returns false
Objects.equal("a", null); // returns false
Objects.equal(null, null); // returns true

ps:JDK7引入的Objects类提供了一样的方法Objects.equals。

2.hashCode

Guava的Objects.hashCode(Object...)会对传入的字段序列计算出合理的、顺序敏感的散列值。你可以使用Objects.hashCode(field1, field2, …, fieldn)来代替手动计算散列值。

Objects.hashCode("abc", 11, new User());

ps:JDK7引入的Objects类提供了一样的方法Objects.equals。

3.toString

使用 Objects.toStringHelper可以轻松编写有用的toString方法。例如:

// Returns "ClassName{x=1}"
Objects.toStringHelper(this).add("x", 1).toString();
// Returns "User{id=1, name=a}"
Objects.toStringHelper("User").add("id", 1).add("name", "a").toString();

4.compare/compareTo

实现一个比较器[Comparator],或者直接实现Comparable接口有时也伤不起。考虑一下这种情况:

class Person implements Comparable<Person> {
  private String lastName;
  private String firstName;
  private int zipCode;

  public int compareTo(Person other) {
    int cmp = lastName.compareTo(other.lastName);
    if (cmp != 0) {
      return cmp;
    }
    cmp = firstName.compareTo(other.firstName);
    if (cmp != 0) {
      return cmp;
    }
    return Integer.compare(zipCode, other.zipCode);
  }
}

使用Guava提供的 ComparisonChain,可以将compareTo方法改写为:

public int compareTo(Person that) {
    return ComparisonChain.start()
            .compare(this.lastName, that.lastName)
            .compare(this.firstName, that.firstName)
            .compare(this.zipCode, that.zipCode, Ordering.natural().nullsLast())
            .result();
}

 Ordering.natural().nullsLast() 表示根据数字字符串自然排序(从小到大),但把null值排到最后面。

二、集合类

1.不可变集合:ImmutableSet

为什么使用不可变集合:

1)当对象被不可信的库调用时,不可变形式是安全的;
2)不可变对象被多个线程调用时,不存在竞态条件问题
3)不可变集合不需要考虑变化,因此可以节省时间和空间。所有不可变的集合都比它们的可变形式有更好的内存利用率(分析和测试细节);
4)不可变对象因为有固定不变,可以作为常量来安全使用。

不可变集合的创建:

1)copyOf方法

ImmutableSet.copyOf(bars)

2)of方法

ImmutableSet.of(“a”, “b”, “c”)

3)Builder工具

public static final ImmutableSet<Color> GOOGLE_COLORS =
        ImmutableSet.<Color>builder()
            .addAll(WEBSAFE_COLORS)
            .add(new Color(0, 191, 255))
            .build();

ps:对有序不可变集合 ImmutableSortedSet 来说,排序是在构造集合的时候完成的,如下,顺序为a, b, c, d

ImmutableSortedSet.of("a", "b", "c", "a", "d", "b");

2. 带计数功能的集合:Multiset

Multiset以Map<T,Count>为存储结构,一种元素并没有被存多分,而且巧妙的利用iterator指针来模拟多份数据。可以用两种方式看待Multiset:

1)没有元素顺序限制的ArrayList<E>
2)Map<E, Integer>,键为元素,值为计数

统计一个词在文档中出现了多少次,传统的做法是这样的:

Map<String, Integer> counts = new HashMap<String, Integer>();
for (String word : words) {
    Integer count = counts.get(word);
    if (count == null) {
        counts.put(word, 1);
    } else {
        counts.put(word, count + 1);
    }
}

如果使用Multset就变成:

HashMultiset<String> multiSet = HashMultiset.create(); 
 multiSet.addAll(words); 
 //count word “the”
 Integer count = multiSet.count(“the”);

使用方法有:

方法描述
count(E)给定元素在Multiset中的计数
elementSet()Multiset中不重复元素的集合,类型为Set<E>
entrySet()和Map的entrySet类似,返回Set<Multiset.Entry<E>>,其中包含的Entry支持getElement()和getCount()方法
add(E, int)增加给定元素在Multiset中的计数
remove(E, int)减少给定元素在Multiset中的计数
setCount(E, int)设置给定元素在Multiset中的计数,不可以为负数
size()返回集合元素的总个数(包括重复的元素)

特点:

1)Multiset中的元素计数只能是正数。任何元素的计数都不能为负,也不能是0。elementSet()和entrySet()视图中也不会有这样的元素。
2)multiset.size()返回集合的大小,等同于所有元素计数的总和。对于不重复元素的个数,应使用elementSet().size()方法。(因此,add(E)把multiset.size()增加1)
3)multiset.iterator()会迭代重复元素,因此迭代长度等于multiset.size()。
4)Multiset支持直接增加、减少或设置元素的计数。setCount(elem, 0)等同于移除所有elem。
5)对multiset 中没有的元素,multiset.count(elem)始终返回0。

常用实现 Multiset 接口的类有:

1)HashMultiset: 元素存放于 HashMap
2)LinkedHashMultiset: 元素存放于 LinkedHashMap,即元素的排列顺序由第一次放入的顺序决定
3)TreeMultiset:元素被排序存放于TreeMap
4)EnumMultiset: 元素必须是 enum 类型
5)ImmutableMultiset: 不可修改的 Mutiset
 

3.键映射多个值:Multimap

可以用两种方式思考Multimap的概念:

1)键-单个值映射”的集合:a -> 1 a -> 2 a ->4 b -> 3 c -> 5

2)键-值集合映射”的映射:a -> [1, 2, 4] b -> 3 c -> 5

当需要 Map<K, Collection<V>>这样的结构时,如以下场景:

void putMyObject(String key, Object value) {
    List<Object> myClassList = myClassListMap.get(key);
    if(myClassList == null) {
        myClassList = new ArrayList<object>();
        myClassListMap.put(key,myClassList);
    }
    myClassList.add(value);
}

此时我们可以使用Multmap替换,如下面例子:

Multimap<String, String> myMultimap = ArrayListMultimap.create();

// Adding some key/value
myMultimap.put("Fruits", "Bannana");
myMultimap.put("Fruits", "Apple");
myMultimap.put("Fruits", "Pear");
myMultimap.put("Fruits", "Pear");
myMultimap.put("Vegetables", "Carrot");

// Getting the size
int size = myMultimap.size();
		
// Getting values
Collection<String> fruits = myMultimap.get("Fruits");
System.out.println(fruits); //  [Bannana, Apple, Pear, Pear]

Multmap的接口实现:

@GwtCompatible
public interface Multimap<K, V> {
  //返回Multimap集合的key、value pair的数量  
  int size();
  //判断Multimap是否包含key、value pair
  boolean isEmpty();
  //判断Multimap中是否包含指定key的value值
  boolean containsKey(@Nullable Object key);
  //判断Multimap中是否包含指定value的key
  boolean containsValue(@Nullable Object value);
  //判断Multimap中是否包含指定的key-value pair的数据
  boolean containsEntry(@Nullable Object key, @Nullable Object value);
  //将数据加入到Multimap中
  boolean put(@Nullable K key, @Nullable V value);
  //删除Multimap中指定key-value pair
  boolean remove(@Nullable Object key, @Nullable Object value);
  //将指定的key-集合数据加入Multimap中
  boolean putAll(@Nullable K key, Iterable<? extends V> values);
  //将指定的Multimap和当前的Multimap合并
  boolean putAll(Multimap<? extends K, ? extends V> multimap);
  //替换指定key的value
  Collection<V> replaceValues(@Nullable K key, Iterable<? extends V> values);
  //删除Imultimap中的指定key数据
  Collection<V> removeAll(@Nullable Object key);
  //清空Imultimap中的数据
  void clear();
  //获取指定key的值
  Collection<V> get(@Nullable K key);
  //获取所有的key集合
  Set<K> keySet();

  Multiset<K> keys();

  Collection<V> values();

  Collection<Map.Entry<K, V>> entries();

  Map<K, Collection<V>> asMap();

  @Override
  boolean equals(@Nullable Object obj);

  @Override
  int hashCode();
}

Multimap提供了多种形式的实现

实现键行为类似值行为类似
ArrayListMultimapHashMapArrayList
HashMultimapHashMapHashSet
LinkedListMultimap*LinkedHashMap*LinkedList*
LinkedHashMultimap**LinkedHashMapLinkedHashMap
TreeMultimapTreeMapTreeSet
ImmutableListMultimapImmutableMapImmutableList
ImmutableSetMultimapImmutableMapImmutableSet

 

4.可反转map:BiMap

这个反转的map不是新的map对象,它实现了一种视图关联,这样你对于反转后的map的所有操作都会影响原先的map对象。

传统上,实现键值对的双向映射需要维护两个单独的map,并保持它们间的同步。但这种方式很容易出错,而且对于值已经在map中的情况,会变得非常混乱。例如:

Map<String, Integer> nameToId = Maps.newHashMap();
Map<Integer, String> idToName = Maps.newHashMap();

nameToId.put("Bob", 42);
idToName.put(42, "Bob");
//如果"Bob"和42已经在map中了,会发生什么?
//如果我们忘了同步两个map,会有诡异的bug发生...

在BiMap中,可以使用 inverse()实现键值反转。如果你想把值反转成已经存在的键,会抛出IllegalArgumentException异常。如果想要强制覆盖该键的值,可以使用 BiMap.forcePut(key, value)。

例子:

 public void BimapTest(){
        BiMap<Integer,String> logfileMap = HashBiMap.create(); 
        logfileMap.put(1,"a.log");
        logfileMap.put(2,"b.log");
        logfileMap.put(3,"c.log"); 
        System.out.println("logfileMap:"+logfileMap); 
        BiMap<String,Integer> filelogMap = logfileMap.inverse();
        System.out.println("filelogMap:"+filelogMap);
        
        logfileMap.put(4,"d.log"); 

        System.out.println("logfileMap:"+logfileMap); 
        System.out.println("filelogMap:"+filelogMap); 
    }

输出为:

logfileMap:{3=c.log, 2=b.log, 1=a.log}
filelogMap:{c.log=3, b.log=2, a.log=1}
logfileMap:{4=d.log, 3=c.log, 2=b.log, 1=a.log}
filelogMap:{d.log=4, c.log=3, b.log=2, a.log=1}

BiMap的各种实现:

值实现键实现对应的BiMap实现
HashMapHashMapHashBiMap
ImmutableMapImmutableMapImmutableBiMap
EnumMapEnumMapEnumBiMap
EnumMapHashMapEnumHashBiMap

三、集合工具类拓展

1.Lists

方法描述
partition(List, int)把List按指定大小分割
reverse(List)返回给定List的反转视图。注: 如果List是不可变的,考虑改用ImmutableList.reverse()
List countUp = Ints.asList(1, 2, 3, 4, 5);
List countDown = Lists.reverse(theList); // {5, 4, 3, 2, 1}
List<List> parts = Lists.partition(countUp, 2);//{{1,2}, {3,4}, {5}}

2.Sets

方法
union(Set, Set)
intersection(Set, Set)
difference(Set, Set)
symmetricDifference(Set,   Set)
Set<String> wordsWithPrimeLength = ImmutableSet.of("one", "two", "three", "six", "seven", "eight");
Set<String> primes = ImmutableSet.of("two", "three", "five", "seven");
SetView<String> intersection = Sets.intersection(primes,wordsWithPrimeLength);
// intersection包含"two", "three", "seven"
return intersection.immutableCopy();//可以使用交集,但不可变拷贝的读取效率更高

3.Maps

Maps.uniqueIndex(Iterable,Function)通常针对的场景是:有一组对象,它们在某个属性上分别有独一无二的值,而我们希望能够按照这个属性值查找对象,这个方法返回一个Map,键为Function返回的属性值,值为Iterable中相应的元素,因此我们可以反复用这个Map进行查找操作。

ImmutableMap<Integer, String> stringsByIndex = Maps.uniqueIndex(strings,
    new Function<String, Integer> () {
        public Integer apply(String string) {
            return string.length();
        }
    });

4.Multisets

方法说明Collection方法的区别
containsOccurrences(Multiset   sup, Multiset sub)对任意o,如果sub.count(o)<=super.count(o),返回trueCollection.containsAll忽略个数,而只关心sub的元素是否都在super中
removeOccurrences(Multiset   removeFrom, Multiset toRemove)对toRemove中的重复元素,仅在removeFrom中删除相同个数。Collection.removeAll移除所有出现在toRemove的元素
retainOccurrences(Multiset   removeFrom, Multiset toRetain)修改removeFrom,以保证任意o都符合removeFrom.count(o)<=toRetain.count(o)Collection.retainAll保留所有出现在toRetain的元素
intersection(Multiset,   Multiset)返回两个multiset的交集;没有类似方法

 

Multiset<String> multiset1 = HashMultiset.create();
multiset1.add("a", 2);

Multiset<String> multiset2 = HashMultiset.create();
multiset2.add("a", 5);

multiset1.containsAll(multiset2); //返回true;因为包含了所有不重复元素,
//虽然multiset1实际上包含2个"a",而multiset2包含5个"a"
Multisets.containsOccurrences(multiset1, multiset2); // returns false

multiset2.removeOccurrences(multiset1); // multiset2 现在包含3个"a"
multiset2.removeAll(multiset1);//multiset2移除所有"a",虽然multiset1只有2个"a"
multiset2.isEmpty(); // returns true

5.Multimaps

1)index

作为Maps.uniqueIndex的兄弟方法,Multimaps.index(Iterable, Function)通常针对的场景是:有一组对象,它们有共同的特定属性,我们希望按照这个属性的值查询对象,但属性值不一定是独一无二的。

比方说,我们想把字符串按长度分组。

ImmutableSet digits = ImmutableSet.of("zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine");
Function<String, Integer> lengthFunction = new Function<String, Integer>() {
    public Integer apply(String string) {
        return string.length();
    }
};

ImmutableListMultimap<Integer, String> digitsByLength= Multimaps.index(digits, lengthFunction);
/*
*  digitsByLength maps:
*  3 => {"one", "two", "six"}
*  4 => {"zero", "four", "five", "nine"}
*  5 => {"three", "seven", "eight"}
*/

2)invertFrom

反转Multimap。

ArrayListMultimap<String, Integer> multimap = ArrayListMultimap.create();
multimap.putAll("b", Ints.asList(2, 4, 6));
multimap.putAll("a", Ints.asList(4, 2, 1));
multimap.putAll("c", Ints.asList(2, 5, 3));

TreeMultimap<Integer, String> inverse = Multimaps.invertFrom(multimap, TreeMultimap<String, Integer>.create());
//注意我们选择的实现,因为选了TreeMultimap,得到的反转结果是有序的
/*
* inverse maps:
*  1 => {"a"}
*  2 => {"a", "b", "c"}
*  3 => {"c"}
*  4 => {"a", "b"}
*  5 => {"c"}
*  6 => {"b"}
*/

3)forMap

把Map包装成SetMultimap,与Multimaps.invertFrom结合使用,可以把多对一的Map反转为一对多的Multimap。

Map<String, Integer> map = ImmutableMap.of("a", 1, "b", 1, "c", 2);
SetMultimap<String, Integer> multimap = Multimaps.forMap(map);
// multimap:["a" => {1}, "b" => {1}, "c" => {2}]
Multimap<Integer, String> inverse = Multimaps.invertFrom(multimap, HashMultimap<Integer, String>.create());
// inverse:[1 => {"a","b"}, 2 => {"c"}]

四、缓存

适用条件:

Guava Cache适用于:

1)你愿意消耗一些内存空间来提升速度。

2)你预料到某些键会被查询一次以上。

3)缓存中存放的数据总量不会超出内存容量。(Guava Cache是单个应用运行时的本地缓存。它不把数据存放到文件或外部服务器。如果这不符合你的需求,请尝试Memcached这类工具)

简单的例子:

public class GuavaCacheTest {
    public static void main(String[] args) {
        Cache<Integer, String> cache = CacheBuilder.newBuilder().build();
        cache.put(1, "a");
        System.out.println(cache.getIfPresent(1)); //输出 a
        System.out.println(cache.getIfPresent(2)); //输出 null
    }
}

如果没有缓存,计算值并缓存,

如果使用的 LoadingCache 需要在创建对象时重写 CacheLoader 方法;

如果使用 Cache 类则后续 get 方法中多传入一个 Callable 的对象并重写 call 方法,缓存不存在时用于计算并加到缓存,此方法更为灵活。

public class GuavaCacheTest {
    public static void main(String[] args) {
        LoadingCache<Integer, String> cache = CacheBuilder.newBuilder().build(
                new CacheLoader<Integer, String>() {
                    @Override
                    public String load(Integer key) throws Exception {
                        return "key-" + key;
                    }
                }
        );
        cache.put(1, "a"); 
        System.out.println(cache.getIfPresent(1));
        try {
            System.out.println(cache.get(2));
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

=========================================================
或

public class GuavaCacheTest {
    public static void main(String[] args) {
       Cache<Integer, String> cache = CacheBuilder.newBuilder().build();
       cache.put(1, "a"); 
       try {
            cache.get(2, new Callable<String>() {
                @Override
                public String call() throws Exception {
                    return "b";
                }
            });
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

如何释放内存,防止发生OOM:限量、定时、手动

1)限量:

CacheBuilder.maximumSize(long) 按个数来回收, 

public class GuavaCacheTest {
    public static void main(String[] args) {
        Cache<Integer, String> cache = CacheBuilder.newBuilder().maximumSize(2).build();
        cache.put(1, "a");
        cache.put(2, "b");
        cache.put(3, "c");
        System.out.println(cache.asMap()); //输出{3=c, 2=b}
        System.out.println(cache.getIfPresent(2)); //输出b,此处被使用,故为最新
        cache.put(4, "d"); //输出{4=d, 2=b}
        System.out.println(cache.asMap());
    }
}

CacheBuilder.weigher(Weigher) 按权重来回收,CacheBuilder.maximumWeight(long)指定最大总权重。CacheBuilder.maximumSize(long),CacheBuilder.maximumWeight(long)是互斥的,只能二选一。

public class GuavaCacheTest {
    public static void main(String[] args) {
        Cache<Integer, Integer> cache = CacheBuilder.newBuilder().maximumWeight(100)
                .weigher(new Weigher<Integer, Integer>() {
                    @Override
                    public int weigh(Integer key, Integer value) {
                        if (value % 2 == 0) {
                            return 20;
                        } else {
                            return 5;
                        }
                    }
                }).build();
//         放偶数
        for (int i = 0; i <= 20; i += 2) {
            cache.put(i, i);
        }
        System.out.println(cache.asMap()); //输出{20=20, 18=18, 16=16, 14=14}
        cache.invalidateAll(); //全部失效
        for (int i = 1; i < 10; i += 1) {
            cache.put(i, i);
        }
        System.out.println(cache.asMap()); //输出{6=6, 5=5, 8=8, 7=7, 2=2, 9=9, 3=3, 4=4}
    }
}

2)定时

expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问 来回收

public class GuavaCacheTest {
    public static void main(String[] args) {
        Cache<Integer, Integer> cache = CacheBuilder.newBuilder().expireAfterWrite(3, TimeUnit.SECONDS).build();
        cache.put(1,1);
        cache.put(2,2);
        System.out.println(cache.asMap()); //输出{1=1, 2=2}
        try {
            Thread.sleep(1000);
            cache.getIfPresent(2); //使用2,该值会重新计时
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(cache.asMap()); //输出{2=2}
    }
}

3)限量+定时

Cache<Integer, Integer> cache = CacheBuilder.newBuilder().maximumSize(100).expireAfterAccess(3, TimeUnit.SECONDS).build();

4)手动

个别清除:Cache.invalidate(key)

批量清除:Cache.invalidateAll(keys)

清除所有缓存项:Cache.invalidateAll()

public class GuavaCacheTest {
    public static void main(String[] args) {
        Cache<Integer, Integer> cache = CacheBuilder.newBuilder().maximumSize(100).expireAfterAccess(3, TimeUnit.SECONDS).build();
        cache.put(1, 1);
        cache.put(2, 2);
        cache.invalidateAll(Lists.newArrayList(1)); 
        System.out.println(cache.asMap()); //{2=2}
        cache.put(3, 3);
        System.out.println(cache.asMap()); //{2=2, 3=3}
        cache.invalidateAll();
        System.out.println(cache.asMap()); //{}
    }
}

监听器:可在加入缓存或者清除是做额外操作

public class GuavaCacheTest {
    public static void main(String[] args) {
        LoadingCache<Integer, Integer> cache = CacheBuilder.newBuilder().expireAfterWrite(3, TimeUnit.SECONDS).removalListener(new RemovalListener<Object, Object>() {
            @Override
            public void onRemoval(RemovalNotification<Object, Object> notification) {
                System.out.println("remove key[" + notification.getKey() + "],value[" + notification.getValue() + "],remove reason[" + notification.getCause() + "]");
            }
        }).recordStats().build(
                new CacheLoader<Integer, Integer>() {
                    @Override
                    public Integer load(Integer key) throws Exception {
                        return 2;
                    }
                }
        );
        cache.put(1, 1);
        cache.put(2, 2);
        System.out.println(cache.asMap()); //{2=2, 1=1}
        cache.invalidateAll(); //remove key[1], value[1], remove reason[EXPLICIT] 
                               //remove key[2], value[2], remove reason[EXPLICIT]
        System.out.println(cache.asMap()); //{}
        cache.put(3, 3);
        try {
            System.out.println(cache.getUnchecked(3)); //3
            Thread.sleep(4000); //remove key[3], value[3], remove reason[EXPLICIT]
            System.out.println(cache.getUnchecked(3)); //2,新生成的
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

刷新:

刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值.
而不像回收操作,读缓存的线程必须等待新值加载完成。
如果刷新过程抛出异常,缓存将保留旧值

public class GuavaCacheTest {
    public static void main(String[] args) {
        LoadingCache<Integer, Integer> cache = CacheBuilder.newBuilder().expireAfterWrite(3, TimeUnit.SECONDS).removalListener(new RemovalListener<Object, Object>() {
            @Override
            public void onRemoval(RemovalNotification<Object, Object> notification) {
                System.out.println("remove key[" + notification.getKey() + "],value[" + notification.getValue() + "],remove reason[" + notification.getCause() + "]");
            }
        }).recordStats().build(
                new CacheLoader<Integer, Integer>() {
                    @Override
                    public Integer load(Integer key) throws Exception {
                        return 2;
                    }
                }
        );
        cache.put(1, 1);
        cache.put(2, 2);
        System.out.println(cache.asMap());
        cache.refresh(1);
        System.out.println(cache.asMap());
    }
}

统计:Guava Cache具有统计功能

1)CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后,Cache.stats()方法会返回对象以提供如下统计信息:
2)hitRate():缓存命中率;
3)averageLoadPenalty():加载新值的平均时间,单位为纳秒;
4)evictionCount():缓存项被回收的总数,不包括显式清除
5)统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。

 

五、参考

http://ifeve.com/google-guava/
https://blog.csdn.net/u014082714/article/details/52080647
https://blog.csdn.net/gongxinju/article/details/53634434
https://www.yiibai.com/guava/guava_bimap.html
https://www.cnblogs.com/peida/p/Guava_Bimap.html
https://www.jianshu.com/p/5299f5a11bd5

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值