Java代码神器-Guava

一、简介

Optional类是Java8为了解决null值判断问题,借鉴google guava类库的Optional类而引入的一个同名Optional类,使用Optional类可以避免显式的null值判断(null的防御性检查),避免null导致的NPE(NullPointerException)。

二、Optional 类典型接口的使用

2.1 get()方法

public T get() {
	if (value == null) {
		throw new NoSuchElementException("No value present");
	}
	return value;
}

get()方法主要用于获取实体值,当值为null时,跑出异常,所以一般不用这个方法,建议用orElse()

2.2  isPresent()方法

public boolean isPresent() {
	return value != null;
}

其实就是判断一下这个optional对象是不是为空,觉得用处不大,因为optional本身就是避免盘空逻辑才出现的

public static String getGender(Student student)
{
	Optional<Student> stuOpt =  Optional.ofNullable(student);
  if(stuOpt.isPresent()){
		return stuOpt.get().getGender();
	}
	return "Unkown";
}

以上就是回归了以前的代码逻辑,所以个人觉得没啥用处

2.3 ifPresent()方法

public void ifPresent(Consumer<? super T> consumer) {
	if (value != null){
		consumer.accept(value);
  }
}

用来给消费者场景做NPE判断,避免空指针问题。

public static void printName(Student student){
		Optional.ofNullable(student).ifPresent(u ->  System.out.println("The student name is : " + u.getName()));
	}

2.4 filter()方法

public Optional<T> filter(Predicate<? super T> predicate) {
	Objects.requireNonNull(predicate);
	if (!isPresent()){
		return this;
  }else{
		return predicate.test(value) ? this : empty();
	}
}

对optional对象按一定条件进行过滤,所以可以理解为代替stream了吧

public static void filterAge(Student student){
	Optional.ofNullable(student).filter( u -> u.getAge() > 18).ifPresent(u ->  System.out.println("The student age is more than 18."));
}

2.5 map()方法

public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
	Objects.requireNonNull(mapper);
	if (!isPresent()){
		return empty();
  }else {
		return Optional.ofNullable(mapper.apply(value));
	}
}

map方法主要实现一些映射,可以实现一个数据的处理

public static Optional<Integer> getAge(Student student){
	return Optional.ofNullable(student).map(u -> u.getAge()); 
}

2.6 flatMap()方法

public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
	Objects.requireNonNull(mapper);
	if (!isPresent()){
		return empty();
	else {
		return Objects.requireNonNull(mapper.apply(value));
	}
}

不太懂,二维变一维

public static Optional<Integer> getAge(Student student){
	return Optional.ofNullable(student).flatMap(u -> Optional.ofNullable(u.getAge())); 
}

2.7 orElse()方法

public T orElse(T other) {
		return value != null ? value : other;
}

这个应该是最常见的操作了吧

public static String getGender(Student student){
		return Optional.ofNullable(student).map(u -> u.getGender()).orElse("Unkown");
}

为什么要使用Preconditions

那么你可能会问,为啥要用Preconditions,如果是空指针在用的时候,他自己就会抛出异常啊。

但是我们期望的是尽早抛出异常,而不是等到数据被层层传递,传递到非常深的位置,这样浪费系统资源更不利于我们开发精确定位错误。

所以推荐在方法的入口,或运算开始前,先检查数据。

int length = datas.get(5).getDescription().concat("XX").split(",").length;

像这种代码,就算抛出空指针也不知道到底是那个对象空指针了,需要一步一步调试

这就是Preconditions另一个作用:尽早发现错误,精确控制出错的位置。

1 .checkArgument(boolean) :

功能描述:检查boolean是否为真。 用作方法中检查参数

失败时抛出的异常类型: IllegalArgumentException

public static void main(String[] args) {
		Boolean value = new Boolean(Boolean.FALSE);
		Preconditions.checkArgument(value, "参数不对");
}
//Exception in thread "main" java.lang.IllegalArgumentException: 参数不对

2.checkNotNull(T):     

功能描述:检查value不为null, 直接返回value;

失败时抛出的异常类型:NullPointerException

