Java新特性(Jshell、文字块、档案类、封闭类、类型匹配、switch表达式、模块化等)

Java新特性

Jshell

JShell 这个特性,是在 JDK 9 正式发布的。从名字我们就能想到,JShell 是 Java 的脚本语言。一门编程语言,为什么还需要支持脚本语言呢?编程语言的脚本语言,会是什么样子的?它又能够给我们带来什么帮助呢?

学习编程语言的时候,我们可能都是从打印“Hello, world!”这个简单的例子开始的。一般来说,Java 语言的教科书也是这样的。今天,我们也从这个例子开始,温习一下 Java 语言第一课里面涉及的知识。

class HelloWorld {
   
    public static void main(String[] args) {
   
        System.out.println("Hello, world!");
    }
}

好了,有了这段可以拷贝的代码,接下来我们该怎么办呢?

首先,我们需要一个文本编辑器,比如 vi 或者类似于 IDEA 这样的集成编辑环境,把这段代码记录下来

接下来,我们要把这段源代码编译成 Java 的字节码

$ javac HelloWorld.java

编译完成之后,我们要运行编译好的字节码,把程序的结果显示出来。在这里,我们一般使用 java 命令行,或者通过集成编辑环境来运行。

$ java HelloWorld

最后一步,我们要观察运行的结果,检查一下是不是我们期望的结果

Hello, world!

万事开头难,完成 Java 语言的第一个小程序,尤其难! 你要学习使用编辑器、使用编译器、使用运行环境。对于一个编程语言的初学者而言,这是迈入 Java 语言世界的第一步,也是很大的一步

当然,会有同学试着改动这段代码。比如说,把“Hello, world!”改成“世界你好”或者“How are you?”。 这样一来,我们就还要经历编辑、编译、运行、观察这样的过程

class HowAreYou {
   
    public static void main(String[] args) {
   
        System.out.println("How are you?");
    }
}

也许,你已经习惯了这样的过程,并没有感觉得到有什么不妥当的地方。不过,如果我们看看 bash 脚本语言的处理,也许你会发现问题所在。

bash $ echo HelloWorldHello, world!
bash $

显然,使用 bash 编写的“Hello, world!”要简单得多。你只需要在命令行输入代码,bash 就会自动检查语法,立即打印出结果;它不需要我们调用额外的编辑器、编译器以及解释器。当然,这并不是说 bash 不需要编译和运行过程。bash 只是把这些过程处理得自动化了,不再需要我们手动处理了。

JDK 17 发布的时候,我们经常可以看到这样的评论,“然而,我还是在使用 JDK 8”。确实,没有任何人,也没有任何理由责怪他们。除非有着严格的自律和强烈的好奇心,没有人喜欢学习新东西,尤其是学习门槛比较高的时候。如果需要半个小时,我们才能看一眼一个新特性的样子,重点是,这个新特性还不一定能对我们有帮助,那很可能我们就懒得去看了。或者,我们也就是看一眼介绍新特性的文档,很难有动手试一试的冲动。最后,我们对它的了解也就仅仅停留在“听过”或者“看过”的程度上,而不是进展到“练过”或者“用过”的程度。那你试想一下,如果仅仅需要一分钟,我们就能看到一个新特性的样子呢?我想,在稍纵即逝的好奇心消逝之前,我们很有可能会尝试着动动手,看一看探索的成果。实际上,学习新东西,及时的反馈能够给我们极大的激励,推动着我们深入地探索下去。那 Java 有没有办法,变得像 bash 那样,一分钟内就可以展示学习、探索的成果呢?

办法是有的。JShell,也就是 Java 的交互式编程环境,是 Java 语言给出的其中一个答案

启动 JShell
$ jshell
|  欢迎使用 JShell -- 版本 17.0.4
|  要大致了解该版本, 请键入: /help intro

jshell>

另外,JShell 的交互式编程环境,还有一个详细模式,能够提供更多的反馈结果。启用这个详尽模式的办法,就是使用“-v”这个命令行参数

$ jshell -v
|  欢迎使用 JShell -- 版本 17.0.4
|  要大致了解该版本, 请键入: /help intro
退出 JShell
jshell> /exit
|  再见
JShell 的命令

除了退出命令,我们还可以使用帮助命令,来查看 JShell 支持的命令。比如,在 JDK 17 里,帮助命令的显示结果,其中的几行大致是下面这样:

jshell> /help
|  键入 Java 语言表达式, 语句或声明。
|  或者键入以下命令之一:
|  /list [<名称或 id>|-all|-start]
|       列出您键入的源
|  /edit <名称或 id>
|       编辑源条目
|  /drop <名称或 id>
|       删除源条目
|  /save [-all|-history|-start] <文件>
|       将片段源保存到文件
......

熟悉 JShell 支持的命令,能给我们带来很大的便利。限于篇幅,我们这里不讨论 JShell 支持的命令

