Java实操问题避坑

如何从根源上避免空指针?

可能产生空指针的场景

public class WhatIsNpe {
    public static class User {
        private String name;

        private String[] address;

        public void print() {
            System.out.println("This is User Class.");
        }

        public String readBook() {
            System.out.println("User read book.");
            return null;
        }
    }

    /**
     * <p>
     * 自定义一个运行时异常
     * </p>
     */
    public static class CustomException extends RuntimeException {

    }

    public static void main(String[] args) {
        // 场景一: 调用了空对象的实例方法
//        User user = null;
//        user.print();

        // 场景二: 访问了空对象的属性
//        User user = null;
//        System.out.println(user.name);

        // 场景三: 当数组是一个空对象时,取它的长度
//        User user = new User();
//        System.out.println(user.address);
        // 产生空指针异常
//        System.out.println(user.address.length);

        // 场景四: null 当做 Throwable 的值,即抛出异常时不能抛出一个内存地址为空的异常,需要对异常进行初始化
//        CustomException exception = null;
//        throw exception;

        // 场景五: 方法的返回值是 null,调用方直接去使用
        User user = new User();
        System.out.println(user.readBook());
        // 产生空指针异常  
        System.out.println(user.readBook().contains("MySQL"));
    }
}

解决方式
在这里插入图片描述

赋值时自动拆箱出现空指针

基本类型与包装类型
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
自动拆箱引发的空指针问题
场景复现

@SuppressWarnings("all")
public class UnboxingNpe {
    public static void main(String[] args) {
        // 场景一: 变量赋值时自动拆箱出现的空指针
        // 为什么自动拆箱会引发空指针?
        // 使用该场景分析为何自动拆箱时会出现空指针:
        // 1. javac UnboxingNpe.java
        // 2. javap -c UnboxingNpe.class
        /*
        Compiled from "UnboxingNpe.java"
        public class cn.xilikeli.java.escape.UnboxingNpe {
          public cn.xilikeli.java.escape.UnboxingNpe();
            Code:
               0: aload_0
               1: invokespecial #1                  // Method java/lang/Object."<init>":()V
               4: return

          public static void main(java.lang.String[]);
            Code:
               0: aconst_null
               1: astore_1
               2: aload_1
               3: invokevirtual #2                  // Method java/lang/Long.longValue:()J
               6: lstore_2
               7: return
        }
         */
        // 通过上面的反编译结果可以知道在拆箱时会调用 Long.longValue 方法,而由于 count 为 null,所以就会抛出空指针异常
        Long count = null;
        long countA = count;

        // 场景二: 方法传参时自动拆箱出现的空指针
//        Integer left = null;
//        Integer right = null;
//        System.out.println(add(left, right));

        // 场景三: 用于大小比较的场景
//        Long x = 10L;
//        Long y = null;
//        System.out.println(compare(x, y));
    }

    private static int add(int x, int y) {
        return x + y;
    }

    private static boolean compare(long x, long y) {
        return x >= y;
    }
}

规避自动拆箱引发空指针的建议

  1. 基本类型优于包装器类型,优先考虑使用基本类型。
  2. 对于不确定的包装器类型,一定要校验是否是 NULL。
  3. 对于值为 NULL 的包装器类型,赋值为 0。

字符串、数组、集合在使用时出现空指针怎么办?

场景复现
在这里插入图片描述

@SuppressWarnings("all")
public class BasicUsageNpe {
    private static boolean stringEquals(String x, String y) {
        return x.equals(y);
    }

    public static class User {
        private String name;
    }