Preconditions.checkNotNull(null, "参数不对");
//Exception in thread "main" java.lang.NullPointerException: 参数不对

3.checkState(boolean):

功能描述:检查对象的一些状态,不依赖方法参数。 例如, Iterator可以用来next是否在remove之前被调用。

失败时抛出的异常类型:IllegalStateException

4.checkElementIndex(int index, int size):

功能描述:检查index是否为在一个长度为size的list, string或array合法的范围。 index的范围区间是[0, size)(包含0不包含size)。无需直接传入list, string或array, 只需传入大小。返回index。  

失败时抛出的异常类型:IndexOutOfBoundsException

5.checkPositionIndex(int index, int size):

功能描述:检查位置index是否为在一个长度为size的list, string或array合法的范围。 index的范围区间是[0, size)(包含0不包含size)。无需直接传入list, string或array, 只需传入大小。返回index。

失败时抛出的异常类型:IndexOutOfBoundsException

6.checkPositionIndexes(int start, int end, int size):

功能描述:检查[start, end)是一个长度为size的list, string或array合法的范围子集。伴随着错误信息。

失败时抛出的异常类型:IndexOutOfBoundsException

连接器 Joiner

用分隔符将多个字符串(或数组元素)连接成一个字符串。

常用方法如下:

  1. on(String):静态工厂方法,生成一个新的 Joiner 对象,参数为连接符

  2. skipNulls():如果元素为空,则跳过

  3. useForNull(String):如果元素为空,则用这个字符串代替

  4. join(数组/链表):要连接的数组/链表

  5. appendTo(String,数组/链表):在第一个参数后面新加上 拼接后的字符串

  6. withKeyValueSeparator(String):得到 MapJoiner,连接Map的键、值

@Test
public void test(){
  List<String> list1 = Arrays.asList("aa", "bb", "cc");
  System.out.println(Joiner.on("-").join(list1));
  
  List<String> list2 = Arrays.asList("aa", "bb", "cc", null, "dd");
  System.out.println(Joiner.on("-").skipNulls().join(list2));
  System.out.println(Joiner.on("-").useForNull("nulla").join(list2));
  
  Map map = ImmutableMap.of("k1", "v1", "k2", "v2");
  System.out.println(Joiner.on("-").withKeyValueSeparator("=").join(map));
}
aa-bb-cc
aa-bb-cc-dd
aa-bb-cc-null-dd
k1=v1-k2=v2
public class JoinerTest {
    // on(String separator) 初始化Joiner连接器,separator为Joiner连接器的连接符.
    // join(Iterable<?> parts) 将parts通过连接器的连接符连接成字符串
    // skipNulls() 用于过滤集合中为null的元素,然后返回一个新的Joiner对象实例.
    // useForNull(String nullText) 连接器做join连接操作时用nullText替换null元素值
    private static final List<String> list = Lists.newArrayList("java","php","python");
    private static final List<String> listWithNull = Lists.newArrayList("java",null,"php","python");

    @Test
    public void on_join(){
        String join = Joiner.on(";").join(list);
        System.out.println(join);   //java;php;python
    }

    @Test(expected = NullPointerException.class)
    public void on_join_listWithNull(){
        String join = Joiner.on(":").join(listWithNull);    //空元素会导致NullPointerException
        System.out.println(join);
    }

    @Test
    public void skipNulls_useForNull(){
        //跳过null对象
        String join = Joiner.on(",").skipNulls().join(listWithNull);
        System.out.println(join);   //java,php,python

        //null对象用默认值代替
        String join1 = Joiner.on(",").useForNull("null").join(listWithNull);
        System.out.println(join1);  //java,null,php,python
    }

    // appendTo 将结果输出到文件中
    // appendTo 将结果追加到StringBuilder中
    @Test
    public void appendTo(){
        try (FileWriter writer = new FileWriter(new File("D:\\temp.txt"))){
            FileWriter fileWriter = Joiner.on(":").useForNull("默认值").appendTo(writer, listWithNull);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }

        StringBuilder stringBuilder = Joiner.on(",").appendTo(new StringBuilder(), list);
        System.out.println(stringBuilder.toString());   //java,php,python
    }

