【高级进阶】写给大忙人看的JDK14新特性

约定不等于承诺〃 2023-03-14 13:10 59阅读 0赞
更多学习资料,可以关注下方微信公众号,回复关键字:Java高级资料 。 即可免费获取完整的Java高级开发工程师视频课程。

在这里插入图片描述
注意

  • IDEA 2020.1以上版本才支持JDK14

从哪几个方面学习新特性

  • 关注 语法层面(学习其他语言的语法)
  • 关注 API层面(新增 / 过时 / 废弃 )
  • 底层优化(JVM优化) 面试 / 了解底层原理

JDK版本使用者占比

根据国外网站2019年统计,使用 Java 8 的开发者比重最大,其次就是Java 11。其他版本的比重不大。这主要是由于 Java 8 和 Java 11 是 LTS 版本。其他的都是非 LTS 版本,意思就是其他版本Oracle不会一直维护。
在这里插入图片描述

版本更新时间

在这里插入图片描述

JDK14概述

2020年3月17日,Java 14 正式GA(General Available)。这是自从Java宣布采用六个月一次的发布周期后的第五次发布。

此次发布的版本包含 16 个JEP(Java Enhancement Proposals,JDK增强提案)。包括两个孵化器模块丶三个预览特性丶两个废弃功能丶两个删除功能。

  • 孵化器模块:将尚未定稿的API和工具提前给开发者使用,以便获得反馈,并对反馈的信息整理分析后进一步改进Java的特性。
  • 预览特性:功能已经成型丶实现已经确定,但是还没有最终定稿的功能。为了收集用户使用后真实的反馈信息,将功能提供给用户使用。促进这些功能的完善以便最终定稿。
    在这里插入图片描述

总特性概述

中文版本










































































JEP(Java 增强提案) 功能概述
305: instanceof的模式匹配(预览)
343: 包装工具(孵化器)
345: G1的NUMA感知内存分配
349: JFR事件流
352: 非易失性映射字节缓冲区
358: 有用的NullPointerExceptions
359: 记录(预览)
361: 开关表达式(标准)
362: 弃用Solaris和SPARC端口
363: 删除并发标记扫描(CMS)垃圾收集器
364: Mac OS上的ZGC
365: Windows上的ZGC
366: 弃用ParallelScavenge + SerialOld GC组合
367: 删除Pack200工具和API
368: 文本块(第二预览)
370: 外部存储器访问API(孵化器)

英文版本










































































JEP(Java 增强提案) 功能概述
305: Pattern Matching for instanceof (Preview)
343: Packaging Tool (Incubator)
345: NUMA-Aware Memory Allocation for G1
349: JFR Event Streaming
352: Non-Volatile Mapped Byte Buffers
358: Helpful NullPointerExceptions
359: Records (Preview)
361: Switch Expressions (Standard)
362: Deprecate the Solaris and SPARC Ports
363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector
364: ZGC on macOS
365: ZGC on Windows
366: Deprecate the ParallelScavenge + SerialOld GC Combination
367: Remove the Pack200 Tools and API
368: Text Blocks (Second Preview)
370: Foreign-Memory Access API (Incubator)

实用特性

JDK 14 的新特性中,对于语法层面的只有5个。所以下面将对这5个新特性进行详细讲解。






























JEP(Java 增强提案) 功能概述
305: Pattern Matching for instanceof (Preview)
358: Helpful NullPointerExceptions
359: Records (Preview)
361: Switch Expressions (Standard)
368: Text Blocks (Second Preview)

JEP 305:instanceof的模式匹配(预览)

这个特性目前在 JDK 14 中还是预览版本。通过为开发人员提供 模式匹配 来增强Java编程语言instanceof。模式匹配使程序中的通用逻辑(如从对象中有条件的提取组件)更加简洁安全。

几乎所有程序都会包含一种特殊逻辑,就是先用表达式判断是否满足某种条件。然后有条件的做进一步处理。如:instanceof表达式判断。

  1. Object obj = "string";
  2. if (obj instanceof String) {
  3. String s = (String) obj;
  4. // 使用 s 对象
  5. System.out.println(s);
  6. }

这一段代码需要处理三件事情:

  • 判断(str 是否为String类型)
  • 转换(将obj对象转换为String)
  • 定义一个新的局部变量 s ,然后我们就可以使用这个字符串。