    public static void main(String[] args) {
        // 场景一: 字符串使用 equals 方法时可能会报空指针错误
        // false
//        System.out.println(stringEquals("xyz", null));
        // 空指针异常, 原因: null.equals("xyz")
//        System.out.println(stringEquals(null, "xyz"));

        // 场景二: 对象数组虽然 new 出来了, 但是如果元素没有初始化, 一样会报空指针错误
//        User[] users = new User[10];
//        for (int i = 0; i != 10; ++i) {
        // 需要对元素初始化才不会报空指针, users[i] = new User();
//            users[i].name = "abc-" + i;
//        }

        // 场景三: List 对象 add null 不报错, 但是 addAll 不能添加 null, 否则会报空指针错误
        List<User> userList = new ArrayList<>();
        User user = null;
        // 不会报错
        userList.add(user);
        List<User> userListA = null;
        // 空指针
        userList.addAll(userListA);
        /*
            addAll 源码:
            public boolean addAll(Collection<? extends E> c) {
                // 由于传进来的 userListA 是 null, 这里会 null.toArray(); 引发空指针异常
                Object[] a = c.toArray();
                int numNew = a.length;
                ensureCapacityInternal(size + numNew);  // Increments modCount
                System.arraycopy(a, 0, elementData, size, numNew);
                size += numNew;
                return numNew != 0;
            }
         */
    }
}

使用 Optional 规避空指针时的注意点

什么是Optional
在这里插入图片描述
常用方法

/**
 * Returns an {@code Optional} with the specified present non-null value.
 * 
 * 将指定的值用 Optional 封装之后去返回,如果这个值为 null,将抛出空指针异常
 *
 * @param <T> the class of the value
 * @param value the value to be present, which must be non-null
 * @return an {@code Optional} with the value present
 * @throws NullPointerException if value is null
 */
public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

/**
 * Returns an empty {@code Optional} instance.  No value is present for this
 * Optional.
 *
 * 用于创建一个空的 Optional 实例
 *
 * @apiNote Though it may be tempting to do so, avoid testing if an object
 * is empty by comparing with {@code ==} against instances returned by
 * {@code Option.empty()}. There is no guarantee that it is a singleton.
 * Instead, use {@link #isPresent()}.
 *
 * @param <T> Type of the non-existent value
 * @return an empty {@code Optional}
 */
public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

/**
 * Returns an empty {@code Optional} instance.  No value is present for this
 * Optional.
 *
 * 用于创建一个空的 Optional 实例
 *
 * @apiNote Though it may be tempting to do so, avoid testing if an object
 * is empty by comparing with {@code ==} against instances returned by
 * {@code Option.empty()}. There is no guarantee that it is a singleton.
 * Instead, use {@link #isPresent()}.
 *
 * @param <T> Type of the non-existent value
 * @return an empty {@code Optional}
 */
public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}


/**
 * Returns an {@code Optional} describing the specified value, if non-null,
 * otherwise returns an empty {@code Optional}.
 *
 * 与 of 方法类似,将指定的值用 Optional 封装之后去返回,但是并不会因为传递进来的值是 null 抛出空指针异常。
 * 如果传递进来的值是 null,返回一个空的 Optional 实例
 *
 * @param <T> the class of the value
 * @param value the possibly-null value to describe
 * @return an {@code Optional} with a present value if the specified value
 * is non-null, otherwise an empty {@code Optional}
 */
public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}


/**
 * If a value is present in this {@code Optional}, returns the value,
 * otherwise throws {@code NoSuchElementException}.
 *
 * 如果 Optional 的 value 这个值存在就返回这个 value, 否则抛出 NoSuchElementException 异常
 *
 * @return the non-null value held by this {@code Optional}
 * @throws NoSuchElementException if there is no value present
 *
 * @see Optional#isPresent()
 */
public T get() {
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }
    return value;
}


/**
 * Return {@code true} if there is a value present, otherwise {@code false}.
 *
 * 判断 Optional 的 value 是否存在,如果存在返回 true,如果不存在返回 false
 *
 * @return {@code true} if there is a value present, otherwise {@code false}
 */
public boolean isPresent() {
    return value != null;
}

Optional 的日常使用复现

@SuppressWarnings("all")
public class OptionalUsage {
    public static class User {
        private String name;

        public String getName() {
            return name;
        }
    }