    // MapJoiner 的使用,将 map 转换为字符串
    // withKeyValueSeparator 初始化一个Map连接器,连接器连接Map对象时,keyValueSeparator为key和value之间的分隔符
    @Test
    public void mapJoiner(){
        Map<String, String> map = ImmutableMap.of("hello", "java", "hi", "python");
        String join = Joiner.on("; ").withKeyValueSeparator("=").join(map);
        System.out.println(join);   //hello=java; hi=python
    }

    // java8 测试joining
    @Test
    public void joining(){
        String joining = listWithNull.stream().filter(s -> s != null && !"".equals(s)).collect(Collectors.joining(":"));
        System.out.println(joining);    //java:php:python
    }

}

Splitter 能将一个字符串按照分隔符生成字符串集合,是 Joiner的反向操作。

常用方法如下:

  1. on(String):静态工厂方法,生成一个新的 Splitter 对象,参数为连接符

  2. trimResults():结果去除子串中的空格

  3. omitEmptyStrings():去除null的子串

  4. split(String):拆分字符串

  5. withKeyValueSeparator(String):得到 MapSplitter,拆分成Map的键、值。注意,这个对被拆分字符串格式有严格要求,否则会抛出异常

    public static void main(String[] args) {
//        Preconditions.checkNotNull(null, "参数不对");
        System.out.printf("sss");
        String string = " ,a,b,";
        System.out.println(Splitter.on(",").split(string));
        System.out.println(Splitter.on(",").trimResults().split(string));
        System.out.println(Splitter.on(",").omitEmptyStrings().split(string));
        System.out.println(Splitter.on(",").trimResults().omitEmptyStrings().split(string));

        // 根据长度拆分
        string = "12345678";
        System.out.println(Splitter.fixedLength(2).split(string));

        // 拆分成Map
        System.out.println(Splitter.on("#").withKeyValueSeparator(":").split("1:2#3:4"));
    }
[ , a, b, ]
[, a, b, ]
[ , a, b]
[a, b]
[12, 34, 56, 78]
{1=2, 3=4}

Strings 类主要提供了对字符串的一些操作。主要方法如下:

  1. nullToEmpty(String string) :null字符串转空字符串

  2. emptyToNull(String string):空字符串转null字符串

  3. isNullOrEmpty(String string):判断字符串为null或空字符串

  4. padStart(String string, int minLength, char padChar):如果string的长度小于minLeng,在string前添加padChar,直到字符串长度为minLeng。

  5. String padEnd(String string, int minLength, char padChar):和padStart类似,不过是在尾部添加新字符串

  6. commonPrefix(CharSequence a, CharSequence b):返回共同的前缀

  7. commonSuffix(CharSequence a, CharSequence b):返回共同的后缀

public void test(){
  String aa = "12345";
  // A12345
  System.out.println(Strings.padStart(aa, 6, 'A'));
  // 12345
  System.out.println(Strings.padStart(aa, 5, 'A'));
}
public void test2(){
  String aa = "abc123def";
  String bb = "abc789def";
  System.out.println(Strings.commonPrefix(aa, bb));
  System.out.println(Strings.commonSuffix(aa, bb));
}
abc
def

集合工具(Collections)

1. 不可变集合

不可变集合,即创建后就只可读,不可修改的集合。为什么要使用不可变集合呢?主要有如下优点:

  • 当对象被不可信的库调用时,不可变形式是安全的;

  • 不可变对象被多个线程调用时,不存在竞态条件问题

  • 不可变集合不需要考虑变化,因此可以节省时间和空间。所有不可变的集合都比它们的可变形式有更好的内存利用率(分析和测试细节);

  • 不可变对象因为有固定不变,可以作为常量来安全使用。

JDK也提供了Collections.unmodifiableXXX方法把集合包装为不可变形式,但我们认为不够好:

  • 笨重而且累赘:不能舒适地用在所有想做防御性拷贝的场景;

  • 不安全:要保证没人通过原集合的引用进行修改,返回的集合才是事实上不可变的;

