Java之JDK新特性

原创
2019/03/08 11:27
阅读数 108

JDK新特性

Author: Lijb

Email: lijb1121@163.com

JDK9新特性

modularity System 模块系统

Modularity提供了类似于OSGI框架的功能,模块之间存在相互的依赖关系,可以导出一个公共的API,并且隐藏实现的细节,Java提供该功能的主要的动机在于,减少内存的开销,在JVM启动的时候,至少会有30~60MB的内存加载,主要原因是JVM需要加载rt.jar,不管其中的类是否被classloader加载,第一步整个jar都会被JVM加载到内存当中去,模块化可以根据模块的需要加载程序运行需要的class。在引入了模块系统之后,JDK 被重新组织成 94 个模块。Java 应用可以通过新增的 jlink 工具,创建出只包含所依赖的 JDK 模块的自定义运行时镜像。这样可以极大的减少 Java 运行时环境的大小。使得JDK可以在更小的设备中使用。采用模块化系统的应用程序只需要这些应用程序所需的那部分JDK模块,而非是整个JDK框架了。

HTTP/2

JDK9之前提供HttpURLConnection API来实现Http访问功能,但是这个类基本很少使用,一般都会选择Apache的Http Client,此次在Java 9的版本中引入了一个新的package:java.net.http,里面提供了对Http访问很好的支持,不仅支持Http1.1而且还支持HTTP2以及WebSocket,据说性能特别好。

JShell

类似于Python
java9引入了jshell这个交互性工具,让Java也可以像脚本语言一样来运行,可以从控制台启动 jshell ,在 jshell 中直接输入表达式并查看其执行结果。当需要测试一个方法的运行效果,或是快速的对表达式进行求值时,jshell 都非常实用。
除了表达式之外,还可以创建 Java 类和方法。jshell 也有基本的代码完成功能。我们在教人们如何编写 Java 的过程中,不再需要解释 “public static void main(String [] args)” 这句废话。

不可变集合工厂方法

也叫作只读集合

Java 9增加了List.of()、Set.of()、Map.of()和Map.ofEntries()等工厂方法来创建不可变集合.

  • 在JDK1.9之前创建只读集合
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
//设为只读List集合
list = Collections.unmodifiableList(list);
System.out.println(list);
Set<String> set = new HashSet<>();
set.add("E");
set.add("F");
set.add("G");
//设为只读Set集合
set = Collections.unmodifiableSet(set);
System.out.println(set);
Map<String, String> map = new HashMap<>();
map.put("k1", "v1");
map.put("k2", "v2");
map.put("k3", "v3");
//设为只读Map集合
map = Collections.unmodifiableMap(map);
System.out.println(map);

  • JDK1.9创建只读集合
List<String> list = List.of("A", "B", "C");
System.out.println(list);
Set<String> set = Set.of("E", "F", "G");
System.out.println(set);
Map<String, String> map = Map.of("k1", "v1", "k2", "v2", "k3", "v3");
System.out.println(map);

/*除了更短和更好阅读之外,这些方法也可以避免您选择特定的集合实现。在创建后,继续添加元素到这些集合会导致 “UnsupportedOperationException” 。*/

私有接口方法

Java 8 为我们提供了接口的默认方法和静态方法,接口也可以包含行为,而不仅仅是方法定义。

默认方法和静态方法可以共享接口中的私有方法,因此避免了代码冗余,这也使代码更加清晰。如果私有方法是静态的,那这个方法就属于这个接口的。并且没有静态的私有方法只能被在接口中的实例调用。

interface InterfaceWithPrivateMethods {

    private static String staticPrivate() {
        return "static private";
    }

    private String instancePrivate() {
        return "instance private";
    }

    default void check() {    
        String result = staticPrivate();
        InterfaceWithPrivateMethods pvt = new InterfaceWithPrivateMethods() {
            // anonymous class 匿名类
        };

        result = pvt.instancePrivate();
    }
}

增强 Stream API

JDK9在Stream接口中新增4个方法:dropWhile、takeWhile、ofNullable,为iterate方法新增重载方法。

takeWhile

takeWhile可以用于从 Stream 中获取一部分数据,接受一个 Predicate 来进行选择,在有序的 Stream 中,takeWhile 返回从头开始的尽可能多的元素。

