• Day710.文字块-Java8后最重要新特性


    文字块

    Hi,我是阿昌,今天学习的是关于所见即所得的文字块(text blocks)

    文字块(text blocks)这个特性,首先在 JDK 13 中以预览版的形式发布。

    在 JDK 14 中,改进的文字块再次以预览版的形式发布。

    最后,文字块在 JDK 15 正式发布

    文字块的概念很简单,它是一个由多行文字构成的字符串

    一、阅读案例

    我们在编写代码的时候,总是或多或少地要和字符串打交道。

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

    String stringBlock =
            "\n" +
            "\n" +
            "    \n" +
            "        

    \"Hello World!\"

    \n"
    + " \n" + "\n";
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这样的字符串不好写,不好看,也不好读。更糟糕的是,我们有时候需要从别的地方拷贝一段 HTML 或者 SQL 语句,然后再转换成类似于上面的字符串。

    是不是出力多,收效少,需要特别的耐心?

    遗憾的是,这样的工作还特别多,HTML, SQL, XML, JSON, HTTP, 随便就可以列一大堆。

    不论对于写代码的人,还是阅读代码的人来说,处理这样的字符串都不是一件赏心悦目的事情。

    软件的质量是一个反馈系统,糟糕的事情总是可以让事情变得更糟糕。

    摊开来说,这样的字符串编写起来不省心,不仅消耗了更多时间,代码质量也没有保障。

    与此同时,复杂的语句也容易分散评审者的精力,让疏漏和错误不易被发现。费时费力、质量还难以控制,这让复杂字符串的处理变成了一个很没有效率的事情。

    没有效率,也就意味着投入产出比低,所以我们就更不愿意投入精力和时间来做好这件事情。对于用户来说,糟糕的结果也会耗费他们更多的精力和时间。用户有多少,这个糟糕的成本就放大多少倍。

    如果你经常需要阅读调试日志,你可能会有更深刻的体会。

    难以阅读的调试日志,可能会让你产生短暂的抗拒心理,甚至暂时地放弃调试,直到你的耐心又回来了。

    遗憾的是,提高调试日志的可读性,似乎永远排不上开发者的日程表。

    这不是一个让人愉快的事情。不过,我们似乎也不曾有过更好的办法。

    二、所见即所得的文字块

    文字块是人们在试图扭转这种糟糕局面的过程中一个最重要的尝试。

    文字块是一个由多行文字构成的字符串。既然是字符串,文字块能有什么影响呢?

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

    下面的这段代码,就是我使用文字块对阅读案例所做的改进。

    String textBlock = """
            
            
                
                    

    "Hello World!"

    """
    ; System.out.println( "Here is the text block:\n" + textBlock);
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

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

    另外,出现在文字块开始和结束位置的,是三个双引号序列

    而不是我们在字符串声明里看到的单个双引号。 文字块由零个或多个内容字符组成,从开始分隔符开始,到结束分隔符结束。

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

    结束分隔符是一个由三个双引号字符 (”“”) 组成的序列

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

    所以,一个文字块至少有两行代码。

    即使是一个空字符,结束分隔符也不能和开始分隔符放在同一行代码里。

    jshell> String s = """""";
    |  Error:
    |  illegal text block open delimiter sequence, missing line terminator
    |  String s = """""";
    
    jshell> String s = """
       ...> """;
    s ==> ""
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    jshell> String s = """
       ...> OneLine""";
    s ==> "OneLine"
    
    
    
    jshell> String s = """
       ...> TwoLines
       ...> """;
    s ==> "TwoLines\n"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    由于文字块不再需要特殊字符、开始分隔符和结束分隔符这些格式安排,我们几乎就可以直接拷贝、粘贴看到的文字,而不再需要特殊的处理了。

    同样地,你在代码里看到的文字块是什么样子,它实际要表达的文字就是什么样子的。这也就是说,“所见即所得”。

    很多系统里常见的“所见即所得”的境界,终于也能够在 Java 语言里呈现出来了。

    三、文字块的编译过程

    那么,我们用文字块改进过的阅读案例,打印结果是什么样子的呢?

    从下面的打印结果,我们可以看到,为了代码整洁而使用的缩进空格并没有出现在打印的结果里。

    Here is the text block:
    DOCTYPE html>
    <html>
        <body>
            <h1>"Hello World!"h1>
        body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    也就是说,文字块的内容并没有计入缩进空格。

    文字块是怎么处理缩进空格的呢?这是我们学习文字块必须要了解的一个问题。

    像传统的字符串一样,文字块是字符串的一种常量表达式。

    不同于传统字符串的是,在编译期,文字块要顺序通过如下三个不同的编译步骤:

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

    阅读一下下面的代码,你能不能预测一下下面这两个问题的结果?

    使用传统方式声明的字符串和使用文字块声明的字符串的内容是一样的吗?

    这两个字符串变量指向的是同一个对象,还是不同的对象?

    package co.ivi.jus.text.modern;
    public class TextBlocks {
        public static void main(String[] args) {
            String stringBlock =
                    "\n" +
                    "\n" +
                    "    \n" +
                    "        

    \"Hello World!\"

    \n"
    + " \n" + "\n"; String textBlock = """

    "Hello World!"

    """
    ; 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)); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    第一个问题的答案应该没有意外,第二个问题的答案可能就会有意外出现了。

    使用传统方式声明的字符串和使用文字块声明的字符串,它们的内容是一样的,而且指向的是同一个对象。该怎么理解这样的结果呢?

    其实,这就说明了,文字块是在编译期处理的,并且在编译期被转换成了常量字符串,然后就被当作常规的字符串了。

    所以,如果文字块代表的内容,和传统字符串代表的内容一样,那么这两个常量字符串变量就指向同一内存地址代表同一个对象

    虽然表达形式不同,但是文字块就是字符串。

    既然是字符串,就能够使用字符串支持的各种 API 和操作方法

    比如,传统的字符串表现形式和文字块的表现形式可以混合使用:

    System.out.println("Here is the text block:\n" +
            """
            
            
                
                    

    "Hello World!"

    """
    );
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

    int stringSize = """
            
            
                
                    

    "Hello World!"

    """
    .length();
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    String greetingHtml = """
            
            
                
                    

    %s

    """
    .formatted("Hello World!");
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    四、巧妙的结束分隔符

    好的,我们现在看看文字块编译的细分步骤。

    第一个和第二个步骤都很好理解。

    不过,第二个步骤里“删除共享的前导空格”,是一个我们可以巧妙使用的规则。通过合理地安排共享的前导空格,我们可以实现文字的编排和缩进。

    为了方便理解,在下面的例子里,我们使用小数点号‘.’表示编译期要删除的前导空格,使用叹号‘!’表示编译期要删除的尾部空格。

    第一个例子,我们把结束分隔符单独放在一行,和文本内容左边对齐。

    这时候,共享的前导空格就是文本内容本身共享的前导空格;结束分隔符仅仅是用来结束文字块的。

    这个例子里,我还加入了文字内容行的尾部空格,它们在编译期会被删除掉

    // There are 8 leading white spaces in common
    String textBlock = """
    ........
    ........
    ........    
    ........        

    "Hello World!"

    !!!! ........ ........ ........"""
    ;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    第二个例子,我们也把结束分隔符单独放在一行,但是放在比文本内容更靠左的位置。

    这时候,结束分隔符除了用来结束文字块之外,还参与界定共享的前导空格

    // There are 4 leading white spaces in common
    String textBlock = """
    ....    
    ....    
    ....        
    ....            

    "Hello World!"

    !!!! .... .... ...."""
    ;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    第三个例子,我们也把结束分隔符单独放在了一行,但是放在文本内容左对齐位置的右侧。

    这时候,结束分隔符的左侧,除了共享的前导空格之外,还有多余的空格。

    这些多余的空格,就成了文字内容行的尾部空格,它们在编译期会被删除掉

    // There are 8 leading white spaces in common
    String textBlock = """
    ........
    ........
    ........    
    ........        

    "Hello World!"

    !!!! ........ ........ ........!!!!"""
    ;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    五、尾部空格还能回来吗?

    一般情况下,尾部空格确实没有什么实质性的作用。

    但是万一需要尾部空格,它们还能回来吗?

    其实是可以的。

    为了能够支持尾部附带的空格,文字块还引入了另外一个新的转义字符,‘\s’,空格转义符。

    空格转义符表示一个空格。我们前面说过的文字块的编译器处理顺序,空格转义符不会在文字块的编译期被删除,因此空格转义符之前的空格也能被保留。

    所以,每一行使用一个空格转义符也就足够了。

    下面的代码,就是一个重新带回尾部空格的例子,这个字符串的前两行就包含有尾部空格。

    // There are 8 leading white spaces in common
    String textBlock = """
    ........    \s!!!!
    ........             \s
    ........    !!!!!!!!!!
    ........        

    "Hello World!"

    ........ ........ ........"""
    ;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    六、该怎么表达长段落?

    编码规范一般都限定每一行的字节数 ,通常是 80 个或者 120 个字节。

    可是一个文本的长段落通常要超出这个限制。

    文字块里的换行符通常需要保留,编码规范通常要遵守,那该如何表达长段落或者长行呢?

    针对这种情况,文字块引入了一个新的转义字符,‘< 行终止符 >’,换行转义符。

    换行转义符的意思是,如果转移符号出现在一个行的结束位置,这一行的换行符就会被取缔。下面的例子就使用了换行转义符,它就把分散在两行的"Hello World!"连接在一行里了。

    String textBlock = """
            <!DOCTYPE html>
            <html>
                <body>
                    <h1>"Hello \
            World!"</h1>
                </body>
            </html>
            """;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    需要注意的是,上面的例子里,换行转义符之前,还有一个空格。这个空格会被删除吗?

    连接后的字符,是没有空格间隔的“HelloWorld!”,还是中间有空格的“Hello World!”?还记得我们前面说过的编译器处理顺序吗?

    空格处理先于转义字符处理

    因此,换行转义符之前的空格不算是文字块的尾部空格,因此会得到保留。

    七、总结

    了解了文字块的基本概念,它的表达形式以及编译的过程。

    文字块是 Java 语言中一种新的文字。 字符串能够出现的任何地方,也都可以用文字块表示。

    但是,文字块提供了更好的表现力和更少的复杂性。

    文字块“所见即所得”的表现形式,使得使用复杂字符串的代码更加清晰,便于编辑,也便于阅读。

    这是一个能够降低代码错误,提高生产效率的改进。

    • 知道文字块的基本概念,以及文字块和字符串的关系;
      • 面试问题:你知道 Java 的文字块吗?它和字符串有什么区别?
    • 了解文字块要解决的问题,并且能够准确使用文字块;
      • 面试问题:应当什么时候使用文字块?
    • 了解文字块的表达形式,编译过程以及文字块特有的转义字符。
      • 面试问题:怎么用文字块实现文本缩进?

    总结点

    1. 认识 textarea 是什么?基本语法是什么?
    2. textarea 和 原始的长字符串处理方式产生的对象指向同一内存地址。
    3. 文字块可以调用字符串 String 的 API 。
    4. .可以使用嵌入式的表达式 。
    5. textarea 是如何处理共享的前导空格的。
    6. 需要保留 textarea 怎么办?添加一个 \s 即可保留 此行 ‘\s’ 及之前的空格。
    7. 如何在遵守编码规范的前提下处理长段落:在需要断行的地方 添加 \ 即可。

  • 相关阅读:
    MAC一体机密码修改或者重新创建账户?
    【Pandas总结】第一节 Pamdas 简介与Series,DataFrame的创建
    学会根据数据手册指令格式发送数据
    ERMiner: Sequential Rule Mining Using Equivalence Classes
    信息系统及其技术发展
    原型模型(clone()和拷贝构造器之间的选择)
    python中使用xlrd、xlwt操作excel表格详解
    MyBatis调用SqlServer存储过程
    rust学习——变量遮蔽特性(继承式可变)
    深度学习基础知识 register_buffer 与 register_parameter用法分析
  • 原文地址:https://blog.csdn.net/qq_43284469/article/details/126374794