JDK8-17的特性发生了哪些变化

JDK8-17的特性发生了哪些变化

垃圾回收器

JDK 8 默认的垃圾回收器组合是:Parallel Scavenge ( 新生代 ) 和 Parallel ( 老年代 )
在 JDK 9 之后,G1 成为默认的垃圾收集器。它将内存空间无差别地划分为 region 块,且将大型对象单独放在 Humongous 区域管理。其设计目标是建立 “可停顿时间模型”,保证用户在 M 秒时间片内,回收器工作不超过 N ( GC 的资源用量占比不超过 N/M )。
CMS ( Concurrent Mark Sweep ) 在 Oracle JDK 14 当中被正式移除,同时弃用了 ParallelScavenge + serialOld GC 组合。
Oracle JDK 11 引入了新一代 ZGC 垃圾回收器,其支持 TB 级内存容量,暂停时间低 ( <10ms ),对整个程序吞吐量的影响小于 15%。在 JDK 15 之后,ZGC 可正式投入到生产环境中,并通过 -XX:+UseZGC 启动,且性能在 JDK 16,17 之后得到了进一步增强。
Open JDK 推出的则是Shenandoah垃圾回收器,由 Red Hat 团队开发,是 ZGC 垃圾回收器的 “竞品”。但由于它并不是 Oracle 的 “亲儿子”,因此在一定程度上遭到了排挤,Oracle 明确拒绝将这款垃圾回收器纳入其中。

Error occurred during initialization of VM
Option -XX:+UseShenandoahGC not supported

JDK 9 之后的版本至今,默认的垃圾回收器仍然为 G1

Java交互式编程(无需启动idea也可执行代码)

简单讲就是不用启动idea也可以通过终端的方式运行Java代码了,那通过什么启动?当然是我们的jshell了。默认在jdk的安装目录的jdk17/bin/jshell.exe。效果类似如下了:

PS C:\Users\liJunhu\IdeaProjects\testForJava17> jshell
|  Welcome to JShell -- Version 17.0.2
|  For an introduction type: /help intro

jshell> int i = 10
i ==> 10

jshell> System.out.println(i)
10

接口定义扩展(默认方法、静态方法属性、私有方法)

JDK 8,9 两个版本中均对接口的概念做了少许拓展,现在接口支持定义默认方法,静态方法/属性,私有方法

interface IService{
    // implements 该接口的类将自动获取该方法。 - jdk 8.
    default void h(){}

    // 在接口定义的属性直接被视为 static。 -jdk 8.
    double Pi = 3.1415;

    // 可以在接口直接定义静态方法。 - jdk 8.
    static void f(){}
    
    // 可以在接口内直接定义私有方法。 -jdk 9.
    private void g(){}
}

String底层结构变更(char[] to bytes[])

在 JDK 8 及之前的版本当中,String 的底层使用char[]数组存储内容;而在 JDK 9 之后,字符串的内容实际改用bytes[]字节形式存储,并配合 encodingFlag 配合编码解码。这个修改对上层代码透明,即用户层面的各种 String 操作没有发生任何变化

of 创建不可变序列(List.of、Set.of、Map.of)

集合提供了创建不可变的集合实例方法

List<Integer> list = List.of(1, 2, 3, 4, 5);
Set<Integer> set = Set.of(1, 2, 3, 4, 5);

// k1,v1,k2,v2,...,形式创建
Map<Integer, String> map1 = Map.of(1, "v1", 2, "v2");

// 通过 Map.entry 形式创建
Map<Integer, String> map2 = Map.ofEntries(
    Map.entry(1, "v1"),
    Map.entry(2, "v2"),
    Map.entry(3, "v3"),
    Map.entry(4, "v4")
);

HTTP 2 协议接口

JDK 9 中引入了新的 API 以支持 HTTP 2.0 版本。注,部分 API 在 JDK 11 之后的版本发生了更替

// throws IOException, InterruptedException
// 构建 client, 支持 http 2.0
HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build();

// 构建请求
HttpRequest req = HttpRequest.newBuilder(URI.create("https://www.baidu.com")).build();