    private static void isUserEqualNull() {
        // 不使用 Optional 的判空
        User user = null;
        if (user != null) {
            System.out.println("User is not null.");
        } else {
            System.out.println("User is null.");
        }

        // 获取 Optional 实例
        Optional<User> optional = Optional.empty();
        // 判断这个实例是否为空,为空则返回 false,不为空返回 true
        if (optional.isPresent()) {
            System.out.println("User is not null.");
        } else {
            System.out.println("User is null.");
        }

        // 以上两段代码几乎一样,实质上也是没有区别的,isPresent 的源码也是和第一段的判空是一样的,如果这样使用 Optional,Optional 也是没有价值的
        // 应该认识到 orElse、orElseGet、map 等方法的妙用
    }

    private static User anoymos() {
        return new User();
    }

    public static void main(String[] args) {
        // 没有意义的使用方法
        isUserEqualNull();

        // 有意义的使用方法
        User user = null;
        // 获取 Optional 实例
        Optional<User> optionalUser = Optional.ofNullable(user);
        // orElse 的使用: 存在即返回, 空则提供默认值
        User userA = optionalUser.orElse(new User());
        // orElseGet 的使用: 存在即返回, 空则由函数去产生值, 相比 orElse 复用性更强, 因为通过函数产生值更加灵活
        User userB = optionalUser.orElseGet(() -> anoymos());
        // orElseThrow 的使用: 存在即返回, 否则会抛出异常
        optionalUser.orElseThrow(() -> new RuntimeException("抛出自定义异常."));
        // ifPresent 的使用: 存在才去做相应的处理, 否则什么都不做
        optionalUser.ifPresent(u -> System.out.println(u.getName()));
        // map 的使用: 可以对 Optional 中的对象执行某种操作, 且会返回一个 Optional 对象
        // 获取用户名称, 获取之后判断是否存在, 不存在返回默认值
        String name = optionalUser.map(u -> u.getName()).orElse("默认名称");
        System.out.println("name: " + name);
        // map 是可以无限级联操作的, 需要注意每一个 map 操作返回的 Optional 包含的是什么类 型
        // 获取用户名称, 获取之后获取名称的长度, 如果为空返回 0
        Integer length = optionalUser.map(u -> u.getName())
                .map(n -> n.length())
                .orElse(0);
        System.out.println("length: " + length);
    }
}

日常的使用方法有问题吗?
一些方法是无意义的, 应该多使用有意义的方法, 其中一些无意义的方法应该看作是 Optional 的私有方法。
在这里插入图片描述

try catch 了却没有真正的解决好异常?

什么是异常?
在这里插入图片描述
Java 异常处理类
在这里插入图片描述
● Error
Error 类对象由 Java 虚拟机生成并且抛出,大多数错误与代码编写者所执行的操作是没有关系的,比如说 Java 虚拟机运行时出错会抛出 Error 异常。
● RuntimeException
运行时异常,这一类的异常为程序员所编写的程序定义了很多异常,比如空指针异常、数组下标越界异常等等。这些异常是非检查异常,程序中可以选择捕获处理,也可以选择不去处理。
● CheckedException
检查性异常,这种异常的发生是可以预测的,并且一旦发生这种异常就必须采取某种措施去处理,这一类型的异常可以选择捕获去处理或抛出由上层调用者继续处理,如果不去处理,代码无法编译通过。

案例

@SuppressWarnings("all")
public class ExceptionProcess {
    public static class User {
    }

    /**
     * <p>
     * Java 异常本质: 抛出异常
     * </p>
     */
    private void throwException() {
        User user = null;

        // 对 user 的一些相关的处理。。。

        // 处理完之后
        if (null == user) {
            // 当想要去执行某种操作时,而当前的情况不允许向下执行,可以抛出异常让调用方去处理这个异常
            throw new NullPointerException();
        }
    }

    /**
     * <p>
     * 不能够捕获异常的例子
     * <p>
     * 没有指定捕获空指针异常, 这个异常会继续往上抛, 如果还没有相应的捕获器会一直抛到 main 方法,
     * 如果还没有相应的处理会导致程序的终止
     * </P>
     */
    private void canNotCatchNpeException() {
        try {
            throwException();
        } catch (ClassCastException cce) {
            System.out.println("捕获类型转换异常: ");
            System.out.println(cce.getMessage());
            System.out.println(cce.getClass().getName());
        }
    }