JEP 305写法

  1. Object obj = "String";
  2. if (obj instanceof String s) {
  3. // 使用 s 对象
  4. System.out.println(s);
  5. } else {
  6. // 不可以使用 s 对象
  7. }

如果obj是一个String实例,那么它被转换为String并分配给String类型变量 s 。绑定变量在if语句的true代码块中,而不是在if语句的false代码块中。

与局部变量的作用范围不同,绑定变量的作用域由包含的表达式和语句的语义决定。例如下面的代码中

  1. Object obj = "String";
  2. if (!(obj instanceof String s)) {
  3. // 报错,不可以使用 s
  4. System.out.println(s.trim());
  5. } else {
  6. // 可以使用 s
  7. System.out.println(s.trim());
  8. }
  9. // 复杂的判断条件写法
  10. Object obj = "String";
  11. // 由于obj instanceof String s条件成立,那么这个if域就可以使用s变量,所以&&条件可以正常
  12. if (obj instanceof String s && s.length() == 5) {
  13. System.out.println("obj对象是String类型,并且长度为:5");
  14. }
  15. // 错误的判断条件写法
  16. Object obj = "String";
  17. // 如果obj instanceof String s条件不成立,那么if域就不可以使用s变量,s.length() == 5中不可以继续使用s变量。变量s不在||右侧的范围内。
  18. if (obj instanceof String s || s.length() == 5) {
  19. System.out.println("obj对象是String类型,并且长度为:5");
  20. }
  21. // 类型转换和条件判断,简化代码
  22. public boolean strNotBlank(Object obj) {
  23. return obj instanceof String s && s.length() > 0;
  24. }

JEP 358:有用的NullPointerExceptions

官方描述

  • 通过精确地描述哪个变量是null来提高由JVM生成的NullPointerException信息的可用性。

目标

  • 向开发人员和支持人员提供有关程序提前终止的有用信息。
  • 通过更清晰地将动态异常与静态程序代码关联起来,提高对程序的理解。
  • 减少新开发人员对NullPointerException的混淆和关注。

任何一个 Java 开发人员都会遇到NullPointerException(NPE)。由于NPE几乎会出现在程序中的任何位置,因此尝试捕捉和恢复它们通常都不切实际。

即使开发人员依赖JVM查明NPE的实际来源。但是对于复杂的代码,不适用调试器就无法确定那个变量为空。假设下面的代码出现一个NPE:

  1. Object obj = null;
  2. System.out.println(obj.toString().trim().contains(""));

JVM将打印出导致NPE的方法丶文件名和行号:

  1. java.lang.NullPointerException
  2. at com.zhichunqiu.features.java14.Features02.test(Features02.java:16)

文件名称和行号无法精准的定位到那个变量丶方法为空。objobj.toString()还是obj.toString().trim()

如果JVM可以提供所需的信息以查明NPE的来源,然后确定其根本原因,而无需使用额外的工具或改组代码,则整个Java生态系统都将受益。自2006年以来,SAP的商业JVM就已经做到了这一点,获得了开发人员和支持工程师的一致好评。

JDK 14 优化JVM处理NPE

JVM NullPointerException在程序试图取消引用引用的位置处抛出(NPE)null。通过分析程序的字节码指令,JVM将精确地确定哪个变量是null,并在NPE 中用空细节消息描述该变量(根据源代码)。然后,null-detail消息将显示在JVM的消息中,以及方法,文件名和行号。

注意

在 JDK 14 中并没有默认开启Helpful NullPointerException特性。需要自己配置JVM参数,可能以后的版本会默认开启。开启参数:-XX:+ShowCodeDetailsInExceptionMessages
在这里插入图片描述
还是用上面的代码执行,看报错效果:

  1. Object obj = null;
  2. System.out.println(obj.toString().trim().contains(""));

JVM 消息将剖析该语句并通过显示导致以下内容的完整访问路径来查明原因null

  1. java.lang.NullPointerException: Cannot invoke "Object.toString()" because "obj" is null
  2. at com.zhichunqiu.features.java14.Features02.test(Features02.java:16)

JVM打印的消息除了说明NPE产生的类丶方法还说明了是obj为空导致产生了NPE。

JEP 359:Records(预览)