// 发送请求
HttpResponse<String> resp = client.send(req,HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body());

引入 var 关键字(类型声明简化)

JDK 10 支持使用 var 简化局部变化的类型声明,比如:

// 显然 arrayList 是一个 ArrayList<Integer> 类型。
ArrayList<Integer> arrayList = new ArrayList<>();

适当的精简可以提高代码的可读性,其 arrayList 的实际类型将交给编译器进行推断。

var arrayList = new ArrayList<Integer>();

字符串增强(去空格、拷贝)

JDK 11 新增了部分对字符串的处理方法

println("".isBlank());      // 判断字符串是否为 "".
println("  welcome to java 17 ".strip());  // 去掉首尾空格
println("  welcome to java 17".stripLeading()); // 去掉首部空格
println("welcome to java 17 ".stripTrailing()); // 去掉尾部空格
println("word".repeat(2));     // "wordword"

lambda 表达式类型推导(通过var特性省略具体类型定义)

JDK 11 允许将 var 关键字应用到 lambda 表达式中,起到简化声明的作用。比如:

@FunctionalInterface
interface Mapper<A,B>{
       B map(A a);
}

// var 关键字现可以用于 lambda 表达式。
Mapper<String,Integer> string2int = (var a) -> a.length(); 

switch 增强(支持返回值)

增强版 switch 在 JDK 12 作为预览特性引入。在 JDK 14 之后,增强版 switch 语句块具备返回值。

var simple = switch (lang) {
    case "java" -> "j";
    case "go"   -> "g";
    default -> "<non>";
};

在新版本的switch语句中,分支创建可以使用 -> 符号,且 cases 的判断不会引发穿透现象,因此不需要显式地在每一个分支后面标注break了。下面例子展示了增强 switch 的更多特性:

var simple = switch (lang) {
    // 1. 允许在一个 case 中匹配多个值    
    case "java","scala","groovy" -> "jvm";
    case "c","cpp" -> "c";
    case "go"   -> "g";
    default -> {
        // 2. 复杂分支内返回值使用 yield 关键字 (相当于 return)
        if(lang == null){
            yield "<unknown>";
        }else yield "<non>";
    }
};

支持文本块定义