public void test3(){
  List<Integer> list = Lists.newArrayList(1,2,3);
  List<Integer> list1 = Collections.unmodifiableList(list);
  System.out.println(list);
  // [1, 2, 3]
  System.out.println(list1);
  // [1, 2, 3]
  // list修改,list1也会被修改
  list.add(4);
  System.out.println(list1);
  // [1, 2, 3, 4]
}
  • 低效:包装过的集合仍然保有可变集合的开销,比如并发修改的检查、散列表的额外空间,等等。

注意:所有Guava不可变集合的实现都不接受null值。因为谷歌内部调查代码发现,只有5%的情况需要在集合中允许null元素。如果要存储null值,请使用JDK的Collections.unmodifiable方法

创建不可变集合的几个方法:

  • copyOf 方法,如ImmutableSet.copyOf(set);

  • of方法,如ImmutableSet.of(“a”, “b”, “c”)或 ImmutableMap.of(“a”, 1, “b”, 2);

  • Builder工具,如

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

copyOf 是很智能和高效的,在特定会避免线性拷贝。下期有机会来分析下它的实现原理。

关联可变集合和不可变集合

可变集合接口

属于JDK还是Guava

不可变版本

Collection

JDK

ImmutableCollection

List

JDK

ImmutableList

Set

JDK

ImmutableSet

SortedSet/NavigableSet

JDK

ImmutableSortedSet

Map

JDK

ImmutableMap

SortedMap

JDK

ImmutableSortedMap

Multiset

Guava

ImmutableMultiset

SortedMultiset

Guava

ImmutableSortedMultiset

Multimap

Guava

ImmutableMultimap

ListMultimap

Guava

ImmutableListMultimap

SetMultimap

Guava

ImmutableSetMultimap

BiMap

Guava

ImmutableBiMap

ClassToInstanceMap

Guava

ImmutableClassToInstanceMap

Table

Guava

ImmutableTable

Multiset继承自JDK中的Collection接口,而不是Set接口,所以可以包含重复元素。可以从以下角度理解:

  • 没有元素顺序限制的ArrayList

  • Map<E, Integer>,键为元素,值为计数

Multiset提供像无序的ArrayList的基本操作:

  • add(E)添加单个给定元素

  • iterator()返回一个迭代器,包含Multiset的所有元素(包括重复的元素)

  • size()返回所有元素的总个数(包括重复的元素)

当把Multiset看作Map<E, Integer>时,它也提供了Map的查询操作:

  • count(Object)返回给定元素的计数。

  • entrySet()返回Set<Multiset.Entry>,和Map的entrySet类似。

  • elementSet()返回所有不重复元素的Set,和Map的keySet()类似。

常用方法如下:

方法

描述

count(E)

给定元素在Multiset中的计数

elementSet()

Multiset中不重复元素的集合,类型为Set

entrySet()

和Map的entrySet类似,返回Set<Multiset.Entry>,
其中包含的Entry支持getElement()和getCount()方法

add(E, int)

增加给定元素在Multiset中的计数

remove(E, int)

减少给定元素在Multiset中的计数

setCount(E, int)

设置给定元素在Multiset中的计数,不可以为负数

size()

返回集合元素的总个数(包括重复的元素)

统计一个词在文档中出现了多少次。

1.传统的做法是这样的

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);
    }
}

2.使用Multiset操作:

Multiset<String> multiset = HashMultiset.create();
for (String word : words) {
    multiset.add(word);
}
int count = multiset.count("today");

Multimap

通俗来讲,Multimap 是一键对多值HashMap,类似于 Map<K, List> 的数据结构。

@Test
public void test2() {
  Multimap<String, String> multimap = ArrayListMultimap.create();
  multimap.put("name", "Jack");
  multimap.put("name", "Jack");
  multimap.put("name", "Tom");
  multimap.put("age", "10");
  multimap.put("age", "12");
  System.out.println(multimap);
  System.out.println(multimap.get("name").size());
}

输出:

{name=[Jack, Jack, Tom], age=[10, 12]}
3