通过Records增强 Java编程语言。Records提供了一种紧凑的语法来声明类,这些类都是浅拷贝不可变数据。

动机和目标

早在2019年2月份,Java语言架构师Brian Goetz曾经写过一篇文章,详细的说明并吐槽了Java语言,他和许多程序员一样抱怨”Java太罗嗦太繁琐”。他提到开发人员想要创建纯数据载体类,通常都必须编写大量低价值丶重复丶容易出错的代码。比如:构造函数丶getter / setter丶equals()丶hashCode()丶toString()等方法。

官方说明如下
在这里插入图片描述
描述

Records是 Java 语言中一种新的类型声明。就像enum一样,record是一种受限制的Class形式。record的引入获得了极大程度的语言简洁性,但是也没有了类的自由度:将API与表示分离的能力。

record是包含名称和状态描述的,这个状态描述是声明record组件的。record的主体是可选的。record的定义如下:

  1. public record Features03(String name, int age) { }

隐藏特征

由于records的声明非常简单,所以records将会默认的获得很多标准的成员变量或方法。

  • 每一个组成部分(变量 )都有一个隐藏的final字段声明;
  • 每个成员变量都有一个与变量名称相同的获取值的方法;例如:name属性的获取值方法不是instance.getName()而是instance.name()方法。这些都是编译后默认生成的。
  • 一个public的全参构造方法,构造方法的参数和顺序与类名称后面的()中声明的一致。
  • 实现了equalshashCode方法。如果两个records类型的类具有相同的类型和相同的状态,那么他们一定是相等。(其实就是说明实现的equalshashCode方法是标准的)
  • 实现了toString方法,会打印类名称和相关的属性说明。(其实就是说明实现的toString方法是标准的)

反编译后的Features03解读

  1. // 特点1:record定义的类,默认都继承了Record,由于Java是单继承,所以record定义的类不允许在继承其他类。
  2. // 错误示范:public record Features03 extends Object(String name, int age) { }
  3. // 特点2:record都是final类,不允许被继承。
  4. public final class Features03 extends java.lang.Record {
  5. // 特点3:所有属性都是final类型的
  6. private final java.lang.String name;
  7. private final int age;
  8. // 特点4:默认生成全参数构造函数
  9. public Features03(java.lang.String name, int age) { /* compiled code */ }
  10. // 特点5:默认生成toString hashCode equals方法
  11. public java.lang.String toString() { /* compiled code */ }
  12. public final int hashCode() { /* compiled code */ }
  13. public final boolean equals(java.lang.Object o) { /* compiled code */ }
  14. // 特点6:所有字段都提供getter方法,但是方法名称和属性名称一致
  15. public java.lang.String name() { /* compiled code */ }
  16. public int age() { /* compiled code */ }
  17. }

Records的限制

  • records不能继承任何的其他类,除了头部定义的属性(字段)外,声明的任何其他属性都必须是static的。
  • records是隐式final类,不可以是abstract抽象类。
  • records的组件是隐式final。因此可以将records作为一个数据的集合,因为不用担心数据会被修改。records只提供了属性的getter方法,属性都是final的赋值后无法修改。
  • 除了以上的限制外,records的其他行为和正常的类是一样的。具体如下

    • 可以嵌套类丶声明泛型丶实现接口丶通过new关键字实例化;
    • 可以声明静态方法丶静态字段丶静态初始化容器丶构造函数丶实例方法和嵌套类型;

    // 特征1:定义泛型 / 实现接口
    public record Features03(String name, int age) implements Serializable {

    1. // 特征2:定义的成员变量,必须是静态的,可以初始化值
    2. private static String address;
    3. // 定义公共静态常量
    4. private static final String desc = null;
    5. //特征3:实例方法
    6. public String getDesc() {
    7. return null;
    8. }
    9. //特征4:静态方法
    10. public static String getAddress() {
    11. return null;
    12. }
    13. // 特征5:可以自定义构造函数
    14. public Features03 {
    15. if (age > 10) {
    16. throw new IllegalArgumentException("年龄大于10岁");
    17. }
    18. }
    19. // 嵌套的records
    20. record MyFeatures(String dept, String desc) {
    21. }

    }