JDK 13 允许使用三引号"""表示长文本内容。

var block = """
        lang: java
        version: 13
        dbname: mysql
        ip: 192.168.140.2
        usr: root
        pwd: 1000
        """;

// 6 行
block.lines().count();

文本块内允许插入\阻止换行,如:

var block = """
        lang: java\
        version: 13\
        dbname: mysql\
        ip: 192.168.140.2\
        usr: root\
        pwd: 1000
        """;
// 实际效果:            
// "lang:javaversion: 13dbname: mysql..."
// 1 行
block.lines().count();

另外注意,每行末尾的空格会被忽略,除非主动将其替换为 /s

instanceof 模式匹配(省略强转)

如果一个上转型对象的具体类型已经被 instanceof 关键字确定,那么其后续操作可省略强制转换,见下面的示例:

// o 可能是 String 类型或者是 Double 类型。
Object o = new Random().nextInt() % 2 == 0 ?  "java16" : 1000.0d;

// s 相当于被确定类型之后的 o。
if(o instanceof String s){
    // 不再需要 ((String)o).length()
    System.out.println(s.length());
}else {
    System.out.println();
}

引入record 关键字(类上标注该关键字省略get,set,构造方法)

record关键字修饰的类相当于 Scala 的样例类 case class,或者可看成是 Lombok 中 @Data 注解的一个 "低配" 替代。编译器会一次性代替用户生成 getters,constructors,toString 等方法。

record Student(String name,Integer age){}
// ....
var student = new Student("Wang Fang",13);
student.age();
student.name();

新增密封类sealed的定义(不允许继承)

sealed class 的概念和 Scala 类似,密封类不允许在包外被继承拓展,密封类必须具备子类。

// 密封类
public sealed class People {}

// 密封类 People 必须至少有一个子类。
// 非密封的 People 子类
non-sealed class Teacher extends People{}

// 密封的 People 子类。
sealed class Driver extends People{}
non-sealed class TruckDriver extends Driver{}

密封类可以声明子类,但必须需要严格声明其子类是密封( sealed )或者是非密封 ( non-sealed ) 的。以下是 JDK 17 的预览功能:permits 关键字可进一步限制同一个包下有哪些类允许继承它。
// 即使在一个包下,也只有 Teacher Driver 被允许继承 People

public sealed class People permits Teacher,Driver{}

non-sealed class Teacher extends People{}
non-sealed class Driver extends People{}

switch二度加强(类型匹配)

JDK 17 对 switch 语句做了进一步增强,它现在支持匹配类型以及判空的功能:

Number v1 = 3.1415d;

switch (v1) {
    case null -> println("null");
    case Float f -> println(f);
    case Double d -> println(d);
    default -> println("NaN");
}

模块化特性

JDK 9 引入了模块化特性,称 Java Platform Module System,简称 JPMS,或者 Project jigsaw。模块化的优势有三点:

  • 改进组件之间的依赖管理,引入比 jar 包粒度更大的 Module。
  • 使得 java 程序更容易轻量级部署。
  • 改进性能和安全性。

模块化还使得精简庞大的 JRE 成为可能

Why we need module

JDK9-JPMS模块化wshten-CSDN博客jpms
在提及模块化之前,首先要谈谈 “为什么不能只依赖 jar 包”。直到 JDK 8 之前,一切 Java 工程都是基于 jar 包构建的,而 jar 包本质上只是 “多个 package 的压缩包”,自身完全不携带任何描述控制权限和引用依赖等信息。
因此,jar 包本身不会告知 JVM 它还依赖哪些 jar 包,这完全需要开发者自行判断 jar 包之间的依赖关系并下载缺失的 jar 包。好在后来用户拥有了 Maven,Gradle,Sbt 这类包管理工具,才避免了 Jar-hell 的问题 ( 类似地,还有 Windows 的 dll-hell )。
模块 module 本质上仍然是 jar 包 ( 下文称作为模块的 jar 包为 module-jar ),但它额外增加了一个描述模块规则的 module-info.java 文件,可以理解成 "模块 = 普通 jar 包 + 权限控制"。jar 包之间的关联现在就是靠module-info.java文件来组织的,其格式为:

// ${moduleName} 填写模块名称。
module ${moduleName} {
    // 描述此 module 的更多规则,见后文。
}

原则上模块的命名应当和项目名保持一致,但这并不是必须的。注,模块 module 之间是平级的,只有相互依赖的关系,没有 has-a 的 “父子关系”。
"导入 jar 还是 module-jar " 对 用户透明,用户可以放心地将模块的 jar 包当成普通 jar 包来使用,也可以将普通 jar 包当成 module-jar 包来使用 ( 原因见后文的模块分类部分 )。在 IntelliJ IDEA 编译器下打包一个 module-jar, 除了 在项目根目录下 新建一个 module-info.java 文件之外,其它步骤和打包一个普通 jar 没有什么区别。
在这里插入图片描述

( 注:From modules with dependencies... 选项可让 IDE 补充 Jar 包所需的 META-INF/MANIFEST.MF )
特别注意,用户如果打算打包一个 module-jar,那么类文件将不能声明在项目的顶级目录,通俗的说就是不能把类文件直接扔在src源文件根目录下了。否则会报错:

Caused by: java.lang.module.InvalidModuleDescriptorException: XXX.class found in top-level directory (unnamed package not allowed in module)

JDK 9 之后有两种执行模式:作为普通的 jar 包执行 ( 向后兼容 ),或者是作为module-jar执行。

# 视作普通的 jar 包运行
java -jar ${JarPath}
# 运行 module-jar 包,
# --module-path, -p : 提供目标模块及依赖的 .jar, .jmods 所在的目录。 
# --module, -m : 提供模块名称以及主程序类全限定名。
java --module-path ${ModulePath} --module ${moduleName}/${MainClass}
# 示例
# java --module-path . --module priv.jdk9test.utils/priv.jdk9test.utils.TimePrinter

--module-path--class-path 的概念很相似。不过 --module-path 中的 *.jar 或者是*.jmod文件 ( 该文件格式见后文的 JMOD ) 被当作模块来处理,而--class-path中的的jar包仍按照传统的方式处理。

下面简单介绍 module-info.java 文件的几个权限控制规则:

exports [ to ... ]
opens [ to ... ]
requires [ static | transitive ]
provide ... with ...
uses ...

注意,模块导出 ( exports ) 以包 ( package ) 为单位,而导入 (requires) 以模块为单位,只有声明导入了模块,才能继续在代码中import此模块 导出的包。
模块 ( 包括后文的 ) 约束 只对模块起作用。如果用户的项目只是普通项目 ( 没有编写 module-info.java 文件 ),那么反而不会受限制,这听起来虽然有点奇怪,但这种考量是向后兼容的权衡之举,主要是为了照顾那些没有引入 JDK 9 模块化的旧工程。
最基本的关键字是 exposes ,它表明模块将导出哪些包 ( package ) 供其它模块引用。

module priv.jdk9test.helper {
    exposes priv.jdk9test.helper;
}

越界 import 模块内容会被编译器拦截。大意为:在 xxx 模块下定义的ppp包并没有导出给 yyy

Package 'ppp' is declared in module 'xxx', which does not export it to module 'yyy'

同时,越界反射模块内容也会在运行期被拦截,并抛出 java.lang.IllegalAccessException。若想要限制某些包在运行期可被反射获取,但在编译期不可用,则可以用opens替代 exports

// 编译期禁止出现对 priv.jdk9test.unsafe 的 import。
// 但是允许在运行期通过反射获取。
opens priv.jdk9test.unsafe

可以指定将包开放给指定的模块,使用 exports ... to ... 语法,比如:

module priv.jdk9test.helper{
    exports priv.jkd9test.helper;
    // 导出给多个模块可使用 , 分隔。
    exports priv.jdk9test.services to priv.jdk9test.user;
}

而依赖方需要通过 requires 将其它模块纳入声明中,比如:

module priv.jdk9test.user {
    requires priv.jdk9test.helper;
    requires priv.jdk9test.service;
}

导入声明首先会强制要求指定的 module-jar 已经被加载进依赖路径中,否则编译会不通过,这避免了运行期出现 ClassNotFound 的问题。另一方面,只有主动声明导入( requires )之后才可以继续在本模块内importmodule-jar exports 给自身的内容。需要注意,如果模块C requires的模块 A 事实上并没有向模块 C exports 任何包,那么这条 requires 声明不会报错,但实际上不会起任何作用。
顺带一提,如果用户正在 IntelliJ IDEA 环境下开发多个子项目,且子项目之间相互引用 ( 比如 priv.jdk9test.user 需要使用 priv.jdk9test.helper 编译出的 jar 包 ),可以在 project structures 进行如下设置 ( 重点是黄字部分 ):
https://blog.fntop.cn/archives/jdk17change#Joe-2

requires 基础之上还附带 transitive 传递规则。它的作用是:若模块 B requires transitive 另一个模块 A,现有另一模块 C requires 模块 B,则它相当于隐式地声明了 requires A

// Module A -> module-info.java
module A {
    exports a1 to B;
    exports a2 to C;
}
​
// Module B -> module-info.java
module B {
    // 传递导入模块 A
    requires transitive A;    
    // 模块 B 声明导出的包,其中 b2 仅向 C 导出。
    exports b1;
    exports b2 to C;
}
​
// Module C -> module-info.java
module C {
    requires B;
    // 由于 B 传递导入了模块 A,因此
    // 模块 C 相当于隐式地声明了:
    // requires A;
}

在上面的例子当中,即使在模块 C 的规则文件中不主动声明 requires A,它也能够直接访问模块 A 开放的a2包。如果模块 B 对模块 A 的导入并不是传递性质的,那么模块 C 就必须主动附加上这条声明。
如果某些依赖模块只在编译时需要,那么可以为其添加static关键字:

requires static priv.jdk9test.initializer

模块化中另一个特殊的导入导出是 usesprovides with,它们类似于Java的服务发现机制 SPI,实现了接口和实现类的解耦。服务的提供方需要开放自己的接口,并使用 provides ... with ... 声明此服务接口的具体实现类。

// 开放服务接口
exports priv.jdk9test.services to priv.jdk9test.user;
​
// 提供服务接口的实现
provides priv.jdk9test.services.MyService with priv.jdk9test.servicesImpl.IPService;

客户端则需要在module-info.java文件中声明对接口的使用uses ,其具体的实现由 ServiceLoader 负责加载。

// module-info.java
// uses priv.jdk9test.services.MyService
​
// 通过 ServiceLoader 获取服务接口的实现。
// 这段代码没有声明任何关于 priv.jdk9test.servicesImpl.* 的任何声明。
// 返回的服务实现用 Provider<T> 包装,因此还需要借助 map 提取出来。
List<MyService> services = ServiceLoader.load(
    MyService.class
).stream().map(
    ServiceLoader.Provider::get
).toList();
​
// 实际上调用的是 priv.jdk9test.servicesImpl
services.forEach(MyService::getService);

在 JDK 9 当中,JDK 被分为了 94 个 modules,现在只需加载用户程序依赖的modules。Java 保留了一个重要的基础模块 java.base,它仅对外导出而没有任何导入:

module java.base {
    exports java.io;
    exports java.lang;
    exports java.module;
    exports java.math;
    exports java.nio;
    exports java.util;
    ...
}

所有的模块默认依赖 java.base 模块。

模块分类

JDK 9 中的模块化实际上分为四种:普通模块,开放模块,自动模块,未命名模块。其中,普通模块遵循上述的细则进行权限控制。
开放模块的声明格式如下:

open module ${module-name} {
    // 不适用 opens 规则。
}

开放模块意味着内部所有的声明都是默认可以在运行期被反射获取的。和普通模块相比,开放模块可以声明 exportsrequires,provides & uses,但是不包括 opens
当一个普通 jar 包 ( 通常都是在 JDK 8 及之前编译的 ) 被放在模块搜索路径 --module-path 时,它将被视作一个自动模块,这个模块的名称和版本由 jar 包文件的名称派生。此举是 JDK 9 为向后兼容而设计的机制,自动模块总是读取所有模块,且打开 ( open ) 并导出 ( exports ) 所有包,这也解释了在前文删除 module-info.java 文件之后,所有的模块约束反而都 “消失” 的原因。
由于并不是所有的依赖库的厂商目前都提供了模块化版本,因此,若要将旧项目迁移到 JDK 9 及更新的版本,可以将各种依赖放到 --module-path 下,将它们变为自动模块。
可以将 jar 或者module-jar放在类搜索路径 --class-path。这样,当类型加载器在--module-path找不到类文件时,模块系统便尝试在类路径中寻找匹配的类型。如果成功了,则此类型会被归到 unnamed 模块 ( 未命名模块 ) 下。注意,其它模块无法对这个模块声明 requires

JLink

使用 JLink 可以生成一个 Java 程序的运行时镜像,它仅包含精简版 JRE + 项目代码。这意味着:

节省了内存,提高了性能。
允许开发仅提供很小内存的微服务。
更加适合物联网设备。

以下是几个重要的可选参数:

# 模块所在的路径,自动包含 jdk jmods.
--module-path <path>
-p <path>
​
# 添加的模块,至少要有一个。多个模块可以用 , 分割
# 模块的来源可以是 *.jar,也可以是 *.jmod。
# jlink 在 --module-path 指定的路径下搜索模块。
--add-modules <mod>[,<mod>...]
​
# 输出文件夹
--output <path>

下面的 jlink 中,在当前目录 . 下寻找模块,并向 ./jdk9Project 输出两个模块。

 jlink --module-path . --add-modules priv.jdk9test.utils,priv.jdk9test.helper --output ./jdk9Project

所有的用户程序及其依赖被压缩到lib/moudles内。进入到输出路径的 /bin 目录下即可通过 java 命令执行这个镜像:

cd bin
# -p : 依赖模块的路径
# -m : 作为程序入口的模块
java -m testForJava/priv.jdk9test.utils.TimePrinter

IntelliJ IDEA 在 Project Structure 中提供了打包 Jlink 的选项。
在这里插入图片描述

JMOD

JMOD 文件被设计成可以打包比 jar 更丰富的内容,包括本地代码,配置文件,本地命令和其它类型的数据,所以 JMOD 适合于那些依赖本地环境的模块,比如在 Windows 版本的 JDK 中 java.base.jmods/lib 携带*.dll文件,而在 Linux 版本下则是 *.so 文件 ( 因此 JMOD 本身不一定是跨平台的 )。
JDK 9 版本之后,jdk 目录下原有的jre目录被移除,取而代之的是jmods目录,内部保存着 jdk 被模块化拆分后的各种 *.jmod 文件,以供 JLink 工具提取并构建最小化的运行时镜像。JMOD 文件通过 JDK 提供的 jmod 工具打包,它位于 %JAVA_HOME%\bin 目录下。
打包 JMOD 之前需要将文件归类后存放到不同的路径下,由 jmod 工具转储到 *.jmod 包下的不同目录中去 ( 目录的命名遵从jmod自身定义的规范,如配置文件保存在 conf 目录下 )。

# 编译的 class 文件路径
--class-path <path>
# 本地命令路径
--cmds <path>
# 用户可编辑的配置文件路径
--config <path>
# 本地链接库路径,.dll,.so 等。
--libs <path>
# 头文件路径
--header-files <path>
# module 路径
--module-path <path>
-p <path>

下面的命令演示了如何将各种文件打包为一个 jmod。其中--class-path是必须的:

jmod create --config configs/ --class-path classes/* aJmod.jmod

Maven 提供jmod 插件,见:Apache Maven JMod Plugin – jmod:create
关于 JMOD 的文件格式还是一个开放的 issuse,目前它是基于 .zip 格式的,因此只需简单更改后缀名即可查看*.jmod的内容。JMOD 仅限在编译或者在链接时使用,比如在 JLink 工具中可以直接将其内部的模块添加到 --add-modules 当中,相关的配置,头文件等也会自动迁移到镜像的对应目录。

转载

鸣谢

  • 非常感谢你从头到尾阅读了这篇文章,希望其中的内容对你有所启发和帮助。如果你还有其他问题或需要进一步的了解,欢迎随时关注我的动态并留言
  • 最后可以给作者点个关注和小赞赞嘛,谢谢!
  • 觉得有收藏价值可以进行收藏
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在将JDK 8升级JDK 17时,有几个注意事项需要考虑: 1. 兼容性:在升级之前,需要确认你的应用程序是否与JDK 17兼容。由于JDK 17引入了许多新特性和改进,一些旧的应用程序可能需要进行一些修改才能在新的JDK版本下正常运行。 2. API变更:JDK 17可能对一些API进行了修改或删除。在进行升级之前,你需要仔细查看JDK 17的发行说明和API文档,以了解哪些API发生变化,并相应地修改你的代码。 3. 工具和库的兼容性:除了应用程序本身,还需要考虑与JDK 8相关的工具和库的兼容性。确保你使用的开发工具、构建工具和第三方库支持JDK 17,并根据需要进行更新。 4. 测试:在升级之前,强烈建议进行全面的测试。确保你的应用程序在JDK 17下能够正常运行,并检查是否有任何功能缺陷或性能问题。 5. 配置更新:在升级后,你可能需要更新一些配置文件或脚本,以反映新的JDK版本。例如,你可能需要更新环境变量、路径或运行脚本。 综上所述,升级JDK 8到JDK 17需要考虑兼容性、API变更、工具和库的兼容性、全面测试以及配置更新等注意事项。确保在升级之前进行充分的准备和测试,以确保你的应用程序能够平稳地迁移到新的JDK版本。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [JDK8升级JDK17过程中遇到的那些坑](https://blog.csdn.net/BASK2312/article/details/130509221)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

幽·

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

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

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

打赏作者

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

抵扣说明:

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

余额充值