本文档描述了对Java语言规范的更改,以支持Java SE 21的预览功能String Templates。有关该功能的概述,请参阅JEP 430。
更改是针对JLS现有部分描述的。新文本显示为这样,删除的文本显示为
这样。必要时,解释和讨论将放在灰色框中。
变更日志:
2023-05-26:进行了轻微的编辑更改。
2023-03-29:移除SimpleProcessor和StringProcessor接口。
2023-03-23:反映新的包组织和重命名。
2023-03-01:进一步进行了轻微更改。
2023-02-22:根据反馈进行了小的更改。
2023-02-09:第三稿。除各种编辑更改外,其他重大更改包括:
- 在3.13中完全指定了模板的标记化(包括引入的新歧义以及如何解决它们)
- 改进了文本块模板的处理。
- 在整个文档中包含了更多示例。
2022-11-15:第二稿。主要更改涉及词法和语法语法的细节。为模板引入了新术语。
2022-01-20:发布了第一稿。
第2章:语法
2.1 上下文无关文法
上下文无关文法由多个产生式组成。每个产生式都有一个称为左侧的抽象符号,以及一个或多个非终结符和终结符符号的序列作为右侧。对于每个文法,终结符从指定的字母表中提取。
从由单个显著非终结符组成的句子开始,给定的上下文无关文法指定了一种语言,即可能从将序列中的任何非终结符替换为其左侧的产生式的右侧得到的终结符序列的集合。
有些文法是歧义的,即从目标符号开始,可能有多种不同的方式应用产生式以得到相同的终结符序列。解决歧义涉及要么优先考虑一种特定的应用产生式的方式,要么考虑其他上下文信息。
2.2 词法文法
Java编程语言的词法文法在3中给出。该文法的终结符是Unicode字符集中的字符。它定义了一组产生式,从目标符号Input(3.5)开始,描述了如何将Unicode字符序列(3.1)转换为输入元素序列(3.2)。
这些输入元素,去除空格(3.6)和注释(3.7),形成Java编程语言的语法文法的终结符,并称为标记(3.5)。这些标记包括Java编程语言的标识符(3.8)、关键字(3.9)、文字(3.10)、分隔符(3.11)和操作符(3.12)。
词法文法是歧义的,有一些规则确定了如何解决这些歧义(3.5)。
2.3 句法文法
Java编程语言的句法文法在第4、6-10、14和15章中给出。该文法的终结符是由词法文法定义的标记。它定义了一组产生式,从目标符号CompilationUnit(7.3)开始,描述了如何形成符合语法正确的程序的标记序列。
在少数几个地方,句法文法的特定产生式提供上下文以解决词法文法中的歧义(3.5)。
为方便起见,句法文法在第19章中一起呈现。
第3章:词法结构
本章指定了Java编程语言的词法结构。
程序使用Unicode(3.1)编写,但提供了词法转换(3.2),以便可以使用Unicode转义符(3.3)仅使用ASCII字符包含任何Unicode字符。定义了行终止符(3.4),以支持现有主机系统的不同约定,同时保持一致的行号。
从词法转换产生的Unicode字符被减少为一系列输入元素(3.5),这些元素是空格(3.6)、注释(3.7)和标记。这些标记是标识符(3.8)、关键字(3.9)、文字(3.10)、分隔符(3.11)、
和操作符(3.12),以及语法文法的片段(3.13)。
3.1 Unicode
程序使用Unicode字符集(1.7)编写。有关此字符集及其相关字符编码的信息,请访问<https://www.unicode.org/>。
Java SE平台跟踪Unicode标准的演变。给定版本使用的Unicode精确版本在Character类的文档中指定。
Unicode标准最初设计为固定宽度的16位字符编码。后来已更改,以允许需要超过16位的表示的字符。合法代码点的范围现在是U+0000到U+10FFFF,使用十六进制的U+n表示法。代码点大于U+FFFF的字符称为补充字符。为了仅使用16位单元表示完整字符范围,Unicode标准定义了一种称为UTF-16的编码。在此编码中,补充字符表示为16位代码单元的对,第一个来自高代理范围(U+D800到U+DBFF),第二个来自低代理范围(U+DC00到U+DFFF)。对于范围在U+0000到U+FFFF的字符,代码点和UTF-16代码单元的值是相同的。
Java编程语言使用UTF-16编码将文本表示为16位代码单元序列。
Java SE平台的一些API,主要在
Character类中,使用32位整数表示代码点作为单独的实体。Java SE平台提供了在16位和32位表示之间进行转换的方法。
本规范在表示相关时使用术语代码点和UTF-16代码单元,在表示与讨论无关时使用通用术语字符。
除了注释(3.7)、标识符(3.8)和字符字面量、字符串字面量、
和文本块以及模板(3.10.4、3.10.5、3.10.6、3.13),程序中的所有输入元素(3.5)仅由ASCII字符(或Unicode转义符(3.3)导致ASCII字符)形成。
ASCII(ANSI X3.4)是信息交换的美国标准代码。Unicode UTF-16编码的前128个字符是ASCII字符。
3.5 输入元素和标记
3.3)和输入行识别( 3.4)产生的输入字符和行终止符被减少为一系列 输入元素。
- Input:
- {InputElement} [Sub]
- InputElement:
- WhiteSpace
- Comment
- Token
- Token:
- Identifier
- Keyword
- Literal
- Separator
- Operator
- Fragment
- Sub:
- ASCII SUB字符,也称为"控制-Z"
标记。标记是语法文法的终结符( 2.3)。
3.6)和注释( 3.7)可以用来分隔标记,如果相邻可能以另一种方式标记化。
例如,输入字符
-和=只有在没有空格或注释时才能形成运算符标记-=(3.12)。另一个例子,十个输入字符staticvoid形成一个单一的标识符标记,而十一个输入字符static void(在c和v之间有一个ASCII SP字符)形成一对关键字标记,static和void,之间用空格分隔。
为了与某些操作系统兼容,如果ASCII SUB字符(\u001a,或控制-Z)是转义输入流中的最后一个字符,则会被忽略。
输入产生是模棱两可的,这意味着对于一些输入字符序列,将输入字符减少为输入元素(即对输入字符进行标记化)的方式不止一种。歧义解决如下:
-
一个可以减少为标识符标记或文字标记的输入字符序列总是减少为文字标记。
-
一个可以减少为标识符标记或保留关键字标记(3.9)的输入字符序列总是减少为保留关键字标记。
-
一个可以减少为上下文关键字标记或其他(非关键字)标记的输入字符序列根据上下文减少,如3.9中指定的。
-
如果输入字符
>出现在类型上下文中(4.11),即作为句法语法中的类型或未声明类型的一部分(4.1,8.3),它总是减少为数值比较运算符>,即使它可以与相邻的>字符结合形成不同的运算符。如果没有这个
>字符的规则,类型中的两个连续>括号(例如List<List<String>>)将被标记为有符号右移运算符>>,而类型中的三个连续>括号(例如List<List<List<String>>>)将被标记为无符号右移运算符>>>。更糟糕的是,类型中四个或更多连续的>括号(例如List<List<List<List<String>>>>)的标记是模棱两可的,因为各种组合的>,>>和>>>标记可以代表>>>>字符。
考虑结果输入流中的两个标记x和y。如果x在y之前,则我们说x在y的左侧,并且y在x的右侧。
例如,在这段简单的代码中:
class Empty { }我们说
}标记在{标记的右侧,即使在这个二维表示中,它出现在{标记的下方和左侧。关于使用左和右这两个词的约定使我们可以谈论,例如,二元运算符的右操作数或赋值的左侧。
3.10 文字
3.10.7 转义序列
在字符文字、字符串文字、
和文本块以及模板片段(3.10.4,3.10.5,3.10.6,3.13)中,转义序列允许表示一些非图形字符,而不使用Unicode转义(3.3),以及单引号、双引号和反斜杠字符。
- 转义序列:
-
\ b*(退格 BS,Unicode\u0008)* -
\ s*(空格 SP,Unicode\u0020)* -
\ t*(水平制表符 HT,Unicode\u0009)* -
\ n*(换行 LF,Unicode\u000a)* -
\ f*(换页 FF,Unicode\u000c)* -
\ r*(回车 CR,Unicode\u000d)* -
\行终止符 *(行继续,没有Unicode表示)* -
\ "*(双引号",Unicode\u0022)* -
\ '*(单引号',Unicode\u0027)* -
\ \*(反斜杠\,Unicode\u005c)* -
八进制转义 *(八进制值,Unicode
\u0000到\u00ff)* - 八进制转义:
-
\八进制数字 -
\八进制数字 八进制数字 -
\ZeroToThree 八进制数字 八进制数字 # - 八进制数字:
- (之一)
-
0 1 2 3 4 5 6 7 - ZeroToThree:
- (之一)
-
0 1 2 3
上述八进制数字产生来自3.10.1。八进制转义是为了与C兼容而提供的,但只能表示Unicode值
\u0000到\u00FF,因此通常更喜欢使用Unicode转义。
如果在转义序列中反斜杠后面的字符不是行终止符或ASCII b、s、t、n、f、r、"、'、\、0、1、2、3、4、5、6或7,则是编译时错误。
字符文字、字符串文字、
或文本块或模板片段中的转义序列通过用转义序列语法中的Unicode转义表示的单个字符替换其\和尾随字符来解释。行继续转义序列没有对应的Unicode转义,因此通过将其替换为无内容来解释。
行继续转义序列可以出现在文本块中,但不能出现在字符文字或字符串文字中,因为每个都不允许行终止符。
字符序列\{不是转义序列,但在模板中出现时具有特殊含义(3.13)。
3.13 片段
一个模板(15.8.6)类似于一个字符串字面量或文本块,但包含一个或多个嵌入表达式,这些表达式以字符序列\{为前缀,以字符}为后缀。
一个片段表示模板的非表达式部分。
- 片段:
- StringTemplateBegin
- StringTemplateMid
- StringTemplateEnd
- TextBlockTemplateBegin
- TextBlockTemplateMid
- TextBlockTemplateEnd
- StringTemplateBegin:
-
"StringFragment\{ - StringTemplateMid:
-
}StringFragment\{ - StringTemplateEnd:
-
}StringFragment" - StringFragment:
- { StringCharacter }
- TextBlockTemplateBegin:
-
"""{ TextBlockWhiteSpace } LineTerminator TextBlockFragment\{ - TextBlockTemplateMid:
-
}TextBlockFragment\{ - TextBlockTemplateEnd:
-
}TextBlockFragment""" - TextBlockFragment:
- { TextBlockCharacter }
- StringCharacter:
- InputCharacter 但不是
"或\- EscapeSequence
- TextBlockWhiteSpace:
- WhiteSpace 但不是LineTerminator
- LineTerminator:
- ASCII LF字符,也称为“换行符”
- ASCII CR字符,也称为“回车符”
- ASCII CR字符后跟ASCII LF字符
- TextBlockCharacter:
- InputCharacter 但不是
\- EscapeSequence
- LineTerminator
一个片段的内容定义如下:
- StringTemplateBegin的内容是紧跟在开头
"之后,紧接着第一个\{序列之前结束的字符序列。(由于序列\{不是有效的转义序列,它将作为第一个嵌入表达式的前缀。) - StringTemplateMid的内容是紧跟在字符
}之后,紧接着下一个\{序列之前结束的字符序列。 - StringTemplateEnd的内容是紧跟在字符
}之后,紧接着结束引号"之前结束的字符序列。 - TextBlockTemplateBegin的内容是紧跟在开头分隔符(3.10.6)之后,紧接着第一个
\{序列之前结束的字符序列。(由于序列\{不是有效的转义序列,它将作为第一个嵌入表达式的前缀。) - TextBlockTemplateMid的内容是紧跟在字符
}之后,紧接着下一个\{序列之前结束的字符序列。 - TextBlockTemplateEnd的内容是紧跟在字符
}之后,紧接着结束分隔符(3.10.6)之前结束的字符序列。
在StringTemplateBegin、StringTemplateMid或StringTemplateEnd标记的内容中出现行终止符(3.4)是编译时错误。
TextBlockTemplateBegin、TextBlockTemplateMid或TextBlockTemplateEnd标记的内容进一步通过以下步骤进行转换:
-
行终止符被规范化为ASCII LF字符,如下:
-
ASCII CR字符后跟ASCII LF字符被转换为ASCII LF字符。
-
ASCII CR字符被转换为ASCII LF字符。
-
虽然模板类似于字符串字面量(和文本块),但它们不会产生歧义,因为一系列输入字符不可能同时形成语法正确的字符串字面量和语法正确的模板。这是因为模板必须包含至少一个嵌入表达式,但是前缀嵌入表达式的序列\{在字符串字面量(或文本块)中不是有效的转义序列。
然而,片段产生式确实会与其他标记产生式(3.5)引入歧义。这些歧义解决如下:
-
在将输入字符减少为输入元素(3.5)期间,如果一系列输入字符在概念上匹配StringTemplateMid(或StringTemplateEnd),则仅当初始输入字符
}的减少不是在被识别为ClassBody、ConstructorBody、EnumBody、RecordBody、InterfaceBody、ElementValueArrayInitializer、ArrayInitializer、Block或SwitchBlock(8.1.7、8.8.7、8.9.1、8.10.2、9.1.5、9.7.1、10.6、14.2、14.11.1)的终端时,才将其减少为StringTemplateMid(或StringTemplateEnd),该终端出现在模板的嵌入表达式中。 -
在将输入字符减少为输入元素(3.5)期间,如果一系列输入字符在概念上匹配TextBlockTemplateMid(或TextBlockTemplateEnd),则仅当初始输入字符
}的减少不是在被识别为ClassBody、ConstructorBody、EnumBody、RecordBody、InterfaceBody、ElementValueArrayInitializer、ArrayInitializer、Block或SwitchBlock中的终端时,才将其减少为TextBlockTemplateMid(或TextBlockTemplateEnd),该终端出现在模板的嵌入表达式中。
例如,考虑18个输入字符的序列
" \ { n e w i n t [ ] { 4 2 } } "。前三个输入字符被减少为StringTemplateBegin。接下来的十二个输入字符被减少为标记Keyword(new)、Keyword(int)、Separator([)、Separator(])、Separator({)和Literal(42)。序列中的下一个输入字符}造成了歧义。它可以被减少为Separator,或者它可以与接下来的}和"输入字符一起被减少为StringTemplateEnd。由于语法文法将提供数组创建表达式的ArrayInitializer的上下文(15.10.1),上述规则确保将输入字符}减少为Separator。剩下的}和"输入字符将被减少为StringTemplateEnd。
第7章:包和模块
7.3 编译单元
CompilationUnit是Java程序的语法文法(2.3)的目标符号(2.1)。它由以下产生式定义:
- CompilationUnit:
- OrdinaryCompilationUnit
- ModularCompilationUnit
- OrdinaryCompilationUnit:
- [PackageDeclaration] {ImportDeclaration} {TopLevelClassOrInterfaceDeclaration}
- ModularCompilationUnit:
- {ImportDeclaration} ModuleDeclaration
普通编译单元由三个部分组成,每个部分都是可选的:
-
package声明(7.4),给出编译单元所属包的完全限定名称(6.7)。没有
package声明的编译单元属于一个未命名包(7.4.2)。 -
import声明(7.5),允许引用其他包中的类和接口,以及类和接口的static成员,使用它们的简单名称。 -
类和接口的顶层声明(7.6)。
模块编译单元由一个module声明(7.7)组成,可选地在前面有import声明。这些import声明允许在module声明内使用它们的简单名称引用来自此模块和其他模块的包中的类和接口,以及static类和接口的成员。
每个编译单元隐式导入
- 每个在预定义包
java.lang中声明的public类或接口,就好像在每个编译单元的开头出现了声明import java.lang.*;,紧随任何package声明之后。
- 在预定义类
StringTemplate中声明的静态成员STR,就好像在每个编译单元的开头出现了声明import static java.lang.StringTemplate.STR;,紧随任何package声明之后。
因此,所有
那些隐式导入的
类和接口类、接口和静态字段的名称在每个编译单元中都可以作为简单名称使用。
主机系统确定哪些编译单元是可观察的,除了预定义包java及其子包lang和io中的编译单元始终是可观察的。
§7.3的其余部分保持不变。
7.5 导入声明
7.5.3 单静态导入声明
一个单静态导入声明从一个类或接口中导入所有可访问的具有给定简单名称的static成员。这使得这些static成员在单静态导入声明所在的模块、类和接口声明中可以使用其简单名称。
- SingleStaticImportDeclaration:
-
importstaticTypeName.Identifier;
TypeName必须是一个类或接口的规范名称(6.7)。
该类或接口必须是命名包的成员,或者是一个类或接口的成员,其最外层词法封闭的类或接口声明(8.1.3)是命名包的成员,否则将出现编译时错误。
如果命名的类或接口不可访问(6.6),则会出现编译时错误。
Identifier必须命名命名类或接口的至少一个static成员。如果没有该名称的static成员,或者所有命名的成员都不可访问,则会出现编译时错误。
一个单静态导入声明可以导入具有相同名称的多个字段、类或接口,或者具有相同名称和签名的多个方法。当命名的类或接口从其自身的超类型继承多个具有相同名称的字段、成员类、成员接口或方法时,就会发生这种情况。
允许单静态导入声明冗余地导入已经隐式导入的static成员。
如果同一编译单元中的两个单静态导入声明尝试导入具有相同简单名称的类或接口,则会出现编译时错误,除非这两个类或接口相同,此时重复声明将被忽略。
如果单静态导入声明导入一个简单名称为x的类或接口,并且编译单元还声明了一个顶级类或接口(7.6),其简单名称为x,则会出现编译时错误。
如果一个编译单元既包含导入一个简单名称为x的类或接口的单静态导入声明,又包含一个单类型导入声明(7.5.1),导入一个简单名称为x的类或接口,则会出现编译时错误,除非这两个类或接口相同,此时重复声明将被忽略。
7.5.4 静态导入所有声明
一个静态导入所有声明允许根据需要导入一个命名类或接口的所有可访问的static成员。
- StaticImportOnDemandDeclaration:
-
importstaticTypeName.*;
TypeName必须是一个类或接口的规范名称(6.7)。
该类或接口必须是命名包的成员,或者是一个类或接口的成员,其最外层词法封闭的类或接口声明(8.1.3)是命名包的成员,否则将出现编译时错误。
如果命名的类或接口不可访问(6.6),则会出现编译时错误。
允许静态导入所有声明冗余地导入已经隐式导入的static成员。
同一编译单元中的两个或更多静态导入所有声明可以命名相同的类或接口;效果就好像确实有一个这样的声明。
§7.5.4的其余部分保持不变。
第12章:执行
12.5 创建新类实例
当对类实例创建表达式(15.9)进行评估导致实例化类时,将显式创建一个新的类实例。
在以下情况下可能隐式创建一个新的类实例:
-
加载包含字符串文字(3.10.5)或文本块(3.10.6)的类或接口可能创建一个新的
String对象,用于表示由字符串文字或文本块表示的字符串。(如果表示与字符串文字或文本块表示的Unicode代码点序列相同的String实例已经被interned,则不会发生对象创建。) -
执行导致装箱转换(5.1.7)的操作。装箱转换可能会创建与原始类型之一关联的包装类(
Boolean、Byte、Short、Character、Integer、Long、Float、Double)的新对象。 -
执行不是常量表达式的字符串连接运算符
+(15.18.1)总是创建一个新的String对象来表示结果。字符串连接运算符还可能为原始类型的值创建临时包装对象。 -
评估方法引用表达式(15.13.3)或lambda表达式(15.27.4)可能需要创建一个实现函数式接口类型(9.8)的类的新实例。
- 评估模板表达式(15.8.6)可能需要创建一个实现函数式接口类型
StringTemplate的类的新实例。
这些情况中的每一个都确定要调用的特定构造函数(8.8),并在类实例创建过程中为其分配指定参数(可能没有)的内存空间。
每当创建一个新的类实例时,都会为其分配内存空间,以容纳类中声明的所有实例变量以及类的每个超类中声明的所有实例变量,包括可能被隐藏的所有实例变量(8.3)。
如果没有足够的空间可用来为对象分配内存,则创建类实例将以OutOfMemoryError异常突然完成。否则,新对象中的所有实例变量,包括在超类中声明的变量,都将被初始化为它们的默认值(4.12.5)。
在将新创建的对象的引用作为结果返回之前,将处理指定构造函数以使用以下过程初始化新对象:
-
将构造函数的参数分配给此构造函数调用的新创建的参数变量。
-
如果此构造函数以同一类中的另一个构造函数的显式构造函数调用(8.8.7.1)(使用
this)开头,则评估参数并递归地处理该构造函数调用,使用相同的五个步骤。如果该构造函数调用突然完成,则出于同样的原因,此过程也会突然完成;否则,继续进行第5步。 -
此构造函数不以同一类中的另一个构造函数的显式构造函数调用(使用
this)开头。如果此构造函数不是为Object类而设计的,则此构造函数将以超类构造函数的显式或隐式调用(使用super)开头。评估参数并递归地处理该超类构造函数调用,使用相同的五个步骤。如果该构造函数调用突然完成,则出于同样的原因,此过程也会突然完成。否则,继续进行第4步。 -
执行此类的实例初始化程序和实例变量初始化程序,将实例变量初始化程序的值分配给源代码中以文本方式按从左到右的顺序出现的相应实例变量。如果执行任何这些初始化程序导致异常,则不会处理更多的初始化程序,并且此过程将以相同的异常突然完成。否则,继续进行第5步。
-
执行此构造函数的其余部分。如果执行完成突然完成,则出于同样的原因,此过程也会突然完成。否则,此过程正常完成。
与C++不同,Java编程语言在创建新类实例期间不指定方法分派的修改规则。如果在初始化对象时调用了被子类覆盖的方法,则会使用这些覆盖方法,即使在新对象完全初始化之前也是如此。
示例 12.5-1. 实例创建的评估
class Point {
int x, y;
Point() { x = 1; y = 1; }
}
class ColoredPoint extends Point {
int color = 0xFF00FF;
}
class Test {
public static void main(String[] args) {
ColoredPoint cp = new ColoredPoint();
System.out.println(cp.color);
}
}
在这里,创建了一个新的ColoredPoint实例。首先,为新的ColoredPoint分配空间,以容纳字段x、y和color。然后,将所有这些字段初始化为它们的默认值(在本例中,每个字段的默认值为0)。接下来,首先调用没有参数的ColoredPoint构造函数。由于ColoredPoint没有声明构造函数,因此会隐式声明以下形式的默认构造函数:
ColoredPoint() { super(); }
然后,此构造函数调用没有参数的Point构造函数。Point构造函数不以构造函数的调用开始,因此Java编译器提供了一个隐式调用其无参数的超类构造函数,就好像它已经被写成:
Point() { super(); x = 1; y = 1; }
因此,调用了没有参数的Object构造函数。
Object类没有超类,因此递归在此处终止。接下来,调用Object的任何实例初始化程序和实例变量初始化程序。接下来,执行没有参数的Object构造函数的主体。在Object中未声明这样的构造函数,因此Java编译器提供了一个默认的构造函数,在这种特殊情况下是:
Object() { }
此构造函数执行无效果并返回。
接下来,执行Point类的实例变量的所有初始化程序。由于x和y的声明没有提供任何初始化表达式,因此此示例的此步骤不需要任何操作。然后执行Point构造函数的主体,将x设置为1,将y设置为1。
接下来,执行ColoredPoint类的实例变量的初始化程序。此步骤将值0xFF00FF分配给color。最后,执行ColoredPoint构造函数的其余部分(在调用super之后);在此部分中实际上没有语句,因此不需要进一步操作,初始化完成。
示例 12.5-2. 实例创建期间的动态分派
class Super {
Super() { printThree(); }
void printThree() { System.out.println("three"); }
}
class Test extends Super {
int three = (int)Math.PI; // 即,3
void printThree() { System.out.println(three); }
public static void main(String[] args) {
Test t = new Test();
t.printThree();
}
}
此程序产生输出:
0
3
这表明在Super类的构造函数中调用printThree不会调用Super类中printThree的定义,而是调用Test类中printThree的覆盖定义。因此,该方法在Test的字段初始化程序执行之前运行,这就是为什么第一个输出值为0,即Test的three字段初始化为的默认值。稍后在main方法中调用printThree时,会调用相同的printThree定义,但在那时实例变量three的初始化程序已经执行,因此打印出值3。
第15章:表达式
15.8 主要表达式
主要表达式包括大多数最简单的表达式类型,所有其他表达式都是由它们构建的:字面量、对象创建、字段访问、方法调用、方法引用、数组访问和模板表达式。带括号的表达式在语法上也被视为主要表达式。
- 主要:
- 非新数组主要
- 数组创建表达式
- 非新数组主要:
- 字面量
- 类字面量
-
this -
类型名称
.this -
(表达式) - 类实例创建表达式
- 字段访问
- 数组访问
- 方法调用
- 方法引用
- 模板表达式
Java编程语言的这部分语法在两个方面是不寻常的。首先,人们可能期望简单名称,例如局部变量和方法参数的名称,是主要表达式。出于技术原因,当引入后缀表达式时(15.14),名称稍后与主要表达式一起分组。
技术原因与允许仅具有一个令牌前瞻的左到右解析Java程序有关。考虑表达式
(z[3])和(z[])。第一个是带括号的数组访问(15.10.3),第二个是强制转换的开始(15.16)。在前瞻符号为[时,左到右解析将z减少为非终结符Name。在强制转换的上下文中,我们不希望将名称减少为Primary,但如果Name是Primary的替代方案之一,那么我们无法确定是否进行减少(也就是说,我们无法确定当前情况是否会变成带括号的数组访问或强制转换),而不查看两个令牌的前瞻,即[后面的令牌。这里提供的语法通过保持Name和Primary分开,并允许在某些其他语法规则中使用任一者(ClassInstanceCreationExpression、MethodInvocation、ArrayAccess和PostfixExpression,尽管不包括FieldAccess,因为它直接使用标识符)。这种策略有效地推迟了是否应将Name视为Primary的问题,直到可以检查更多上下文。
第二个不寻常的特征避免了表达式"
new int[3][3]"中的潜在语法歧义,该表达式在Java中始终表示创建多维数组的单个操作,但如果没有适当的语法技巧,也可能被解释为与"(new int[3])[3]"相同的含义。
通过将Primary的预期定义分为Primary和PrimaryNoNewArray来消除此歧义。(这可以与将Statement分为Statement和StatementNoShortIf(14.5)以避免"悬挂else"问题进行比较。)
15.8.1 词法字面量
字面量(3.10)表示一个固定的、不变的值。
以下来自3.10的产生式仅供参考:
- 字面量:
- 整数字面量
- 浮点数面量
- 布尔字面量
- 字符字面量
- 字符串字面量
- 文本块
- 空字面量
字面量的类型确定如下:
-
以
L或l(小写L)结尾的整数字面量(3.10.1)的类型为long(4.2.1)。任何其他整数字面量的类型为
int(4.2.1)。 -
以
F或f结尾的浮点数面量(3.10.2)的类型为float(4.2.3)。任何其他浮点数面量的类型为
double(4.2.3)。
整数字面量、浮点数面量、布尔字面量或字符字面量的求值结果为字面量是源代码表示的值。字符串字面量或文本块的求值结果为String类的实例,如3.10.5和3.10.6中所述。空字面量的求值结果为null引用。
对字面值的评估总是正常完成。
15.8.6 模板表达式
一个模板表达式提供了一种将文字文本与表达式的值结合的通用方法。文本和表达式由一个模板指定。将文本与表达式的值结合的任务被委托给一个模板处理器。
将文本和值简单插值到一个
String中,可以从预定义的模板处理器STR(7.3)中获得。其他模板处理器可以以任意方式组合文本和值,以产生比String更复杂类型的结果。
- 模板表达式:
-
模板处理器
.模板参数 - 模板处理器:
- 表达式
- 模板参数:
- 模板
- String文本
- 文本块
- 模板:
- String模板
- 文本块模板
- String模板:
-
String模板开始 嵌入表达式
{ String模板中部 嵌入表达式 } String模板结束 - 文本块模板:
-
文本块模板开始 嵌入表达式
{ 文本块模板中部 嵌入表达式 } 文本块模板结束 - 嵌入表达式:
- [ 表达式 ]
以下来自3.13的产生式仅供参考:
- String模板开始:
"String片段\{- String模板中部:
}String片段\{- String模板结束:
}String片段"- String片段:
- { String字符 }
- 文本块模板开始:
"""文本块片段\{- 文本块模板中部:
}文本块片段\{- 文本块模板结束:
}文本块片段"""- 文本块片段:
- { 文本块字符 }
模板可以是字符串模板或文本块模板。字符串模板(分别是文本块模板)类似于字符串文字(文本块),但包含一个或多个嵌入表达式,这些表达式以字符序列\{为前缀,以字符}为后缀。如果字符序列\{和}之间没有任何内容,则嵌入表达式被隐式视为null文字(3.10.8)。
具有n个嵌入表达式(n>0)的字符串模板由n+1个片段与n个嵌入表达式的交替插入组成。第一个片段是一个String模板开始标记(3.13);接下来的n-1个片段是String模板中部标记;最后一个片段是一个String模板结束标记。
以下是一些字符串模板的拆分:
"\{42} is the answer."由一个String模板开始标记("\{)组成,后跟表达式42(一个整数字面值),后跟String模板结束标记(} is the answer.")。字符串模板"The answer is \{x+y}!"由一个String模板开始标记("The answer is \{)组成,后跟表达式x+y,后跟String模板结束标记(}!")。字符串模板"Hello \{name} from \{address.city},"由一个String模板开始标记("Hello \{)组成,后跟表达式name,后跟String模板中部标记(} from \{),后跟表达式address.city,后跟String模板结束标记(},")。最后,字符串模板"Customer name: \{}"由一个String模板开始标记("Customer name: \{)组成,后跟(隐式的)表达式null,后跟String模板结束标记(}")。
具有n个嵌入表达式(n>0)的文本块模板由n+1个片段与n个嵌入表达式的交替插入组成。第一个片段是一个TextBlock模板开始标记(3.13);接下来的n-1个片段是TextBlock模板中部标记;最后一个片段是一个TextBlock模板结束标记。
模板的片段字符串表示围绕嵌入表达式的文字文本。片段字符串的确定如下:
-
具有n个嵌入表达式和n+1个片段的字符串模板有n+1个片段字符串,其中每个片段字符串是相应片段的内容(3.13)的内容,每个转义序列都被解释,就好像通过在内容上执行
String.translateEscapes一样。 -
具有n个嵌入表达式和n+1个片段的文本块模板的片段字符串如下确定:
-
文本块模板的字符串内容是按照以下步骤的顺序给出的字符序列:
-
TextBlock模板开始的内容,后跟字符序列
\{。 -
对于每个TextBlock模板中部,以字符
}开头的字符序列,后跟TextBlock模板中部的内容,后跟字符序列\{。 -
以字符
}开头的字符序列,后跟TextBlock模板结束的内容。
-
-
然后通过按顺序应用以下步骤来进一步转换字符串内容:
-
删除所有附带的空格,就好像在字符串内容的字符上执行
String.stripIndent一样。 -
解释每个转义序列,就好像在步骤1生成的字符上执行
String.translateEscapes一样。
-
-
片段字符串s1,...,sn+1从字符串内容派生如下:
-
s1是其内容从步骤2生成的字符串内容的开始,直到第一个出现的字符序列
\{}之前的字符串。 -
si(2 ≤ i ≤ n)是其内容从步骤2生成的字符串内容中第(i-1)次出现的字符序列
\{}之后立即开始的字符序列,直到第i次出现的字符序列\{}之前的字符串。 -
sn+1是其内容从步骤2生成的字符串内容中第n次出现的字符序列
\{}之后立即开始的字符序列,直到字符串内容的结尾之前的字符串。
-
-
例如,字符串模板
"\{42} is the answer."的片段字符串首先是空字符串,然后是字符串" is the answer."。字符串模板"The answer is \{x+y}!"的片段字符串是字符串"The answer is ",后跟"!"。字符串模板"Hello \{name} from \{address.city},"的片段字符串是字符串"Hello ",后跟" from ",后跟","。最后,字符串模板"Customer name: \{}"的片段字符串首先是字符串"Customer name: ",然后是空字符串。文本块模板的片段字符串
""" Name: \{customerName}"""首先是内容为六个字符序列
Name:LF的字符串,后跟空字符串。
模板处理器表达式的类型TP必须是StringTemplate.Processor的子类型,否则会出现编译时错误。如果TP是StringTemplate.Processor<Result,Exc>的子类型,其中Result和Exc是参数化类型的类型参数,则模板表达式的类型是Result。如果TP是原始类型StringTemplate.Processor的子类型,则模板表达式的类型是Object。
StringTemplate.Processor<R,E>是一个泛型函数接口(9.8),其单个abstract方法process具有一个StringTemplate形式参数,一个返回类型R,以及一个带有类型E的throws子句。
在模板中出现的任何嵌入表达式的类型没有限制。
示例 15.8.6-1. 简单模板
以下简单示例利用了每个编译单元中隐式导入的StringTemplate的static成员STR,实现了简单的字符串插值。
// 带有简单嵌入字符串变量的字符串模板
String firstName = "Joan";
String lastName = "Smith";
String fullName = STR."\{firstName} \{lastName}";
// 带有嵌入整数表达式的字符串模板
int x = 10, y = 20;
String s1 = STR."\{x} + \{y} = \{x + y}";
// 嵌入表达式可以调用方法和访问字段
String s2 = STR."您有一个等待您的\{getOfferType()}!";
String s3 = STR."在\{req.date} \{req.time}从\{req.ipAddress}访问";
// 一个文本块模板,模拟带有嵌入表达式的HTML元素
String title = "我的网页";
String text = "你好,世界";
String html = STR."""
<html>
<head>
<title>\{title}</title>
</head>
<body>
<p>\{text}</p>
</body>
</html>
""";
// 一个文本块模板,模拟带有嵌入表达式的JSON值
String name = "Joan Smith";
String phone = "555-123-4567";
String address = "1 Maple Drive, Anytown";
String json = STR."""
{
"name": "\{name}",
"phone": "\{phone}",
"address": "\{address}"
}
""";
在运行时,模板表达式的评估如下:
-
评估TemplateProcessor表达式。如果结果值为null,则抛出
NullPointerException,并且整个模板表达式因此突然完成。如果TemplateProcessor的评估突然完成,则整个模板表达式也因同样的原因突然完成。 -
如果TemplateArgument是StringLiteral或TextBlock,则此步骤的结果是一个
StringTemplate的实例,就像通过调用带有TemplateArgument参数的static方法StringTemplate.of一样产生。如果TemplateArgument是Template,则嵌入表达式e1,...,en(n > 0)被评估以产生嵌入值,v1,...,vn。嵌入表达式按照它们在Template中出现的顺序从左到右进行评估。如果任何嵌入表达式的评估突然完成,则整个模板表达式也因同样的原因突然完成。
否则,此步骤的结果是指向具有以下属性的类实例的引用:
-
该类实现了
StringTemplate接口。 -
实例方法
values返回一个包含嵌入值v1,...,vn的java.util.List实例,按顺序排列。 -
实例方法
fragments返回一个包含模板的片段字符串的java.util.List实例,按顺序排列。 -
类实例的
interpolate实例方法返回(1)模板的片段字符串,按顺序严格交替连接,以及(2)嵌入值v1,...,vn,按顺序排列,从第一个片段字符串开始。
-
-
评估模板表达式的结果,就像在步骤1的结果上调用
process方法一样,参数为步骤2的结果。如果此方法调用突然完成,则整个模板表达式也因同样的原因突然完成。
也就是说,模板表达式的含义:
e."..." // "..."是有效的字符串模板等同于方法调用表达式的含义:
e.process(t)其中
t指的是封装了模板中文本和嵌入表达式值的StringTemplate实例。
示例 15.8.6-2. 简单模板处理器
StringTemplate的interpolate方法提供了一种方便的方式来连接模板的片段字符串和嵌入值以生成一个字符串。在以下示例中,UPPER既插值给定的模板,又将所有字母转换为大写。
StringTemplate.Processor<String, RuntimeException> UPPER = (StringTemplate st) ->
st.interpolate().toUpperCase();
String name = "Joan";
String result = UPPER."我的名字是\{name}";
执行这些语句后,result将被初始化为字符串"我的名字是JOAN"。
示例 15.8.6-3. 更复杂的模板处理器
更复杂的模板处理器可以使用以下简单的编程模式。在以下示例中,MY_UPPER_STRINGS首先将片段字符串(由fragments方法返回)转换为大写,然后使用values方法返回的嵌入值,使用interpolate方法返回一个字符串结果。
StringTemplate.Processor<String, RuntimeException> MY_UPPER_STRINGS = (StringTemplate st) -> {
List<String> fragments = st.fragments()
.stream()
.map(String::toUpperCase)
.toList();
List<Object> values = st.values();
return StringTemplate.interpolate(fragments, values);
};
String name = "Joan";
String result = MY_UPPER_STRINGS."我的名字是\{name}";
执行这些语句后,result将被初始化为字符串"我的名字是Joan"。
在以下示例中,MY_UPPER_VALUES将嵌入表达式转换为大写字符串(注意处理任何空值),然后进行插值。
StringTemplate.Processor<String, RuntimeException> MY_UPPER_VALUES = (StringTemplate st) -> {
List<String> values = st.values()
.stream()
.map((o) -> (o==null)?"":o.toString().toUpperCase())
.toList();
return StringTemplate.interpolate(st.fragments(), values);
};
String title = null;
String firstName = "Joan";
String familyName = "Smith";
String result = MY_UPPER_VALUES."欢迎\{title}\{firstName} \{familyName}";
执行这些语句后,result将被初始化为字符串"欢迎JOAN SMITH"。
示例 15.8.6-4. 不返回字符串的模板处理器
可以处理模板并返回字符串以外的值。在以下示例中,JSON返回一个JSONObject类的实例,而不是一个字符串。
StringTemplate.Processor<JSONObject, RuntimeException> JSON =
(StringTemplate st) -> new JSONObject(st.interpolate());
String name = "Joan Smith";
String phone = "555-123-4567";
String address = "1 Maple Drive, Anytown";
JSONObject doc = JSON."""
{
"name": "\{name}",
"phone": "\{phone}",
"address": "\{address}"
}
""";
示例 15.8.6-6. 可能引发异常的模板处理器
有时验证给定模板并在模板不符合要求时引发异常是有用的。
在以下示例中,JSON_VALIDATE将给定模板转换为JSONObject类的实例,但首先检查中间插值的字符串是否以匹配的大括号开头和结尾(忽略任何前导或尾随空格)。如果这些检查中的任何一个失败,则抛出JSONException,否则返回相应的JSONObject实例。
StringTemplate.Processor<JSONObject, JSONException> JSON_VALIDATE = (StringTemplate st) -> {
String stripped = st.interpolate().strip();
if (!stripped.startsWith("{") || !stripped.endsWith("}")) {
throws new JSONException("Missing brace");
}
return new JSONObject(stripped);
};
String name = "Joan Smith";
String phone = "555-123-4567";
String address = "1 Maple Drive, Anytown";
try {
JSONObject doc = JSON_VALIDATE."""
{
"name": "\{name}",
"phone": "\{phone}",
"address": "\{address}"
}
""";
} catch (JSONException ex) {
...
}