立即执行的语句
jshell> System.out.println("Hello, world!");
Hello, world!

jshell>

可以看到,一旦输入完成,JShell 立即就能返回执行的结果,而不再需要编辑器、编译器、解释器。

如果我们使用了错误的方法,或者不合法的语法,JShell 也能立即给出提示。

jshell> System.out.println("Hello, world\!");
|  错误:
|  非法转义符
|  System.out.println("Hello, world\!");
|                                   ^
                                  ^
可覆盖的声明

另外,JShell 还有一个特别好用的功能。那就是,它支持变量的重复声明。JShell 是一个有状态的工具,这样我们就能够很方便地处理多个有关联的语句了。比如说,我们可以先试用一个变量来指代问候语,然后再使用标准输出打印出问候语。

jshell> String greeting;
greeting ==> null
|  已创建 变量 greeting : String

jshell> String language = "English";
language ==> "English"
|  已创建 变量 language : String

jshell> greeting = switch (language) {
   
   ...>     case "English" -> "Hello";
   ...>     case "Spanish" -> "Hola";
   ...>     case "Chinese" -> "Nihao";
   ...>     default -> throw new RuntimeException("Unsupported language");
   ...> };
greeting ==> "Hello"
|  已分配给 greeting : String

jshell> System.out.println(greeting);
Hello

jshell> 

JShell 支持可覆盖的变量,主要是为了简化代码评估,解放我们的大脑。要不然,我们还得记住以前输入的、声明的变量。

文字块

文字块这个特性,首先在 JDK 13 中以预览版的形式发布。在 JDK 14 中,改进的文字块再次以预览版的形式发布。最后,文字块在 JDK 15 正式发布

文字块的概念很简单,它是一个由多行文字构成的字符串。既然是字符串,为什么还需要文字块这个新概念呢?文字块和字符串又有什么区别呢?我们还是通过案例和代码,来弄清楚这些问题

我们在编写代码的时候,总是或多或少地要和字符串打交道。有些字符串很简单,比如我们都知道的"Hello,World!"字符串。有些字符串很复杂,里面可能有换行、对齐、转义字符、占位符、连接符等。比如下面的例子中,我们要构造一个简单的表示"Hello,World!"的 HTML 字符串,就需要处理好文本对齐、换行字符、连接符以及双引号的转义字符。这就使得这段代码既不美观、也不简约,一点都不自然

String stringBlock =
        "<!DOCTYPE html>\n" +
        "<html>\n" +
        "    <body>\n" +
        "        <h1>\"Hello World!\"</h1>\n" +
        "    </body>\n" +
        "</html>\n";

这样的字符串不好写,不好看,也不好读。

摊开来说,这样的字符串编写起来不省心,不仅消耗了更多时间,代码质量也没有保障。与此同时,复杂的语句也容易分散评审者的精力,让疏漏和错误不易被发现。

费时费力、质量还难以控制,没有效率,也就意味着投入产出比低,所以我们就更不愿意投入精力和时间来做好这件事情

所见即所得的文字块

文字块是一个由多行文字构成的字符串。既然是字符串,文字块能有什么影响呢?其实,文字块是使用一个新的形式,而不是传统的形式,来表达字符串的。通过这个新的形式,文字块尝试消除换行、连接符、转义字符的影响,使得文字对齐和必要的占位符更加清晰,从而简化多行文字字符串的表达

String textBlock = """
        <!DOCTYPE html>
        <html>
            <body>
                <h1>"Hello World!"</h1>
            </body>
        </html>
        """;
System.out.println(
        "Here is the text block:\n" + textBlock);

对比一下阅读案例里的代码,我们可以看到,下面的这些特殊的字符从这个表达式里消失了:

  • 换行字符(\n)没有出现在文字块里;
  • 连接字符(+)没有出现在文字块里;
  • 双引号没有使用转义字符(\)

文字块由零个或多个内容字符组成,从开始分隔符开始,到结束分隔符结束。开始分隔符是由三个双引号字符 (“”“) ,后面跟着的零个或多个空格,以及行结束符组成的序列。结束分隔符是一个由三个双引号字符 (”“”) 组成的序列

需要注意的是,开始分隔符必须单独成行;三个双引号字符后面的空格和换行符都属于开始分隔符。

jshell> String s = """""";
|  Error:
|  illegal text block open delimiter sequence, missing line terminator
|  String s = """""";

jshell> String s = """
   ...> """;
s ==> ""

同样需要注意的是,结束分隔符只有一个由三个双引号字符组成的序列。结束分隔符之前的字符,包括换行符,都属于文字块的有效内容。

jshell> String s = """
   ...> OneLine""";
s ==> "OneLine"



jshell> String s = """
   ...> TwoLines
   ...> """;
s ==> "TwoLines\n"