    /**
     * <p>
     * 可以捕获异常的例子
     * </P>
     */
    private void canCatchNpeException() {
        try {
            throwException();
        } catch (ClassCastException cce) {
            System.out.println("捕获类型转换异常: ");
            System.out.println(cce.getMessage());
            System.out.println(cce.getClass().getName());
        } catch (NullPointerException npe) {
            System.out.println("捕获空指针异常: ");
            System.out.println(npe.getMessage());
            System.out.println(npe.getClass().getName());
        }
    }

    public static void main(String[] args) {
        ExceptionProcess process = new ExceptionProcess();
        process.canCatchNpeException();
        System.out.println("成功捕获空指针异常。。。");
        process.canNotCatchNpeException();
        System.out.println("没有处理抛出的异常, 该句不会打印输出。。。");
    }
}

在这里插入图片描述

解决使用 try finally 的资源泄露隐患

资源与资源泄露
在这里插入图片描述
场景复现
传统的 try finally 方式关闭资源

import java.io.*;

/**
 * <p>
 * 解决使用 try finally 的资源泄露隐患
 * </p>
 */
public class Main {
    /**
     * 传统的 try finally 方式去释放资源
     */
    private String traditionalTryCatch() throws IOException {
        // 1. 单一资源的关闭
//        String line = null;
//        BufferedReader br = new BufferedReader(new FileReader("/Users/destroyer/Documents/文档/hello.txt"));
//        try {
            // 读取资源的一行
            // 需要注意, try 中有发生异常的可能性
//            return br.readLine();
//            line = br.readLine();
//        } finally {
            // 操作完成, 释放资源
            // 这种方式可以保证正确关闭了资源, 且是一种比较好的方式
            // 即使程序抛出异常或者直接返回的情况下也可以正确的完成关闭资源的工作
            // 所以这种方式对单一资源的关闭是可行的
            // 需要注意, finally 中有发生异常的可能性, 如果这里发生异常且 try 中也发生异常这里的异常会覆盖掉 try 中的异常, 使调试会变得复杂
//            br.close();
//        }
//        return line;

        // 2. 多个资源的关闭
        // 代码显得冗长, 且容易出错
        File file;
        // 第一个资源
        InputStream in = new FileInputStream("/Users/destroyer/Documents/文档/hello.txt");
        try {
            // 第二个资源
            OutputStream out = new FileOutputStream("/Users/destroyer/Documents/文档/hello2.txt");
            try {
                byte[] buf = new byte[100];
                int n;
                while ((n = in.read(buf)) >= 0) {
                    out.write(buf, 0, n);
                }
            } finally {
                out.close();
            }
        } finally {
            in.close();
        }

        return null;
    }