常用操作如下:

方法签名

描述

等价于

put(K, V)

添加键到单个值的映射

multimap.get(key).add(value)

putAll(K, Iterable)

依次添加键到多个值的映射

Iterables.addAll(multimap.get(key), values)

remove(K, V)

移除键到值的映射;如果有这样的键值并成功移除,返回true。

multimap.get(key).remove(value)

removeAll(K)

清除键对应的所有值,返回的集合包含所有之前映射到K的值,但修改这个集合就不会影响Multimap了。

multimap.get(key).clear()

replaceValues(K, Iterable)

清除键对应的所有值,并重新把key关联到Iterable中的每个元素。返回的集合包含所有之前映射到K的值。

multimap.get(key).clear(); Iterables.addAll(multimap.get(key), values)

主要操作

  • asMap:为Multimap<K, V>提供Map<K,Collection>形式的视图

  • entries:返回所有”键-单个值映射”,包括重复键。Collection<Map.Entry<K, V>>类型

  • keySet:返回所有不同的键,Set类型

  • keys:用Multiset表示Multimap中的所有键,每个键重复出现的次数等于它映射的值的个数。可以从这个Multiset中移除元素,但不能做添加操作;移除操作会反映到底层的Multimap

  • values:用一个”扁平”的Collection包含Multimap中的所有值,包括重复键

Table

看到table的使用时候真的是眼前一亮,之前的代码中写过很多的Map<String,Map<String,String>> 这种格式的代码,这种阅读起来非常的不友好,甚至都不知道map中的key到底是什么还要联系上下文联想才可以,而table类型的出现彻底解决掉了这个麻烦。

Table类似多个索引的表,类似 Map<R, Map<C, V>> 的数据结构。它有两个支持所有类型的键:””和””,可以通过以下方法获取多个视图:

  • rowMap():用Map<R, Map<C, V>>表现Table<R, C, V>。同样的, rowKeySet()返回”行”的集合Set。

  • row(r):用Map<C, V>返回给定”行”的所有列,对这个map进行的写操作也将写入Table中。

  • cellSet():用元素类型为Table.Cell的Set表现Table<R, C, V>。Cell类似于Map.Entry,但它是用行和列两个键区分的。

Table支持 row、column、value  我们把上面定义的map结构想象成一张数据表就可以了:

Table<R,C,V> == Map<R,Map<C,V>>

S.N.

方法 & 描述

1

Set<Table.Cell<R,C,V>> cellSet()
返回集合中的所有行键/列键/值三元组。

2

void clear()
从表中删除所有映射。

3

Map<R,V> column(C columnKey)
返回在给定列键的所有映射的视图。

4

Set<C> columnKeySet()
返回一组具有表中的一个或多个值的列键。

5

Map<C,Map<R,V>> columnMap()
返回关联的每一列键与行键对应的映射值的视图。

6

boolean contains(Object rowKey, Object columnKey)
返回true,如果表中包含与指定的行和列键的映射。

7

boolean containsColumn(Object columnKey)
返回true,如果表中包含与指定列的映射。

8

boolean containsRow(Object rowKey)
返回true,如果表中包含与指定的行键的映射关系。

9

boolean containsValue(Object value)
返回true,如果表中包含具有指定值的映射。

10

boolean equals(Object obj)
比较指定对象与此表是否相等。

11

V get(Object rowKey, Object columnKey)
返回对应于给定的行和列键,如果没有这样的映射存在值,返回null。

12

int hashCode()
返回此表中的哈希码。

13

boolean isEmpty()
返回true,如果表中没有映射。

14

V put(R rowKey, C columnKey, V value)
关联指定值与指定键。

15

void putAll(Table<? extends R,? extends C,? extends V> table)
复制从指定的表中的所有映射到这个表。

16

V remove(Object rowKey, Object columnKey)
如果有的话,使用给定键相关联删除的映射。

17

Map<C,V> row(R rowKey)
返回包含给定行键的所有映射的视图。

18

Set<R> rowKeySet()
返回一组行键具有在表中的一个或多个值。

19