由于文字块不再需要特殊字符、开始分隔符和结束分隔符这些格式安排,我们几乎就可以直接拷贝、粘贴看到的文字,而不再需要特殊的处理了。同样地,你在代码里看到的文字块是什么样子,它实际要表达的文字就是什么样子的。这也就是说,“所见即所得”

像传统的字符串一样,文字块是字符串的一种常量表达式。不同于传统字符串的是,在编译期,文字块要顺序通过如下三个不同的编译步骤:

  • 为了降低不同平台间换行符的表达差异,编译器把文字内容里的换行符统一转换成 LF(\u000A);
  • 为了能够处理 Java 源代码里的缩进空格,要删除所有文字内容行和结束分隔符共享的前导空格,以及所有文字内容行的尾部空格;
  • 最后处理转义字符,这样开发人员编写的转义序列就不会在第一步和第二步被修改或删除。

首先,我们从整体上来理解一下文字块的编译期处理这种方式。阅读一下下面的代码,你能不能预测一下下面这两个问题的结果?使用传统方式声明的字符串和使用文字块声明的字符串的内容是一样的吗?这两个字符串变量指向的是同一个对象,还是不同的对象?

public class TextBlocks {
   
    public static void main(String[] args) {
   
        String stringBlock =
                "<!DOCTYPE html>\n" +
                "<html>\n" +
                "    <body>\n" +
                "        <h1>\"Hello World!\"</h1>\n" +
                "    </body>\n" +
                "</html>\n";

        String textBlock = """
                <!DOCTYPE html>
                <html>
                    <body>
                        <h1>"Hello World!"</h1>
                    </body>
                </html>
                """;

        System.out.println(
                "Does the text block equal to the regular string? " +
                stringBlock.equals(textBlock));
        System.out.println(
                "Does the text block refer to the regular string? " +
                (stringBlock == textBlock));
    }
}

第一个问题的答案应该没有意外,第二个问题的答案可能就会有意外出现了。使用传统方式声明的字符串和使用文字块声明的字符串,它们的内容是一样的,而且指向的是同一个对象。

该怎么理解这样的结果呢?其实,这就说明了,文字块是在编译期处理的,并且在编译期被转换成了常量字符串,然后就被当作常规的字符串了。所以,如果文字块代表的内容,和传统字符串代表的内容一样,那么这两个常量字符串变量就指向同一内存地址,代表同一个对象。

虽然表达形式不同,但是文字块就是字符串。既然是字符串,就能够使用字符串支持的各种 API 和操作方法。比如,传统的字符串表现形式和文字块的表现形式可以混合使用:

System.out.println("Here is the text block:\n" +
        """
        <!DOCTYPE html>
        <html>
            <body>
                <h1>"Hello World!"</h1>
            </body>
        </html>
        """);

再比如,文字块可以调用字符串 String 的 API:

int stringSize = """
        <!DOCTYPE html>
        <html>
            <body>
                <h1>"Hello World!"</h1>
            </body>
        </html>
        """.length();

或者,使用嵌入式的表达式:

String greetingHtml = """
        <!DOCTYPE html>
        <html>
            <body>
                <h1>%s</h1>
            </body>
        </html>
        """.formatted("Hello World!");

档案类

档案类这个特性,首先在 JDK 14 中以预览版的形式发布。在 JDK 15 中,改进的档案类再次以预览版的形式发布。最后,档案类在 JDK 16正式发布

那么,什么是档案类呢?档案类的英文,使用的词汇是“record”。官方的说法,Java 档案类是用来表示不可变数据的透明载体。这样的表述,有两个关键词,一个是不可变的数据,另一个是透明的载体。

该怎么理解“不可变的数据”和“透明的载体”呢

案例

下面的这段代码,就是一个简单的、典型的圆形类的定义

public final class Circle implements Shape {
   
    private double radius;
    
    public Circle(double radius) {
   
        this.radius = radius;
    }
    
    @Override
    public double getArea() {
   
        return Math.PI * radius * radius;
    }
    
    public double getRadius() {
   
        return radius;
    }
    
    public void setRadius(double radius) {
   
        this.radius = radius;
    }
}

这个圆形类之所以典型,是因为它交代了面向对象设计的关键思想,包括面向对象编程的三大支柱性原则:封装、继承和多态。

案例分析

上面这个例子,最重要的问题,就是它的接口不是多线程安全的。如果在一个多线程的环境中,有些线程调用了 setRadius 方法,有些线程调用 getRadius 方法,这些调用的最终结果是难以预料的。这也就是我们常说的多线程安全问题

如果上述例子的实现源代码不能更改,那么就需要在调用这些接口的程序中,增加线程同步的措施。

synchronized (circleObject) {
   
    double radius = circleObject.getRadius();
    // do something with the radius.
}

或者

public final class Circle implements Shape {
   
    private double radius;
    
    public Circle(double radius) {
   
        this.radius = radius;
    }
    
    @Override
    public synchronized double getArea() {
   
        return Math.PI * radius * radius;
    }
    
    public sy
  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值