Features03反编译后的文件

  1. public final class Features03 <T> extends java.lang.Record implements java.io.Serializable {
  2. private final java.lang.String name;
  3. private final int age;
  4. private static java.lang.String address;
  5. private static final java.lang.String desc;
  6. public Features03(java.lang.String name, int age) { /* compiled code */ }
  7. public java.lang.String getDesc() { /* compiled code */ }
  8. public static java.lang.String getAddress() { /* compiled code */ }
  9. public java.lang.String toString() { /* compiled code */ }
  10. public final int hashCode() { /* compiled code */ }
  11. public final boolean equals(java.lang.Object o) { /* compiled code */ }
  12. public java.lang.String name() { /* compiled code */ }
  13. public int age() { /* compiled code */ }
  14. static final class MyFeatures extends java.lang.Record {
  15. private final java.lang.String dept;
  16. private final java.lang.String desc;
  17. public MyFeatures(java.lang.String dept, java.lang.String desc) { /* compiled code */ }
  18. public java.lang.String toString() { /* compiled code */ }
  19. public final int hashCode() { /* compiled code */ }
  20. public final boolean equals(java.lang.Object o) { /* compiled code */ }
  21. public java.lang.String dept() { /* compiled code */ }
  22. public java.lang.String desc() { /* compiled code */ }
  23. }
  24. }

JEP 361:Switch 表达式(标准)

switch表达式并不是在 JDK 14才出现的,早在 JDK 12(JEP 325) 和 13(JEP 354) 中switch就作为预览语言特性出现。JDK 12 中出现了 -> 操作符,JDK 13 中出现了yield关键字。而 JDK 14 是将12 / 13的预览特性最终确定下来作为标准特性发布。

JDK 12 之前的写法

  1. public void test() {
  2. Week day = Week.FRIDAY;
  3. switch (day) {
  4. case MONDAY:
  5. case FRIDAY:
  6. case SUNDAY:
  7. System.out.println(6);
  8. break;
  9. case TUESDAY:
  10. System.out.println(7);
  11. break;
  12. case THURSDAY:
  13. case SATURDAY:
  14. System.out.println(8);
  15. break;
  16. case WEDNESDAY:
  17. System.out.println(9);
  18. break;
  19. }
  20. }
  21. // 将匹配条件后的值赋值给一个变量
  22. public void test2() {
  23. int numLetters;
  24. Week day = Week.FRIDAY;
  25. switch (day) {
  26. case MONDAY:
  27. case FRIDAY:
  28. case SUNDAY:
  29. numLetters = 6;
  30. break;
  31. case TUESDAY:
  32. numLetters = 7;
  33. break;
  34. case THURSDAY:
  35. case SATURDAY:
  36. numLetters = 8;
  37. break;
  38. case WEDNESDAY:
  39. numLetters = 9;
  40. break;
  41. }
  42. }
  43. enum Week {
  44. MONDAY,
  45. FRIDAY,
  46. SUNDAY,
  47. TUESDAY,
  48. THURSDAY,
  49. SATURDAY,
  50. WEDNESDAY
  51. }

JDK 12写法

在 JDK 12 中引入一种新形式的开关标签 ”case L ->“,以表示如果标签匹配则仅执行标签右边的代码。而且还允许使用逗号分隔多个常量。

  • ”case L ->“开关标签右侧的代码不仅限于表达式,还可以是代码块{}或者throw语句。
  • switch既然在 JDK 12 中的描述是表达式,那么就说明可以将结果赋值给一个变量。

    public void test1() {

    1. Week day = Week.FRIDAY;
    2. switch (day) {
    3. case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
    4. case TUESDAY -> {
    5. // case 后面是一个代码块,包含多行代码
    6. System.out.println("输入的日期为:" + TUESDAY);
    7. System.out.println(7);
    8. }
    9. case THURSDAY, SATURDAY -> System.out.println(8);
    10. case WEDNESDAY -> System.out.println(9);
    11. // case 后面抛出异常
    12. default -> throw new IllegalArgumentException("输入的星期有误");
    13. }

    }

    // 将匹配条件后的值赋值给一个变量,然后将结果输出
    // 即将结果赋值给一个变量,作为表达式使用,switch需要以分号结尾
    // 方式一:赋值变量后输出结果
    @Test
    public void test3() {

    1. Week day = Week.FRIDAY;
    2. int numLetters = switch (day) {
    3. case MONDAY, FRIDAY, SUNDAY -> 6;
    4. case TUESDAY -> 7;
    5. case THURSDAY, SATURDAY -> 8;
    6. case WEDNESDAY -> 9;
    7. // case 后面抛出异常
    8. default -> throw new IllegalArgumentException("输入的星期有误");
    9. };// 末尾需要以分号结束,因为这个时候switch作为表达式使用
    10. System.out.println(numLetters);

    }
    // 方式二:将switch作为表达式直接输出
    public void test4() {

    1. Week day = Week.FRIDAY;
    2. System.out.println(
    3. // 将表达式结果输出
    4. switch (day) {
    5. case MONDAY, FRIDAY, SUNDAY -> 6;
    6. case TUESDAY -> 7;
    7. case THURSDAY, SATURDAY -> 8;
    8. case WEDNESDAY -> 9;
    9. // case 后面抛出异常
    10. default -> throw new IllegalArgumentException("输入的星期有误");
    11. }
    12. );

    }

    enum Week {

    1. MONDAY,
    2. FRIDAY,
    3. SUNDAY,
    4. TUESDAY,
    5. THURSDAY,
    6. SATURDAY,
    7. WEDNESDAY

    }