Map<R,Map<C,V>> rowMap()
返回关联的每一行按键与键列对应的映射值的视图。

20

int size()
返回行键/列键/表中的值映射关系的数量。

21

Collection<V> values()
返回所有值,其中可能包含重复的集合。

/*
         *  Company: IBM, Microsoft, TCS
         *  IBM         -> {101:Mahesh, 102:Ramesh, 103:Suresh}
         *  Microsoft     -> {101:Sohan, 102:Mohan, 103:Rohan }
         *  TCS         -> {101:Ram, 102: Shyam, 103: Sunil }
         *
         * */
        //create a table
        Table<String, String, String> employeeTable = HashBasedTable.create();

        //initialize the table with employee details
        employeeTable.put("IBM", "101","Mahesh");
        employeeTable.put("IBM", "102","Ramesh");
        employeeTable.put("IBM", "103","Suresh");

        employeeTable.put("Microsoft", "111","Sohan");
        employeeTable.put("Microsoft", "112","Mohan");
        employeeTable.put("Microsoft", "113","Rohan");

        employeeTable.put("TCS", "121","Ram");
        employeeTable.put("TCS", "102","Shyam");
        employeeTable.put("TCS", "123","Sunil");

        //所有行数据
        System.out.println(employeeTable.cellSet());
        //所有公司
        System.out.println(employeeTable.rowKeySet());
        //所有员工编号
        System.out.println(employeeTable.columnKeySet());
        //所有员工名称
        System.out.println(employeeTable.values());
        //公司中的所有员工和员工编号
        System.out.println(employeeTable.rowMap());
        //员工编号对应的公司和员工名称
        System.out.println(employeeTable.columnMap());
        //row+column对应的value
        System.out.println(employeeTable.get("IBM","101"));
        //IBM公司中所有信息
        Map<String,String> ibmEmployees =  employeeTable.row("IBM");

        System.out.println("List of IBM Employees");
        for(Map.Entry<String, String> entry : ibmEmployees.entrySet()){
            System.out.println("Emp Id: " + entry.getKey() + ", Name: " + entry.getValue());
        }

        //table中所有的不重复的key
        Set<String> employers = employeeTable.rowKeySet();
        System.out.print("Employers: ");
        for(String employer: employers){
            System.out.print(employer + " ");
        }
        System.out.println();

        //得到员工编号为102的所有公司和姓名
        Map<String,String> EmployerMap =  employeeTable.column("102");
        for(Map.Entry<String, String> entry : EmployerMap.entrySet()){
            System.out.println("Employer: " + entry.getKey() + ", Name: " + entry.getValue());
        }


/*
[(IBM,101)=Mahesh, (IBM,102)=Ramesh, (IBM,103)=Suresh, (Microsoft,111)=Sohan, (Microsoft,112)=Mohan, (Microsoft,113)=Rohan, (TCS,121)=Ram, (TCS,102)=Shyam, (TCS,123)=Sunil]
[IBM, Microsoft, TCS]
[101, 102, 103, 111, 112, 113, 121, 123]
[Mahesh, Ramesh, Suresh, Sohan, Mohan, Rohan, Ram, Shyam, Sunil]
{IBM={101=Mahesh, 102=Ramesh, 103=Suresh}, Microsoft={111=Sohan, 112=Mohan, 113=Rohan}, TCS={121=Ram, 102=Shyam, 123=Sunil}}
{101={IBM=Mahesh}, 102={IBM=Ramesh, TCS=Shyam}, 103={IBM=Suresh}, 111={Microsoft=Sohan}, 112={Microsoft=Mohan}, 113={Microsoft=Rohan}, 121={TCS=Ram}, 123={TCS=Sunil}}
Mahesh
List of IBM Employees
Emp Id: 101, Name: Mahesh
Emp Id: 102, Name: Ramesh
Emp Id: 103, Name: Suresh
Employers: IBM Microsoft TCS
Employer: IBM, Name: Ramesh
Employer: TCS, Name: Shyam
*/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

七小琦

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

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

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

打赏作者

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

抵扣说明:

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

余额充值