List<Integer> list = Arrays.asList(45,43,76,87,42,77,90,73,67,88);
list.stream().takeWhile((x) -> x < 80 ).forEach(System.out::println);
//返回结果
45
43
76

从返回结果可以看出,takeWhile将会按照list集合有序的从45开始到第一个不符合条件为止的所有结果。

dropWhile

dropWhile 的方法刚好与 takeWhile想法,返回剩余的元素。

List<Integer> list = Arrays.asList(45,43,76,87,42,77,90,73,67,88);
list.stream().dropWhile((x) -> x < 80 ).forEach(System.out::println);
//返回结果
87
42
77
90
73
67
88

从返回结果可以看出,dropWhile方法刚好和takeWhile方法形成互补,按照list集合有序的返回从第一个不满足条件元素开始到最后为止的所有结果。

ofNullable

在JDK8 中 Stream 不能完全为null,否则会报空指针异常。而在JDK9 中 ofNullable 方法允许创建一个为空的 Stream。

//NullPointerException
//Stream<Object> stream1 = Stream.of(null);
//System.out.println(stream1.count());
//不报异常  允许这样写
Stream<String> stringStream = Stream.of("AA", "BB", null);
System.out.println(stringStream.count());
//不报异常  允许这样写
List<String> list = new ArrayList<>();
list.add("A");
list.add(null);
System.out.println(list.stream().count());
//ofNullable() :允许值为 null
Stream<Object> stream = Stream.ofNullable(null);
System.out.println(stream.count());
Stream<String> stream2 = Stream.ofNullable("Hello World");
System.out.println(stream2.count());


//输出结果:
3
2
0
1

改进 Optional 类

Optional 类是在JDK8中新增的类,主要是为了解决空指针异常。在JDK9中对这个类进行了改进,主要是新增了三个方法:stream,ifPresentOrElse 和 or 。

stream

stream方法将Optional转为一个 Stream,如果Optional 没有值就返回一个 Stream.empty。

List<String> list = List.of("A", "B", "C", "D", "E", "F");
Optional<List<String>> optional = Optional.of(list);
optional.stream().forEach(System.out::println);
Optional<Object> optional1 = Optional.empty();
System.out.println(optional.stream().count());

//输出结果:
[A, B, C, D, E, F]
1

ifPresentOrElse

ifPresentOrElse方法:

public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)

如果 Optional 包含值,则对其包含的值调用函数 action,即 action.accept(value),这与 ifPresent 一致;与 ifPresent 方法的区别在于,ifPresentOrElse 还有第二个参数 emptyAction。如果 Optional 不包含值,那么 ifPresentOrElse 便会调用 emptyAction,即 emptyAction.run()。

//如果optional包含值,执行action.accept方法。
Optional<Integer> optional = Optional.of(1);
optional.ifPresentOrElse( x -> System.out.println("Value: " + x),() ->
        System.out.println("没有值."));
optional = Optional.empty();
//如果optional不包含值,执行emptyAction.run方法。
optional.ifPresentOrElse( x -> System.out.println("Value: " + x),() ->
        System.out.println("没有值."));

//输出结果:
Value: 1
没有值.

or

or方法:

public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier)

如果Optional有值,返回 Optional 指定的值,否则返回一个预设的值。

//如果有值返回值。
Optional<String> optional1 = Optional.of("1");
Supplier<Optional<String>> supplierString = () -> Optional.of("没有值");
optional1 = optional1.or(supplierString);
optional1.ifPresent( x -> System.out.println("Value: " + x));
//如果没值返回预先设定的值。
optional1 = Optional.empty();
optional1 = optional1.or( supplierString);
optional1.ifPresent( x -> System.out.println("Value: " + x));

//输出结果:
Value: 1
Value: 没有值 

多版本兼容 JAR

当一个新版本的 Java 出现的时候,你的库用户要花费很长时间才会切换到这个新的版本。这就意味着库要去向后兼容你想要支持的最老的 Java 版本 (许多情况下就是 Java 6 或者 7)。这实际上意味着未来的很长一段时间,你都不能在库中运用 Java 9 所提供的新特性。幸运的是,多版本兼容 JAR 功能能让你创建仅在特定版本的 Java 环境中运行库程序时选择使用的 class 版本:

改进钻石操作符

JDK9中钻石操作符可以使用匿名实现类,可以在匿名实现类中重写方法等操作。

Set<String> set = new HashSet<>(){
    //匿名实现类重写add方法。
    @Override
    public boolean add(String s) {
        System.out.println("执行add方法");
        return super.add(s);
    }
};
set.add("1");

//输出结果:执行add方法

限制使用单独下划线标识符

在JDK8之前可以使用“_”单独的下划线作为标识符,但在JDK9中将单独的下划线标识符限制使用了,可能后期会将这个标识符做特殊处理如Lambda表达式一样的->操作符一样。如下示例:

在JDK8中可以单独使用“_”命名

String _ = "Hello";
System.out.println(_);
//输出结果:Hello

在JDK9中回抛出异常

String 存储结构变更

从很多不同应用程序收集的信息表名,字符串是堆使用的主要组成部分,而且,大多数字符串对象只包含一个字符,这样的字符只需要一个字节的存储空间,因此这些字符串对象的内部char数组中有一半的空间被闲置。

JDK9之前String底层使用char数组存储数据private final char value[],JDK9将String底层存储数据改为byte数组存储数据private final byte[] value。

StringBuffer和StringBuilder也同样做了变更,将以往char数组改为byte数组。

多分辨率图像 API

在 java.awt.image 包下新增了支持多分辨率图片的API,用于支持多分辨率的图片。

将不同分辨率的图像封装到一张(多分辨率的)图像中,作为它的变体。
获取这个图像的所有变体。
获取特定分辨率的图像变体,表示一张已知分辨率单位为 DPI 的特定尺寸大小的逻辑图像,并且这张图像是最佳的变体。
java.awt.image.MultiResolutionImage接口的基础实现java.awt.image.BaseMultiResolutionImage获取所需要的变体。
通过接口的getResolutionVariant(double destImageWidth, double destImageHeight)方法,根据分辨率获取图像。

智能 JAVA 编译工具

智能 java 编译工具( sjavac )的第一个阶段始于 JEP139 这个项目,用于在多核处理器情况下提升 JDK 的编译速度。如今,这个项目已经进入第二阶段,即 JEP199,其目的是改进 Java 编译工具,并取代目前 JDK 编译工具 javac,继而成为 Java 环境默认的通用的智能编译工具。
JDK 9 还更新了 javac 编译器以便能够将 java 9 代码编译运行在低版本 Java 中。

java 动态编译器

JIT(Just-in-time)编译器可以在运行时将热点编译成本地代码,速度很快。但是 Java 项目现在变得很大很复杂,因此 JIT 编译器需要花费较长时间才能热身完,而且有些 Java 方法还没法编译,性能方面也会下降。AoT 编译就是为了解决这些问题而生的。
在 JDK 9 中, AOT(JEP 295: Ahead-of-Time Compilation)作为实验特性被引入进来,开发者可以利用新的 jaotc 工具将重点代码转换成类似类库一样的文件。虽然仍处于试验阶段,但这个功能使得 Java应用在被虚拟机启动之前能够先将 Java 类编译为原生代码。此功能旨在改进小型和大型应用程序的启动时间,同时对峰值性能的影响很小。

统一 JVM 日志

jdk1.8及之前的JVM详情参考:https://my.oschina.net/u/3991887/blog/2874576

Java 9 中 ,JVM 有了统一的日志记录系统,可以使用新的命令行选项-Xlog 来控制 JVM 上 所有组件的日志记录。该日志记录系统可以设置输出的日志消息的标签、级别、修饰符和输出目标等

java9的垃圾收集机制

jdk1.8及之前的GC机制参考:https://my.oschina.net/u/3991887/blog/2874576

Java 9 移除了在 Java 8 中 被废弃的垃圾回收器配置组合,同时把G1设为默认的垃圾回收器实现。替代了之前默认使用的Parallel GC,对于这个改变,evens的评论是酱紫的:这项变更是很重要的,因为相对于Parallel来说,G1会在应用线程上做更多的事情,而Parallel几乎没有在应用线程上做任何事情,它基本上完全依赖GC线程完成所有的内存管理。这意味着切换到G1将会为应用线程带来额外的工作,从而直接影响到应用的性能