    public static void main(String[] args) {
        Main main = new Main();
        try {
            main.traditionalTryCatch();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

try finally 的问题及改进方案
在这里插入图片描述
改进方案代码:

import java.io.*;

/**
 * <p>
 * 解决使用 try finally 的资源泄露隐患
 * </p>
 */
public class Main {
    /**
     * 传统的 try finally 方式去释放资源
     */
    private String traditionalTryCatch() throws IOException {
        // 1. 单一资源的关闭
//        String line = null;
//        BufferedReader br = new BufferedReader(new FileReader("/Users/destroyer/Documents/文档/hello.txt"));
//        try {
            // 读取资源的一行
            // 需要注意, try 中有发生异常的可能性
//            return br.readLine();
//            line = br.readLine();
//        } finally {
            // 操作完成, 释放资源
            // 这种方式可以保证正确关闭了资源, 且是一种比较好的方式
            // 即使程序抛出异常或者直接返回的情况下也可以正确的完成关闭资源的工作
            // 所以这种方式对单一资源的关闭是可行的
            // 需要注意, finally 中有发生异常的可能性, 如果这里发生异常且 try 中也发生异常这里的异常会覆盖掉 try 中的异常, 使调试会变得复杂
//            br.close();
//        }
//        return line;

        // 2. 多个资源的关闭
        // 代码显得冗长, 且容易出错, 可能会出现忘记关闭资源的情况
        File file;
        // 第一个资源
        InputStream in = new FileInputStream("/Users/destroyer/Documents/文档/hello.txt");
        try {
            // 第二个资源
            OutputStream out = new FileOutputStream("/Users/destroyer/Documents/文档/hello2.txt");
            try {
                byte[] buf = new byte[100];
                int n;
                while ((n = in.read(buf)) >= 0) {
                    out.write(buf, 0, n);
                }
            } finally {
                out.close();
            }
        } finally {
            in.close();
        }

        return null;
    }

    /**
     * 改进方案: Java7 引入的 try with resources 实现自动的资源关闭
     * 使用简单, 出错的可能性非常低
     * BufferedReader 这个接口从 Java7 开始实现了 AutoCloseable 接口, 无论 try 语句是正常结束还是异常结束, 资源都能够被正确的关闭
     */
    private String newTryWithResources() throws IOException {
        // 1. 单个资源的使用与关闭
        // 和 for 循环的语法比较类似, 把资源放在小括号中, 对资源的操作放在花括号中, 操作结束退出花括号之后 try with resources  就能够对小括号中的资源实现资源的关闭
//        try (BufferedReader br = new BufferedReader(new FileReader("/Users/destroyer/Documents/文档/hello.txt"))) {
//            return br.readLine();
//        }

        // 2. 多个资源的使用与关闭
        try (FileInputStream in = new FileInputStream("/Users/destroyer/Documents/文档/hello.txt");
             FileOutputStream out = new FileOutputStream("/Users/destroyer/Documents/文档/hello2.txt")
        ) {
            byte[] buffer = new byte[100];
            int n = 0;
            while ((n = in.read(buffer)) != -1 ) {
                out.write(buffer, 0, n);
            }
        }
        return null;
    }

    public static void main(String[] args) {
        Main main = new Main();
        try {
//            String s1 = main.traditionalTryCatch();
//            System.out.println(s1);
            String s2 = main.newTryWithResources();
            System.out.println(s2);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

finally 中的异常抑制 try 中的异常的场景复现
自定义异常类:

public class MyException extends Exception {
    private static final long serialVersionUID = 5165206067888442631L;

    public MyException() {
        super();
    }

    public MyException(String message) {
        super(message);
    }
}

AutoClose 类:

/**
 * <p>
 * 该类实现了 AutoCloseable 接口, 对于实现这个接口的类, 在使用完之后就可以被 try with resources 主动的关闭掉
 * </p>
 */
public class AutoClose implements AutoCloseable {
    @Override
    public void close() throws Exception {
        // 关闭资源时会调用这个 close 方法
        System.out.println(">>> close()");
        throw new RuntimeException("Exception in close()");
    }

    public void work() throws MyException {
        System.out.println(">>> work()");
        throw new MyException("Exception in work()");
    }
}

Main 方法:

public static void main(String[] args) throws Exception {
        // close 中异常会覆盖 work 中异常
//        AutoClose autoClose = new AutoClose();
//        try {
//            autoClose.work();
//        } finally {
//            autoClose.close();
//        }

        // try with resources 不会有上面的问题, 两个异常的信息都会显示
        try (AutoClose autoClose = new AutoClose()) {
            autoClose.work();
        }
    }

常见异常: 并发修改、类型转换、枚举查找

常见案例
在这里插入图片描述
代码复现

/**
 * <p>
 * 员工类型枚举类
 * </p>
 */
public enum StaffTypeEnum {
    RD,
    QA,
    PM,
    OP;
}

场景复现代码:

import com.google.common.base.Enums;

import java.util.*;

/**
 * <p>
 * 编码中的常见异常: 并发修改、类型转换、枚举查找
 * </p>
 */
@SuppressWarnings("all")
public class GeneralException {
    public static class User {
        private String name;

        public User() {
        }

        public User(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }

    public static class Manager extends User {
    }

    public static class Worker extends User {
    }

    /**
     * 并发修改异常
     * 可迭代对象在遍历的同时做修改操作
     */
    private static void concurrentModificationException(ArrayList<User> userList) {
        // 异常产生原因: 快速失败机制
        // 迭代器工作在一个独立的线程中, 并且拥有一个互斥锁
        // 迭代器被创建之后会建立一个指向原来对象的一个单链的索引表, 当原来对象数量发生变化的时候, 这个索引表的内容不会同步改变
        // 所以, 当索引指针往后去移动的时候, 就找不到要迭代的对象, 所以按照快速失败机制的原则, 迭代器就会马上抛出 ConcurrentModificationException
        // 所以, 在遍历过程中想要删除元素的正确方法应该是使用迭代器而不是使用循环这个迭代过程
//        for (User user : userList) {
//            if ("sky".equals(user.getName())) {
//                userList.remove(user);
//            }
//        }

        // 直接使用迭代器则没有问题
        Iterator<User> iterator = userList.iterator();
        while (iterator.hasNext()) {
            // next 必须在 remove 之前
            // 如果迭代过程中先调用 remove 再调用 next 同样会发生并发修改异常, 原因和之前的循环也是一样
            User user = iterator.next();
            if ("sky".equals(user.getName())) {
                iterator.remove();
            }
        }

        // 虽然使用迭代器可以在遍历过程中删除元素, 但是最好不要这么做,
        // 更好的实现用 Java8 中的流, 就是 Stream 去做 filter 操作, 实现过滤, 而不是在遍历中实现删除
    }

    private static final Map<String, StaffTypeEnum> typeIndex = new HashMap<>(StaffTypeEnum.values().length);

    // typeIndex 初始化填充
    static {
        // 只需要遍历一次
        for (StaffTypeEnum value : StaffTypeEnum.values()) {
            typeIndex.put(value.name(), value);
        }
    }

    private static StaffTypeEnum enumFind(String type) {
        // 可能会出现异常
//        return StaffTypeEnum.valueOf(type);

        // 解决方案:
        // 1. 最普通、最简单的实现
//        try {
//            return StaffTypeEnum.valueOf(type);
//        } catch (IllegalArgumentException e) {
//            return null;
//        }

        // 2. 改进的实现, 但是效率不高, 如果枚举值太多时, 每次都要进行一次全量的循环
//        for (StaffTypeEnum value : StaffTypeEnum.values()) {
//            if (value.name().equals(type)) {
//                return value;
//            }
//        }
//        return null;

        // 3. 使用静态的 Map 索引, 只有一次循环枚举的过程, 缺陷: map 获取不到对应的值时会返回一个 null
//        return typeIndex.get(type);

        // 4. 使用 Google Guava 的 Enums, 需要相关的依赖
        // 如果获取到对应的值返回对应的值, 否则返回 null
        return Enums.getIfPresent(StaffTypeEnum.class, type).orNull();
    }

    public static void main(String[] args) {
        // 1. 并发修改异常
//        ArrayList<User> userList = new ArrayList<>(
//                Arrays.asList(new User("wade"), new User("sky"))
//        );
        // java.util.ConcurrentModificationException
//        concurrentModificationException(userList);

        // 2. 类型转换异常: 类型转换不符合 Java 的继承关系, 则会报类型转换异常
        User user1 = new Manager();
        User user2 = new Worker();

        // 正确的转型
//        Manager m1 = (Manager) user1;
        // 错误的转型, 类型转换异常 java.lang.ClassCastException
//        Manager m2 = (Manager) user2;

        // 如何解决? 如果知道要访问的对象的具体类型, 直接转换这个类型就可以了。
        // 如果不知道具体类型, 可以使用下面两种方法
        // 1. getClass.getName 得到具体的类型, 根据类型进行一个具体的处理过程
        // cn.xilikeli.java.escape.GeneralException$Worker
        System.out.println(user2.getClass().getName());

        // 2. 通过 instanceof 关键字判断这个类型是不是我们想要的类型, 然后再去做相应操作
        // false
        System.out.println(user2 instanceof Manager);
        // 通过以上两种方式就可以规避类型转换异常

        // 3. 枚举查找异常: 枚举在查找时, 如果枚举值不存在, 不会返回空, 而是直接抛出异常
        System.out.println(enumFind("RD"));
        System.out.println(enumFind("abc"));
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值