JDK 13 写法

在 JDK 13 中为了解决 JDK 12 中switch作为表达式如果返回结果时候,case -> 右边如果是一个代码块,那么就不知道何时返回结果。因此引入了一个关键字:yield

  • 如果使用case -> 的写法,如果右边只有一个表达式,就不需要使用yield关键字。只有代码块{}的时候才需要使用。
  • 如果使用case : 的写法,那么无论右边如何,需要返回结果都必须使用yield

    // yield关键字的用法,返回结果
    // 方法一: case -> 写法
    public void test5() {

    1. Week day = Week.FRIDAY;
    2. // 将表达式结果输出,
    3. int numLetters = switch (day) {
    4. case MONDAY, FRIDAY, SUNDAY -> 6;
    5. case TUESDAY -> 7;
    6. case THURSDAY, SATURDAY -> 8;
    7. // case后面如果是代码块,返回结果需要使用关键字:yield,不可以使用return返回结果
    8. case WEDNESDAY -> {
    9. System.out.println("case -> 后面是代码块时候返回结果");
    10. yield 9;
    11. }
    12. // case 后面抛出异常
    13. default -> throw new IllegalArgumentException("输入的星期有误");
    14. };

    }

    // 方法二:case: 写法
    public void test6() {

    1. Week day = Week.FRIDAY;
    2. // 将表达式结果输出
    3. int numLetters = switch (day) {
    4. case MONDAY, FRIDAY, SUNDAY: yield 6;
    5. case TUESDAY: yield 7;
    6. case THURSDAY, SATURDAY: yield 8;
    7. // case后面如果是代码块,返回结果需要使用关键字:yield,不可以使用return返回结果
    8. case WEDNESDAY: {
    9. System.out.println("case -> 后面是代码块时候返回结果");
    10. yield 9;
    11. }
    12. // case 后面抛出异常
    13. default: throw new IllegalArgumentException("输入的星期有误");
    14. };

    }

    enum Week {

    1. MONDAY,
    2. FRIDAY,
    3. SUNDAY,
    4. TUESDAY,
    5. THURSDAY,
    6. SATURDAY,
    7. WEDNESDAY

    }

警告

对于控制语句丶break丶yield丶return和continue,不能跳过switch表达式。

  1. for (int i = 0; i < Integer.MAX_VALUE; ++i) {
  2. int k = switch (e) {
  3. case 0:
  4. yield 1;
  5. case 1:
  6. yield 2;
  7. default:
  8. // 错误的写法,不能跳过switch表达式
  9. continue z;
  10. };
  11. }

JEP 368:文本块(第二次预览)

一句话含义

文本块是多行字符串文字,它可以有效避免字符串特殊字符需要转义序列。

JEP 目标

  • 通过简化跨行源代码的字符串工作,从而简化了编写Java程序的任务,同时避免了常见情况下的转义序列。
  • 增强Java程序中表示用非Java语言编写的代码字符串的可读性。
  • 通过规定相同的转义序列并且以字符串文字的形式操作,从而支持字符串文字的迁移。

病症所在