I/O 流新特性

java.io.InputStream 中增加了新的方法来读取和复制 InputStream 中包含的数据。

    readAllBytes:读取 InputStream 中的所有剩余字节。

    readNBytes: 从 InputStream 中读取指定数量的字节到数组中。

    transferTo:读取 InputStream 中的全部字节并写入到指定的 OutputStream 中 

除以上几点之外,新特性还有很多。。。

JDK10新特性

局部变量类型推断

JDK10 可以使用var作为局部变量类型推断标识符,此符号仅适用于局部变量,增强for循环的索引,以及传统for循环的本地变量;它不能使用于方法形式参数,构造函数形式参数,方法返回类型,字段,catch形式参数或任何其他类型的变量声明。

标识符var不是关键字;相反,它是一个保留的类型名称。这意味着var用作变量,方法名或则包名称的代码不会受到影响;但var不能作为类或则接口的名字(但这样命名是比较罕见的,因为他违反了通常的命名约定,类和接口首字母应该大写)。

var str = "ABC"; //根据推断为 字符串类型
var l = 10L;//根据10L 推断long 类型
var flag = true;//根据 true推断 boolean 类型
var flag1 = 1;//这里会推断boolean类型。0表示false 非0表示true
var list = new ArrayList<String>();  // 推断 ArrayList<String>
var stream = list.stream();          // 推断 Stream<String>
=============================================================
    
//反编译之后的class文件
String str = "ABC";
long l = 10L;
boolean flag = true;
int flag1 = true;
ArrayList<String> list = new ArrayList();
Stream<String> stream = list.stream();

将JDK多存储库合并为单存储库

为了简化开发,将JDK多存储库合并到一个存储库中。多年来,JDK的完整代码已经被分解成多个存储库。在JDK9 中有八个仓库:root、corba、hotspot、jaxp、jaxws、jdk、langtools和nashorn。在JDK10中被合并为一个存储库。

虽然这种多存储库模型具有一些优点,但它也有许多缺点,并且在支持各种可取的源代码管理操作方面做得很差。特别是,不可能在相互依赖的变更存储库之间执行原子提交。例如,如果一个bug修复或RFE的代码现在同时跨越了jdk和hotspot 存储库,那么对于两个存储库来说,在托管这两个不同的存储库中,对两个存储库的更改是不可能实现的。跨多个存储库的变更是常见。

垃圾回收接口

这不是让开发者用来控制垃圾回收的接口;而是一个在 JVM 源代码中的允许另外的垃圾回收器快速方便的集成的接口。

垃圾回收接口为HotSpot的GC代码提供更好的模块化;在不影响当前代码的基础情况下,将GC添加到HotSpot变的更简单;更容易从JDK构建中排除GC。实际添加或删除GC不是目标,这项工作将使HotSpot中GC算法的构建时间隔离取得进展,但它不是完全构建时间隔离的目标。

并行Full GC 的G1

JDK10 通过并行Full GC,改善G1的延迟。G1垃圾收集器在JDK 9中是默认的。以前的默认值并行收集器中有一个并行的Full GC。为了尽量减少对使用GC用户的影响,G1的Full GC也应该并行。

G1垃圾收集器的设计目的是避免Full收集,但是当集合不能足够快地回收内存时,就会出现完全GC。目前对G1的Full GC的实现使用了单线程标记-清除-压缩算法。JDK10 使用并行化标记-清除-压缩算法,并使用Young和Mixed收集器相同的线程数量。线程的数量可以由-XX:ParallelGCThreads选项来控制,但是这也会影响用Young和Mixed收集器的线程数量。

应用数据共享

为了提高启动和内存占用,扩展现有的类数据共享(CDS)特性,允许将应用程序类放置在共享档案中。

通过在不同的Java进程间共享公共类元数据来减少占用空间。
提升启动时间。
CDS允许将来自JDK的运行时映像文件($JAVA_HOME/lib/modules)的归档类和应用程序类路径加载到内置平台和系统类加载器中。
CDS允许将归档类加载到自定义类加载器中。

线程局部管控

在不执行全局VM安全点的情况下对线程执行回调的方法。让它停止单个线程而不是全部线程。

移除Native-Header Generation Tool (javah)

JDK10 从JDK中移除了javah 工具。该工具已被JDK8 (JDK-7150368)中添加javac高级功能所取代。此功能提供了在编译java源代码时编写本机头文件的功能,从而无需使用单独的工具。

Unicode 标签扩展

JDK10 改善 java.util.Locale 类和相关的 API 以实现额外 BCP 47 语言标签的 Unicode 扩展。尤其以下扩展支持:

cu:货币类型
fw:一周的第一天
rg:区域覆盖
tz:时区
为支持以上扩展,JDK10对以下API进行更改:

java.text.DateFormat::get*Instance:将根据扩展ca、rg或tz返回实例。
java.text.DateFormatSymbols::getInstance:将根据扩展rg返回实例。
java.text.DecimalFormatSymbols::getInstance:将根据扩展rg返回实例。
java.text.NumberFormat::get*Instance:将根据nu或rg返回实例。
java.time.format.DateTimeFormatter::localizedBy:将返回DateTimeFormatter 根据ca,rg或rz的实例。
java.time.format.DateTimeFormatterBuilder::getLocalizedDateTimePattern:将根据rg返回String。
java.time.format.DecimalStyle::of:将返回DecimalStyle根据nu或rg的实例。
java.time.temporal.WeekFields::of:将返回WeekFields根据fw或rg的实例。
java.util.Calendar::{getFirstDayOfWeek,getMinimalDaysInWeek}:将根据fw或rg返回值。
java.util.Currency::getInstance:将返回Currency根据cu或rg返回实例。
java.util.Locale::getDisplayName:将返回一个包含这些U扩展名的显示名称的字符串。
java.util.spi.LocaleNameProvider:将为这些U扩展的键和类型提供新的SPI。

备用内存设备上分配堆内存

启用HotSpot VM以在用户指定的备用内存设备上分配Java对象堆。随着廉价的NV-DIMM内存的可用性,未来的系统可能配备了异构的内存架构。这种技术的一个例子是英特尔的3D XPoint。这样的体系结构,除了DRAM之外,还会有一种或多种类型的非DRAM内存,具有不同的特征。具有与DRAM具有相同语义的可选内存设备,包括原子操作的语义,因此可以在不改变现有应用程序代码的情况下使用DRAM代替DRAM。所有其他的内存结构,如代码堆、metaspace、线程堆栈等等,都将继续驻留在DRAM中。

参考以下使用案例:

在多JVM部署中,某些JVM(如守护进程,服务等)的优先级低于其他JVM。与DRAM相比,NV-DIMM可能具有更高的访问延迟。低优先级进程可以为堆使用NV-DIMM内存,允许高优先级进程使用更多DRAM。
诸如大数据和内存数据库等应用程序对内存的需求不断增加。这种应用可以将NV-DIMM用于堆,因为与DRAM相比,NV-DIMM可能具有更大的容量,成本更低。

基于实验JAVA 的JIT 编译器

启用基于Java的JIT编译器Graal,将其用作Linux / x64平台上的实验性JIT编译器。Graal是一个基于Java的JIT编译器,它是JDK 9中引入的Ahead-of-Time(AOT)编译器的基础。使它成为实验性JIT编译器是Project Metropolis的一项举措,它是下一步是研究JDK的基于Java的JIT的可行性。

使Graal可用作实验JIT编译器,从Linux / x64平台开始。Graal将使用JDK 9中引入的JVM编译器接口(JVMCI)。Graal已经在JDK中,因此将它作为实验JIT将主要用于测试和调试工作。要启用Graal作为JIT编译器,请在java命令行上使用以下选项:

-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler

Root 证书

在JDK中提供一组默认的root 认证权威(CA)证书。在Oracle的Java SE根CA程序中开源root证书,以使OpenJDK构建对开发人员更有吸引力,并减少这些构建和Oracle JDK构建之间的差异。

cacerts密钥存储库是JDK的一部分,它的目的是包含一组root证书,这些root证书可以用来在各种安全协议中使用的证书链中建立信任。然而,JDK源代码中的cacerts密钥库目前是空的。因此,诸如TLS之类的关键安全组件在OpenJDK构建中不会默认工作。为了解决这个问题,用户必须配置和填充cacerts密钥库,并使用一组root证书来记录,例如, JDK 9 release notes。