在 Java 中,在字符串文字中嵌入HTML丶XML丶SQL和JSON等片段,通常需要先进行转义和大量的串联,然后才能编译包含该片段的代码。通常这些代码都是难以维护的。

因此,通过具有一种语言学机制,可以将多行文字更直观的表示字符串,从而跨越多行也不会出现转义,从而提高了Java类程序的可读性和可写性。这就是文本块诞生的原因。

语法规则

文本块的定义由开始定界符(三个双引号字符)和结束定界符(三个双引号字符)确定文本块的边界String str = """ 文本内容 """。同时引入了\表示取消换行和\s表示一个空格。

文本块示例

  1. // 注意文本块每一行末尾都有一个换行符,所以普通的字符串长度要多1个单位。
  2. public void testBlock() {
  3. // 每一行字符串后面都有\n转义,可读性太差
  4. String html = "<html>\n" +
  5. "\t<body>\n" +
  6. "\t\t<p>Hello, world</p>\n" +
  7. "\t</body>\n" +
  8. "</html>";
  9. // 文本块写法,简单清晰,可读性好
  10. String htmlBlock = """
  11. <html>
  12. <body>
  13. <p>Hello, world</p>
  14. </body>
  15. </html>
  16. """;
  17. System.out.println(html.length()); // 结果:53
  18. System.out.println(htmlBlock.length()); // 结果:54
  19. }

语法示例

  1. // 正确的定义文本块
  2. String sql = """ // 开始分隔符一行
  3. 文本块内容
  4. """; // 结束分隔符一行
  5. // 文本块中三个"字符的序列,至少需要转义一个字符,以避免和结束定界符冲突
  6. String code =
  7. """
  8. String text = \"""
  9. A text block inside a text block
  10. \""";
  11. """;
  12. // 错误语法示范
  13. String a = """"""; // 错误,在开始分隔符后面没有换行符
  14. String b = """ """; // 错误,在开始分隔符后面没有换行符
  15. String c = """
  16. "; // 错误,并没有结束分隔符
  17. String d = """
  18. abc \ def
  19. """; // 错误,斜杆没有转义

新的转义序列

为了更好的处理换行符和空格的处理,JDK 14 引入了两个新的转义序列。

  • \转义序列明确禁止插入换行符 , 由于字符文字和传统字符串文字不允许嵌入换行符的原因,\转义序列仅适用于文本块
  • 新的\s转义序列仅转换为一个空格(\u0020),该\s转义序列可以在这两个文本块和传统的字符串文字中。

    // 对于一个很长的字符串内容,为了更加方便查看,通常会拆分为较小的子字符串进行拼接,然后将结果的字符串表达式分散到几行中:
    String literal = “Lorem ipsum dolor sit amet, consectetur adipiscing “ +

    1. "elit, sed do eiusmod tempor incididunt ut labore " +
    2. "et dolore magna aliqua.";

    // 使用\转义序列,可以将其表示为:
    // \ 表示后面是没有换行符的,所以字符串会是一行输出
    String text = “””

    1. Lorem ipsum dolor sit amet, consectetur adipiscing \
    2. elit, sed do eiusmod tempor incididunt ut labore \
    3. et dolore magna aliqua.\
    4. """;

    // \s在这个示例中,在每行的末尾使用可以确保每行正好是六个字符长度
    String colors = “””

    1. red \s
    2. green\s
    3. blue \s
    4. """;

    // 可以在任何使用字符串文字的地方使用文本块。例如,文本块和字符串文字可以互换使用:
    String code = “public void print(Object o) {“ +

    1. """
    2. System.out.println(Objects.toString(o));
    3. }
    4. """;

    // 不建议的做法:使用文本块拼接,这样会显得很笨拙
    String code = “””

    1. public void print(""" + type + """
    2. o) {
    3. System.out.println(Objects.toString(o));
    4. }
    5. """;

API对文本块的支持:附加方法

  • String::stripIndent():用于从文本块内容中去除附带的空白
  • String::translateEscapes():用于翻译转义序列
  • String::formatted(Object... args):简化文本块中的值替换
更多学习资料,可以关注下方微信公众号,回复关键字:Java高级资料 。 即可免费获取完整的Java高级开发工程师视频课程。

在这里插入图片描述

发表评论

表情:
评论列表 (有 0 条评论,59人围观)

还没有评论,来说两句吧...

相关阅读