基于时间的版本控制

在JEP 223 引入的版本字符串方案比以往有了显著的改进,但是,该方案并不适合未来,现在Java SE平台和JDK的新版本严格按照六个月的节奏发布。JEP 223方案的主要困难在于发行版的版本号对于其前身的重要性和兼容性进行了编码。然而,在基于时间发布模式中,这些品质并不是事先知道的。在发布的开发周期中,它们可能会发生变化,直到最终的功能被集成为止。因此发布的版本号也是未知的。

使用JEP 223的版本号语义,每个在JDK发布版上工作或者在其上构建或使用组件的人都必须先说明发布的发布日期,然后切换到说版本号,已知。维护库,框架和工具的开发人员必须准备好更改在每个JDK发布周期后期检查版本号的代码。这对所有参与者来说都很尴尬和混乱。

因此,这里提出的主要改变是重新编制版本号来编码,而不是编码的兼容性和重要性,而是按照发布周期的时间推移。这是更适合基于时间的发布模型,因为每个发布周期,因此每个发布的版本号,总是提前知道。

后续的版本格式为:[1-9][0-9]*((\.0)*\.[1-9][0-9]*)*

该格式可以是任意长度,但前四个被赋予特定含义,如:$FEATURE.$INTERIM.$UPDATE.$PATCH

$FEATURE:功能发布计数器,不管发布内容如何,都会为每个功能发布增加。功能可能会添加到功能发布中; 如果提前通知提前至少发布一次功能发布,它们也可能会被删除。如果合理,可能会做出不兼容的更改。
$INTERIM:临时版本计数器,对于包含兼容错误修复和增强功能的非功能版本递增,但没有不兼容的更改,没有功能移除,也没有对标准API的更改。
$UPDATE:更新版本计数器增加了兼容更新版本,可解决新功能中的安全问题,回归和错误。
$PATCH:紧急修补程序释放计数器只有在需要生成紧急释放以解决关键问题时才会增加。
版本号永远不会有零元素结尾。如果一个元素及其后的所有元素在逻辑上具有零值,那么它们全部被省略。

在严格六个月的发布模式下,版本号如下所示:

$FEATURE 每六个月增加一次。如:2018年3月发布的是JDK 10,2018年9月发布的是JDK 11,等等。
$INTERIM 总是为零,因为六个月的模型不包括临时版本。在此为保留其灵活性,以便将来对版本模型的修订可能包括此类版本。
$UPDATE 在$FEATURE发布后的一个月递增,之后每三个月递增一次:如2018年4月发布JDK 10.0.1。7月发布的是JDK 10.0.2等等。

JDK11新特性

正在研究。。。。。。

181:[基于嵌套的访问控制](http://openjdk.java.net/jeps/181)
309:[动态类 - 文件常量](http://openjdk.java.net/jeps/309)
315:[改进Aarch64内在函数](http://openjdk.java.net/jeps/315)
318:[Epsilon:无操作垃圾收集器](http://openjdk.java.net/jeps/318)
320:[移除Java EE和CORBA模块](http://openjdk.java.net/jeps/320)
321:[HTTP客户端(标准)](http://openjdk.java.net/jeps/321)
323:[本地变量Lambda参数](http://openjdk.java.net/jeps/323)
324的[语法](http://openjdk.java.net/jeps/323):[与Curve25519和Curve448的密钥协议](http://openjdk.java.net/jeps/324)
327:[Unicode 10](http://openjdk.java.net/jeps/327)
328:[飞行记录器](http://openjdk.java.net/jeps/328)
329:[ChaCha20和Poly1305加密算法](http://openjdk.java.net/jeps/329)
330:[启动单文件源代码程序](http://openjdk.java.net/jeps/330)
331:[低开销堆分析](http://openjdk.java.net/jeps/331)
332:[传输层安全性(TLS)1.3](http://openjdk.java.net/jeps/332)
333:[ZGC:可扩展的低延迟垃圾收集器
   (实验性)](http://openjdk.java.net/jeps/333)
335:[弃用Nashorn JavaScript引擎](http://openjdk.java.net/jeps/335)
336:[弃用Pack200工具和API](http://openjdk.java.net/jeps/336)
展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部