本文档描述了对Java语言规范的更改,以支持未命名模式和变量,这是Java SE 21的预览功能。有关该功能的概述,请参阅JEP:443。
更改是针对JLS的现有部分描述的。新文本显示为这样,删除的文本显示为
这样。必要时,解释和讨论将放在灰色框中。
变更日志:
2023-03-22:初始规范草案。
第3章:词法结构
3.8 标识符
标识符是一个无限长度的Java字母和Java数字序列,其中第一个必须是一个Java字母。
- 标识符:
- 标识符字符 但不能是 关键字 或 布尔文字 或 空文字
- 标识符字符:
- Java字母 {Java字母或数字}
- Java字母:
- 任何Unicode字符,它是一个“Java字母”
- Java字母或数字:
- 任何Unicode字符,它是一个“Java字母或数字”
“Java字母”是一个方法Character.isJavaIdentifierStart(int)
返回true的字符。
“Java字母或数字”是一个方法Character.isJavaIdentifierPart(int)
返回true的字符。
“Java字母”包括大写和小写ASCII拉丁字母
A-Z
(\u0041-\u005a
),和a-z
(\u0061-\u007a
),以及出于历史原因,ASCII美元符号($
,或\u0024
)和下划线(_
,或\u005f
)。美元符号应仅在机械生成的源代码中使用,或者在传统系统上访问预先存在的名称时偶尔使用。下划线可以用于由两个或更多字符组成的标识符,但由于是关键字,不能用作单字符标识符(3.9)。
“Java数字”包括ASCII数字
0-9
(\u0030-\u0039
)。
字母和数字可以来自整个Unicode字符集,该字符集支持当今世界上大多数使用的书写脚本,包括用于中文、日文和韩文的大型集合。这使程序员可以在其程序中使用用其母语编写的标识符。
仅当忽略可忽略字符后,标识符的每个字母或数字具有相同的Unicode字符时,两个标识符才相同。可忽略字符是一个方法Character.isIdentifierIgnorable(int)
返回true的字符。外观相同的标识符可能是不同的。
例如,由单个字母组成的标识符 LATIN CAPITAL LETTER A(
A
,\u0041
),LATIN SMALL LETTER A(a
,\u0061
),GREEK CAPITAL LETTER ALPHA(A
,\u0391
),CYRILLIC SMALL LETTER A(a
,\u0430
)和MATHEMATICAL BOLD ITALIC SMALL A(a
,\ud835\udc82
)都是不同的。Unicode组合字符与其规范等效的分解字符不同。例如,拉丁大写字母A ACUTE(
Á
,\u00c1
)与紧随其后的非间距ACUTE(´
,\u0301
)在标识符中是不同的。请参阅Unicode标准,第3.11节“规范化形式”。
标识符的示例包括:
String
i3
- αρετη
MAX_VALUE
isLetterOrDigit
标识符永远不会与保留关键字(3.9)、布尔文字(3.10.3)或空文字(3.10.8)具有相同的拼写(Unicode字符序列),这是由于标记化规则(3.5)的规定。但是,标识符可能与上下文关键字具有相同的拼写,因为将输入字符序列标记化为标识符或上下文关键字取决于序列在程序中出现的位置。
为了便于识别上下文关键字,语法语法(2.3)有时通过定义接受仅标识符的子集来禁止某些标识符。子集如下:
- 类型标识符:
-
标识符 但不能是
permits
、record
、sealed
、var
或yield
- 未限定方法标识符:
-
标识符 但不能是
yield
类型标识符用于类、接口和类型参数的声明(8.1、9.1、4.4),以及引用类型时(6.5)。例如,类的名称必须是一个类型标识符,因此不允许声明名为
permits
、record
、sealed
、var
或yield
的类。
未限定方法标识符用于方法调用表达式通过其简单名称引用方法时(6.5.7.1)。由于术语
yield
被排除在未限定方法标识符之外,因此必须对名为yield
的方法的任何调用进行限定,从而区分调用和yield
语句(14.21)。
3.9 关键字
以下更改假定由JEP 440(记录模式)和JEP 441(switch
的模式匹配)导致的JLS更改已应用(JLS:JEP440+441)。
由ASCII字符组成的51个字符序列被保留用作关键字,不能用作标识符(3.8)。另外,由ASCII字符组成的另外16个字符序列,根据它们出现的上下文,可以被解释为关键字或其他标记。
- 关键字:
- 保留关键字
- 上下文关键字
- 保留关键字:
- (其中之一)
-
abstract continue for new switch
assert default if package synchronized
boolean do goto private this
break double implements protected throw
byte else import public throws
case enum instanceof return transient
catch extends int short try
char final interface static void
class finally long strictfp volatile
const float native super while
_
(下划线) - 上下文关键字:
- (其中之一)
-
exports permits sealed var
module provides to when
non-sealed record transitive with
open requires uses yield
opens
关键字
const
和goto
被保留,即使它们目前未被使用。这可以让Java编译器在程序中错误地出现这些C++关键字时产生更好的错误消息。
关键字
strictfp
已过时,不应在新代码中使用。
关键字
_
(下划线)被保留,以备将来在参数声明中使用。
关键字
_
(下划线)可以在声明中代替标识符使用(6.1)。
true
和false
不是关键字,而是布尔文字(3.10.3)。
null
不是关键字,而是空文字(3.10.8)。
在将输入字符减少为输入元素时(3.5),如果概念上匹配上下文关键字的输入字符序列仅在以下两个条件都满足时才减少为上下文关键字:
-
该序列被识别为语法文法适当上下文中的终结符(2.3),如下所示:
-
对于
module
和open
,当被识别为ModuleDeclaration中的终结符时(7.7)。 -
对于
exports
、opens
、provides
、requires
、to
、uses
和with
,当被识别为ModuleDirective中的终结符时。 -
对于
transitive
,当被识别为RequiresModifier中的终结符时。例如,识别序列
requires
transitive
;
并不使用RequiresModifier,因此术语transitive
在此被减少为标识符而不是上下文关键字。 -
对于
var
,当被识别为LocalVariableType(14.4)或LambdaParameterType(15.27.1)中的终结符时。在其他上下文中,尝试将
var
用作标识符将导致错误,因为var
不是TypeIdentifier(3.8)。 -
对于
yield
,当被识别为YieldStatement中的终结符时(14.21)。在其他上下文中,尝试将
yield
用作标识符将导致错误,因为yield
既不是TypeIdentifier也不是UnqualifiedMethodIdentifier。 -
对于
record
,当被识别为RecordDeclaration中的终结符时(8.10)。 -
对于
non-sealed
、permits
和sealed
,当被识别为NormalClassDeclaration(8.1)或NormalInterfaceDeclaration(9.1)中的终结符时。 -
对于
when
,当被识别为Guard中的终结符时(14.11.1)。
-
-
该序列的前一个字符和后一个字符不是JavaLetterOrDigit匹配的输入字符。
通常,在源代码中意外省略空格将导致一系列输入字符被标记为标识符,这是由于“最长可能转换”规则(3.2)。例如,十二个输入字符的序列
p u b l i c s t a t i c
总是被标记为标识符publicstatic
,而不是保留关键字public
和static
。如果意图是两个标记,它们必须用空格或注释分隔。
上述规则与“最长可能转换”规则共同作用,以在可能出现上下文关键字的情况下产生直观的结果。例如,十一个输入字符的序列
v a r f i l e n a m e
通常被标记为标识符varfilename
,但在局部变量声明中,前三个输入字符被第一个规则条件暂时识别为上下文关键字var
。然而,忽略序列中缺少空格会导致下一个八个输入字符被识别为标识符filename
。 (这意味着序列在不同上下文中经历不同的标记化过程:在大多数上下文中是标识符,但在局部变量声明中是上下文关键字和标识符。)因此,第二个条件阻止了对上下文关键字var
的识别,理由是紧随其后的输入字符f
是JavaLetterOrDigit。因此,在局部变量声明中,序列v a r f i l e n a m e
被标记为标识符varfilename
。
作为对上下文关键字的仔细识别的另一个例子,请考虑十五个输入字符的序列
n o n - s e a l e d c l a s s
。该序列通常被转换为三个标记 - 标识符non
、运算符-
和标识符sealedclass
- 但在普通类声明中,第一个条件成立,前十个输入字符被暂时识别为上下文关键字non-sealed
。为避免将序列转换为两个关键字标记(non-sealed
和class
)而不是三个非关键字标记,并且避免因省略class
前的空格而奖励程序员,第二个条件阻止了上下文关键字的识别。因此,在类声明中,序列n o n - s e a l e d c l a s s
被标记为三个标记。
在上述规则中,第一个条件取决于语法文法的细节,但Java编程语言的编译器可以在不完全解析输入程序的情况下实现该规则。例如,可以使用启发式方法来跟踪标记化器的上下文状态,只要该启发式方法保证上下文关键字的有效使用被标记为关键字,标识符的有效使用被标记为标识符。或者,编译器可以始终将上下文关键字标记为标识符,留待后续阶段识别这些标识符的特殊用途。
第6章:名称
6.1 声明
一个声明将一个实体引入程序,并包括一个标识符(3.8),该标识符可用于名称中引用此实体。当引入的实体是类、接口或类型参数时,标识符受限以避免某些上下文关键字。
声明的实体是以下之一:
一个声明将一个实体引入程序,以下之一:
-
一个模块,在
module
声明中声明(7.7) -
一个包,在
package
声明中声明(7.4) -
一个枚举常量(8.9.1)
-
一个记录组件(8.10.3)
-
一个形式参数,以下之一:
-
一个异常处理程序中声明的异常参数,在
try
语句的catch
子句中声明(14.20) -
一个局部变量,以下之一:
-
一个局部类或接口(14.3),以下之一:
-
由普通类声明的局部类声明
-
由枚举声明的局部类声明
-
由记录声明的局部类声明
-
由普通接口声明的局部接口声明
-
构造函数(8.8,8.10.4)也是通过声明引入的,但使用声明它们所在类的名称,而不是引入新名称。
声明通常包括一个标识符(3.8),可用于名称中引用已声明的实体。当引入的实体是类、接口或类型参数时,标识符受限以避免某些上下文关键字。
如果声明不包括标识符,而是包括保留关键字_
(下划线),则该实体不能通过名称引用。以下种类的实体可以使用下划线声明:
使用下划线声明的局部变量、异常参数或lambda参数称为未命名局部变量、未命名异常参数或未命名lambda参数。
泛型类或接口的声明(
class
C<
T>
...
或interface
C<
T>
...
)引入了一个名为C的类和一组类型:原始类型C,参数化类型C<Foo>
,参数化类型C<Bar>
等。
当在不重要的泛型上下文中出现对C的引用时,如下所示标识为非泛型上下文之一,对C的引用表示类或接口C。在其他上下文中,对C的引用表示由C引入的类型或类型的一部分。
15个非泛型上下文如下:
前十二个非泛型上下文对应于TypeName中的前十二个句法上下文[6.5.1]。第十三个非泛型上下文是指一个限定的ExpressionName,例如
C.x
可能包括一个TypeNameC
来表示静态成员访问。在这十三个上下文中普遍使用TypeName是重要的:它表明这些上下文涉及对类型的非一流使用。相比之下,第十四和第十五个非泛型上下文使用ClassType,表明throws
和catch
子句以一流方式使用类型,与例如字段声明一致。将这两个上下文描述为非泛型的原因是异常类型不能被参数化的事实(8.1.2)。
请注意,ClassType生成允许注解,因此可以对
throws
或catch
子句中类型的使用进行注释,而TypeName生成不允许注解,因此不可能对类型的名称进行注释,例如在单类型导入声明中。
命名约定
Java SE平台的类库尽可能使用以下约定选择的名称。这些约定有助于使代码更易读,并避免某些名称冲突。
我们建议在Java编程语言中编写的所有程序中使用这些约定。但是,如果长期以来的传统用法要求不同,就不应盲目遪从这些约定。例如,
java.lang.Math
类的sin
和cos
方法具有数学上的传统名称,尽管这些方法名称违反了这里建议的约定,因为它们很短并且不是动词。
包名和模块名
程序员应采取措施避免两个发布的包具有相同的名称,方法是为广泛分发的包选择唯一包名。这样可以轻松自动地安装和编目包。本节指定了生成这种唯一包名的建议约定。鼓励Java SE平台的实现提供自动支持,将一组包从本地和临时包名称转换为此处描述的唯一名称格式。
如果不使用唯一包名,则可能会在冲突包的创建点远处出现包名冲突。这可能会导致用户或程序员难以解决的情况。在这些情况下,可以使用
ClassLoader
和ModuleLayer
来使具有相同名称的包相互隔离,但这并不透明地对一个天真的程序进行。
您可以通过首先拥有(或隶属于拥有)互联网域名,例如
oracle.com
,来形成唯一包名。然后,逐个组件地反转此名称,以获得在此示例中为com.oracle
,并将其用作包名的前缀,使用您的组织内开发的约定进一步管理包名。这样的约定可能会指定某些包名组件为部门、部门、项目、机器或登录名。
示例 6.1-1. 唯一包名
com.nighthacks.scrabble.dictionary
org.openjdk.compiler.source.tree
net.jcip.annotations
edu.cmu.cs.bovik.cheese
gov.whitehouse.socks.mousefinder
唯一包名的第一个组件始终以全小写ASCII字母编写,并且应为顶级域名之一,例如
com
、edu
、gov
、mil
、net
或org
,或者根据ISO标准3166指定的国家的英文两字代码之一。
在某些情况下,互联网域名可能不是有效的包名。以下是处理这些情况的建议约定:
模块的名称应与其主要导出包的名称相对应。如果模块没有这样的包,或者出于传统原因必须具有与其导出包之一不对应的名称,则其名称仍应以作者关联的互联网域的反转形式开头。
示例 6.1-2. 唯一模块名
com.nighthacks.scrabble
org.openjdk.compiler
net.jcip.annotations
包或模块名称的第一个组件不得是标识符
java
。以标识符java
开头的包和模块名称保留供Java SE平台的包和模块使用。
包或模块的名称并不意味着包或模块存储在互联网上的位置。例如,名为
edu.cmu.cs.bovik.cheese
的包不一定可以从主机cmu.edu
或cs.cmu.edu
或bovik.cs.cmu.edu
获取。生成唯一包和模块名称的建议约定仅仅是在现有广泛知名的唯一名称注册表上附加包和模块命名约定的一种方式,而不必为包和模块名称创建单独的注册表。
类和接口名称
类的名称应为描述性名词或名词短语,不要过长,采用混合大小写,每个单词的首字母大写。
示例 6.1-3. 描述性类名
`ClassLoader`
SecurityManager
`Thread`
Dictionary
BufferedInputStream
同样,接口的名称应该简短且描述性,不要过长,采用混合大小写,每个单词的首字母大写。当接口被用作抽象超类时,适用描述性名词或名词短语,例如
java.io.DataInput
和java.io.DataOutput
接口;或者描述行为的形容词,如Runnable
和Cloneable
接口。
类型变量名称
类型变量名称应简洁(如果可能,使用单个字符),具有启发性,并且不应包含小写字母。这样可以轻松区分类型参数和普通类和接口。
容器类和接口应使用名称
E
表示其元素类型。映射应使用K
表示其键的类型和V
表示其值的类型。对于任意异常类型,应使用名称X
。在没有更具体的类型来区分的情况下,我们使用T
表示类型。(这在泛型方法中经常发生。)
如果有多个表示任意类型的类型参数,则应使用字母,这些字母与字母表中的
T
相邻,例如S
。或者,可以使用数字下标(例如,T1
,T2
)来区分不同的类型变量。在这种情况下,所有具有相同前缀的变量都应带下标。
如果泛型方法出现在泛型类内部,最好避免在方法和类的类型参数中使用相同的名称,以避免混淆。嵌套泛型类也适用相同的规则。
示例 6.1-4. 传统类型变量名称
public class HashSet<E> extends AbstractSet<E> { ... }
public class HashMap<K,V> extends AbstractMap<K,V; { ... }
public class ThreadLocal<T> { ... }
public interface Functor<T, X extends Throwable> {
T eval() throws X;
}
当类型参数不方便地归入上述类别之一时,应选择尽可能有意义的名称,限制在单个字母内。上述提到的名称(
E
,K
,V
,X
,T
)不应用于不属于指定类别的类型参数。
方法名称
方法名称应该是动词或动词短语,混合大小写,第一个字母小写,后续单词的第一个字母大写。以下是方法名称的一些额外特定约定:
用于获取和设置可能被视为变量 V 的属性的方法应命名为
getV
和setV
。例如,类Thread
的方法getPriority
和setPriority
。返回某物的长度的方法应命名为
length
,如类String
中的方法。测试对象关于布尔条件 V 的方法应命名为
isV
。例如,类Thread
的方法isInterrupted
。将对象转换为特定格式 F 的方法应命名为
toF
。例如,类Object
的方法toString
,类java.util.Date
的方法toLocaleString
和toGMTString
。
字段名称
非
final
的字段名称应采用混合大小写,第一个字母小写,后续单词的第一个字母大写。请注意,设计良好的类几乎没有public
或protected
字段,除了常量字段(static
final
字段)。
字段应具有名词、名词短语或名词缩写的名称。
此约定的示例包括类
java.io.ByteArrayInputStream
的字段buf
、pos
和count
,以及类java.io.InterruptedIOException
的字段bytesTransferred
。
常量名称
接口中的常量名称应为大写字母,类的
final
变量通常也可以是一系列一个或多个单词、首字母缩写或缩写,所有字母都大写,组件之间用下划线 "_
" 分隔。常量名称应具有描述性,并且不应不必要地缩写。通常,它们可以是任何适当的词性。
常量名称的示例包括类
Character
的MIN_VALUE
、MAX_VALUE
、MIN_RADIX
和MAX_RADIX
。
一组代表集合的替代值或较少频繁地,整数值中的掩码位的常量,有时可以使用常见首字母缩写作为名称前缀。
例如:
interface ProcessStates { int PS_RUNNING = 0; int PS_SUSPENDED = 1; }
局部变量和参数名称
局部变量和参数名称应该简短但有意义。它们通常是一系列不是单词的小写字母序列,例如:
首字母缩写,即一系列单词的第一个字母,例如用于保存对
ColoredPoint
的引用的变量cp
缩写,例如用于保存某种缓冲区指针的变量
buf
助记符术语,以某种方式组织以帮助记忆和理解,通常通过使用一组局部变量,这些变量的名称按照广泛使用的类的参数名称模式命名。例如:
in
和out
,每当涉及某种输入和输出时,按照System
的字段命名模式
off
和len
,每当涉及偏移量和长度时,按照java.io
的DataInput
和DataOutput
接口的read
和write
方法的参数命名模式
b
代表byte
c
代表char
d
代表double
e
代表Exception
f
代表float
i
、j
和k
代表int
l
代表long
o
代表Object
s
代表String
v
代表某种类型的任意值
第8章:类
8.3 字段声明
字段声明 引入。
- FieldDeclaration:
-
{FieldModifier} UnannType VariableDeclaratorList
;
- VariableDeclaratorList:
-
VariableDeclarator {
,
VariableDeclarator} - VariableDeclarator:
-
VariableDeclaratorId [
=
VariableInitializer]
- VariableDeclaratorId:
- Identifier [Dims]
- VariableDeclaratorId:
- Identifier [Dims]
-
_
- VariableInitializer:
- Expression
- ArrayInitializer
- UnannType:
- UnannPrimitiveType
- UnannReferenceType
- UnannPrimitiveType:
- NumericType
-
boolean
- UnannReferenceType:
- UnannClassOrInterfaceType
- UnannTypeVariable
- UnannArrayType
- UnannClassOrInterfaceType:
- UnannClassType
- UnannInterfaceType
- UnannClassType:
- TypeIdentifier [TypeArguments]
-
PackageName
.
{Annotation} TypeIdentifier [TypeArguments] -
UnannClassOrInterfaceType
.
{Annotation} TypeIdentifier
[TypeArguments] - UnannInterfaceType:
- UnannClassType
- UnannTypeVariable:
- TypeIdentifier
- UnannArrayType:
- UnannPrimitiveType Dims
- UnannClassOrInterfaceType Dims
- UnannTypeVariable Dims
4.3 的产生式仅为方便起见:
- Dims:
- {Annotation}
[
]
{{Annotation}[
]
}
FieldDeclaration 中的每个声明符声明一个字段。如果声明符中没有包含 Identifier,则会发生编译时错误。声明符中的 Identifier 可以用作名称来引用该字段。
FieldDeclaration 中声明多个字段; FieldModifier 和 UnannType 适用于声明中的所有声明符。
FieldModifier 子句在 [8.3.1] 中描述。
8.4.8.3)不同,因为字段隐藏中没有区分静态和非静态字段,而在方法隐藏中区分静态和非静态方法。
一个隐藏字段可以通过使用限定名称(6.5.6.2)来访问,如果它是static
,或者通过包含关键字super
的字段访问表达式(15.11.2)或者转型为超类类型。
在这方面,字段的隐藏类似于方法的隐藏。
如果字段声明隐藏了另一个字段的声明,则这两个字段不需要具有相同的类型。
一个类从其直接超类和直接超接口继承所有非private
字段,这些字段对于类中的代码是可访问的(6.6),并且没有被类中的声明隐藏。
超类的private
字段可能对子类可访问 - 例如,如果两个类都是同一个类的成员。然而,private
字段永远不会被子类继承。
一个类可能从其超类和超接口或仅从其超接口继承具有相同名称的多个字段。这种情况本身不会导致编译时错误。然而,在类的主体内部尝试通过其简单名称引用任何这样的字段将导致编译时错误,因为引用是模糊的。
可能有几条路径通过接口继承相同的字段声明。在这种情况下,该字段被认为只继承一次,并且可以通过其简单名称引用而不会产生歧义。
示例 8.3-1. 多重继承字段
一个类可以从其超类和超接口或两个超接口继承具有相同名称的两个或更多字段。任何尝试通过其简单名称引用任何模糊继承的字段的编译时错误。可以使用限定名称或包含关键字super
的字段访问表达式(15.11.2)来无歧义地访问这些字段。在程序中:
interface Frob { float v = 2.0f; }
class SuperTest { int v = 3; }
class Test extends SuperTest implements Frob {
public static void main(String[] args) {
new Test().printV();
}
void printV() { System.out.println(v); }
}
类Test
从其超类SuperTest
和其超接口Frob
继承了两个名为v
的字段。这本身是允许的,但由于在方法printV
中使用了简单名称v
,因此会导致编译时错误:无法确定意图是哪个v
。
以下变体使用字段访问表达式super.v
来引用类SuperTest
中声明的名为v
的字段,并使用限定名称Frob.v
来引用接口Frob
中声明的名为v
的字段:
interface Frob { float v = 2.0f; }
class SuperTest { int v = 3; }
class Test extends SuperTest implements Frob {
public static void main(String[] args) {
new Test().printV();
}
void printV() {
System.out.println((super.v + Frob.v)/2);
}
}
它编译并打印:
2.5
即使两个不同的继承字段具有相同的类型、相同的值并且都是final
,任何通过简单名称引用任一字段的尝试都被视为模糊,并导致编译时错误。在程序中:
interface Color { int RED=0, GREEN=1, BLUE=2; }
interface TrafficLight { int RED=0, YELLOW=1, GREEN=2; }
class Test implements Color, TrafficLight {
public static void main(String[] args) {
System.out.println(GREEN); // 编译时错误
System.out.println(RED); // 编译时错误
}
}
引用GREEN
被视为模糊并不奇怪,因为类Test
继承了具有不同值的两个不同的GREEN
声明。这个例子的要点是,对RED
的引用也被视为模糊,因为继承了两个不同的声明。两个名为RED
的字段恰好具有相同的类型和相同的不变值并不影响这个判断。
示例 8.3-2. 字段的重新继承
如果同一个字段声明通过多条路径从接口继承,该字段被认为只继承一次。在类PaintedPoint
中可以在不产生歧义的情况下使用简单名称RED
,GREEN
和BLUE
来引用在接口Colorable
中声明的字段。
8.4 方法声明
8.4.1 形式参数
方法或构造函数的形式参数(如果有)由逗号分隔的参数规范器列表指定。每个参数规范器由类型(可选地由final
修饰符和/或一个或多个注释前导)和一个标识符(可选地跟随方括号)组成,指定参数的名称。
如果方法或构造函数没有形式参数,也没有接收者参数,则在方法或构造函数的声明中会出现一个空的括号对。
- FormalParameterList:
-
FormalParameter {
,
FormalParameter} - FormalParameter:
- {VariableModifier} UnannType VariableDeclaratorId
- VariableArityParameter
- VariableArityParameter:
-
{VariableModifier} UnannType {Annotation}
...
Identifier - VariableModifier:
- Annotation
-
final
- VariableDeclaratorId:
- Identifier [Dims]
- VariableDeclaratorId:
- Identifier [Dims]
- _
- Dims:
- {Annotation}
[
]
{{Annotation}[
]
}
方法或构造函数的形式参数可以是可变数量参数,在类型后面跟着省略号表示。对于一个方法或构造函数,最多只允许一个可变数量参数。如果可变数量参数出现在参数规范器列表的除最后位置之外的任何位置,则会导致编译时错误。
在VariableArityParameter的语法中,注意省略号(
...
)是一个独立的标记(3.11)。可以在它和类型之间放置空格,但这在风格上是不鼓励的。
如果一个方法的最后一个形式参数是可变数量参数,则该方法是一个可变数量方法。否则,它是一个固定数量方法。
有关形式参数声明的注释修饰符和接收者参数的规则在9.7.4和9.7.5中指定。
如果final
作为形式参数声明的修饰符出现超过一次,则会导致编译时错误。
从嵌套类或接口或lambda表达式中引用形式参数受限,如6.5.6.1中指定。
每个方法或构造函数的形式参数声明必须包括一个Identifier,否则会导致编译时错误。
如果一个方法或构造函数声明了两个具有相同名称的形式参数,则会导致编译时错误。(即,它们的声明提到相同的Identifier。)
如果一个声明为final
的形式参数在方法或构造函数的主体内部被赋值,则会导致编译时错误。
形式参数的声明类型取决于它是否是可变数量参数:
-
如果形式参数不是可变数量参数,则如果UnannType和VariableDeclaratorId中没有方括号对出现,则声明类型由UnannType表示,否则由10.2指定。
-
如果形式参数是可变数量参数,则声明类型是由10.2指定的数组类型。
如果可变数量参数的声明类型具有不可具体化的元素类型(4.7),则对于可变数量方法的声明会产生编译时未经检查的警告,除非该方法带有@SafeVarargs
注解(9.6.4.7)或警告被@SuppressWarnings
(9.6.4.5)抑制。
当方法或构造函数被调用时(15.12),实际参数表达式的值会在方法或构造函数体执行之前初始化新创建的参数变量,每个都声明了类型。在方法或构造函数体中,FormalParameter中出现的Identifier可以作为简单名称用于引用形式参数。
可变元方法的调用可能包含比形式参数更多的实际参数表达式。所有不对应于可变元参数之前的形式参数的实际参数表达式将被评估,并将结果存储到一个数组中,该数组将传递给方法调用(15.12.4.2)。
以下是实例方法和内部类构造函数中接收器参数的一些示例:
class Test { Test(/* ?? ?? */) {} // 在顶层类的构造函数中不允许有接收器参数,因为没有可想象的类型或名称。 void m(Test this) {} // OK:实例方法中的接收器参数 static void n(Test this) {} // 非法:静态方法中的接收器参数 class A { A(Test Test.this) {} // OK:接收器参数表示立即包围正在构造的A实例的Test实例。 void m(A this) {} // OK:接收器参数表示调用A.m()的A实例。 class B { B(Test.A A.this) {} // OK:接收器参数表示立即包围正在构造的B实例的A实例。 void m(Test.A.B this) {} // OK:接收器参数表示调用B.m()的B实例。 } } }
B
的构造函数和实例方法显示了接收器参数的类型可以用限定的TypeName表示,就像其他类型一样;但是内部类构造函数中接收器参数的名称必须使用封闭类的简单名称。
8.10 记录类
8.10.1 记录组件
如果有的话,记录类的记录组件在记录声明的头部中指定。每个记录组件由一个类型(可选地由一个或多个注释前导)和一个标识符组成,该标识符指定记录组件的名称。记录组件对应于记录类的两个成员:隐式声明的private
字段和显式或隐式声明的public
访问器方法(8.10.3)。
如果记录类没有记录组件,则记录声明的头部中会出现一个空的括号对。
- RecordHeader:
-
(
[RecordComponentList])
- RecordComponentList:
-
RecordComponent {
,
RecordComponent} - RecordComponent:
- {RecordComponentModifier} UnannType Identifier
- VariableArityRecordComponent
- VariableArityRecordComponent:
-
{RecordComponentModifier} UnannType {Annotation}
...
Identifier - RecordComponentModifier:
- Annotation
记录组件可以是可变元记录组件,在类型后面跟着省略号表示。对于记录类,最多只允许一个可变元记录组件。如果可变元记录组件出现在记录组件列表中除了最后位置之外的任何位置,则会导致编译时错误。
有关记录组件声明的注释修饰符的规则在9.7.4和9.7.5中指定。
如果记录声明中声明了一个记录组件的名称为
clone
、finalize
、getClass
、hashCode
、notify
、notifyAll
、toString
或wait
,则会导致编译时错误。
这些是
Object
中的无参public
和protected
方法的名称。禁止将它们作为记录组件的名称有助于避免混淆。首先,每个记录类提供了hashCode
和toString
的实现,返回记录对象作为整体的表示;它们不能作为记录组件的访问器方法(8.10.3), 因此无法从记录类外部访问这些记录组件。类似地,一些记录类可能提供clone
和(遗憾的是)finalize
的实现,因此无法通过访问器方法访问名为clone
或finalize
的记录组件。最后,Object
中的getClass
、notify
、notifyAll
和wait
方法是final
的,因此具有相同名称的记录组件无法具有访问器方法(这些访问器方法具有与final
方法相同的签名,因此会尝试但无法成功地覆盖它们)。
如果记录声明中声明了两个名称相同的记录组件,则会导致编译时错误。
记录声明的每个记录组件声明必须包含一个Identifier,否则会导致编译时错误。
记录组件的声明类型取决于它是否是可变元记录组件:
-
如果记录组件不是可变元记录组件,则声明的类型由UnannType表示。
-
如果记录组件是可变元记录组件,则声明的类型是由10.2指定的数组类型。
如果可变元记录组件的声明类型具有不可具体化的元素类型(4.7),则在可变元记录组件的声明中会发生编译时未经检查的警告,除非规范构造函数(8.10.4)带有@SafeVarargs
注解(9.6.4.7)或通过@SuppressWarnings
抑制警告(9.6.4.5)。
第9章:接口
9.3 字段(常量)声明
- ConstantDeclaration:
-
{ConstantModifier} UnannType VariableDeclaratorList
;
- ConstantModifier:
- (其中之一)
-
Annotation
public
-
static
final
- VariableDeclaratorList:
- VariableDeclarator {
,
VariableDeclarator}- VariableDeclarator:
- VariableDeclaratorId [
=
VariableInitializer]
- VariableDeclaratorId:
- Identifier [Dims]
- _
- Dims:
- {Annotation}
[
]
{{Annotation}[
]
}- VariableInitializer:
- Expression
- ArrayInitializer
接口字段声明的注释修饰符规则在9.7.4和9.7.5中指定。
接口声明体中的每个字段声明都是隐式public
、static
和final
的。对于这些字段,可以冗余地指定这些修饰符中的任何一个或全部。
如果同一个关键字在字段声明的修饰符中出现超过一次,则会导致编译时错误。
如果一个字段声明中出现两个或更多(不同的)字段修饰符,通常情况下,尽管不是必须的,它们应该按照ConstantModifier的产生规则中所示的顺序出现。
如果在UnannType和VariableDeclaratorId中没有出现括号对,则字段的声明类型由UnannType表示,否则由10.2指定。
ConstantDeclaration中的每个声明符必须包含一个Identifier,否则会导致编译时错误。
因为接口字段是static
的,其声明引入了一个静态上下文(8.1.3),这限制了引用当前对象的构造的使用。特别地,关键字this
和super
在静态上下文中是被禁止的(15.8.3,15.11.2),未经限定的引用实例变量、实例方法和词法封闭声明的类型参数也是被禁止的(6.5.5.1,6.5.6.1,15.12.3)。
如果接口声明了一个带有特定名称的字段,则该字段的声明被认为是隐藏了接口的超级接口中具有相同名称的所有可访问字段的声明。
一个接口可以继承多个具有相同名称的字段。这种情况本身不会导致编译时错误。然而,在接口声明的主体内部尝试通过简单名称引用任何这样的字段将导致编译时错误,因为引用是模糊的。
同一个字段声明从一个接口继承可能有多条路径。在这种情况下,该字段被认为只继承一次,并且可以通过其简单名称引用而不会产生歧义。
示例 9.3-1. 模糊继承字段
如果一个接口继承了两个具有相同名称的字段,例如,因为它的两个直接超级接口声明了具有该名称的字段,则会产生一个模糊的成员。对这个模糊成员的任何使用都将导致编译时错误。在程序中:
interface BaseColors {
int RED = 1, GREEN = 2, BLUE = 4;
}
interface RainbowColors extends BaseColors {
int YELLOW = 3, ORANGE = 5, INDIGO = 6, VIOLET = 7;
}
interface PrintColors extends BaseColors {
int YELLOW = 8, CYAN = 16, MAGENTA = 32;
}
interface LotsOfColors extends RainbowColors, PrintColors {
int FUCHSIA = 17, VERMILION = 43, CHARTREUSE = RED+90;
}
接口LotsOfColors
继承了两个名为YELLOW
的字段。只要接口不包含任何通过简单名称引用字段YELLOW
的情况,这是可以的。(这样的引用可能出现在字段的变量初始化器中。)
即使接口PrintColors
将值3
赋给YELLOW
而不是值8
,在接口LotsOfColors
中对字段YELLOW
的引用仍将被视为模糊。
示例 9.3-2. 多重继承字段
如果同一个接口从同一个接口多次继承了单个字段,例如,因为这个接口和这个接口的一个直接超级接口都扩展了声明该字段的接口,则只会产生一个成员。这种情况本身不会导致编译时错误。
在前面的示例中,字段RED
、GREEN
和BLUE
通过接口RainbowColors
和接口PrintColors
多种方式继承到接口LotsOfColors
,但在接口LotsOfColors
中对字段RED
的引用不被视为模糊,因为只涉及一个字段RED
的实际声明。
第14章:块、语句和模式
以下更改假定由JEP 440(记录模式)和JEP 441(switch
模式匹配)导致的JLS更改已经应用(JLS:JEP440+441)。
14.4 本地变量声明
一个本地变量声明声明并可选择初始化一个或多个本地变量(4.12.3)。
- LocalVariableDeclaration:
- {VariableModifier} LocalVariableType VariableDeclaratorList
- LocalVariableType:
- UnannType
-
var
- VariableModifier:
- Annotation
final
- VariableDeclaratorList:
- VariableDeclarator {
,
VariableDeclarator}- VariableDeclarator:
- VariableDeclaratorId [
=
VariableInitializer]
- VariableDeclaratorId:
- Identifier [Dims]
- VariableDeclaratorId:
- Identifier [Dims]
- _
- Dims:
- {Annotation}
[
]
{{Annotation}[
]
}- VariableInitializer:
- Expression
- ArrayInitializer
本地变量声明可以出现在以下位置:
-
块中的本地变量声明语句(14.4.2)
-
基本
for
语句的头部(14.14.1) -
增强
for
语句的头部(14.14.2) -
try
-with-resources语句的资源规范(14.20.3) -
模式(14.30.1)
有关本地变量声明的注解修饰符的规则在9.7.4和9.7.5中指定。
如果关键字final
作为本地变量声明的修饰符出现,则本地变量是一个final
变量(4.12.4)。
如果final
作为本地变量声明的修饰符出现超过一次,则会导致编译时错误。
如果LocalVariableType是var
,并且以下任一情况为真,则会导致编译时错误:
-
列出了多个VariableDeclarator。
-
VariableDeclaratorId有一个或多个方括号对。
-
VariableDeclarator缺少初始化器。
-
VariableDeclarator的初始化器是一个ArrayInitializer。
-
VariableDeclarator的初始化器包含对该变量的引用。
示例 14.4-1. 使用var
声明的本地变量
以下代码说明了限制使用var
的规则:
var a = 1; // 合法
var b = 2, c = 3.0; // 非法:多个声明符
var d[] = new int[4]; // 非法:额外的方括号对
var e; // 非法:没有初始化器
var f = { 6 }; // 非法:数组初始化器
var g = (g = 7); // 非法:初始化器中的自引用
这些限制有助于避免关于var
表示的类型的混淆。
14.11 switch
语句
switch
语句根据表达式的值将控制转移到多个语句或表达式中的一个。
- SwitchStatement:
-
switch
(
Expression)
SwitchBlock
Expression称为选择器表达式。选择器表达式的类型必须是char
、byte
、short
、int
或引用类型,否则会导致编译时错误。
14.11.1 switch
块
switch
语句和switch
表达式的主体都称为switch块。本小节介绍适用于所有switch块的一般规则,无论它们出现在switch
语句中还是switch
表达式中。其他小节介绍适用于switch
语句中的switch块(14.11.2)或switch
表达式中的switch块(15.28.1)的其他规则。
- SwitchBlock:
-
{
SwitchRule {SwitchRule}}
-
{
{SwitchBlockStatementGroup} {SwitchLabel:
}}
- SwitchRule:
-
SwitchLabel
->
Expression;
-
SwitchLabel
->
Block -
SwitchLabel
->
ThrowStatement - SwitchBlockStatementGroup:
-
SwitchLabel
:
{ SwitchLabel:
} BlockStatements
- SwitchLabel:
-
case
CaseConstant {,
CaseConstant} -
case null
[, default
] -
case
CasePattern [ Guard ] -
default
- SwitchLabel:
-
case
CaseConstant {,
CaseConstant} -
case null
[, default
] -
case
CasePattern{,
CasePattern } [ Guard ] -
default
- CaseConstant:
- 条件表达式
- CasePattern:
- 模式
- Guard:
-
when
表达式
一个switch块可以包含以下内容之一:
-
Switch规则,使用
->
引入switch规则表达式、switch规则块或switch规则throw
语句;或 -
Switch标记语句组,使用
:
引入switch标记块语句。
每个switch规则和switch标记语句组都以switch标签开头,可以是case
标签或default
标签。对于switch标记语句组,允许有多个switch标签。
case
标签由case
常量列表或case
模式一个或多个组成。
-
对于具有case常量的case标签,每个
case
常量必须是(1)null
文字、(2)常量表达式(15.29)或(3)枚举常量(简单或限定)的名称(8.9.1);否则会发生编译时错误。单个null
case常量也可以与default
关键字配对。 -
对于具有case模式的case标签,如果任何case模式声明一个或多个模式变量,则会发生编译时错误。
如果一个
case
标签有多个声明模式变量的case
模式,那么如果应用case
标签,则不清楚哪个变量将被初始化。例如,在代码片段switch (obj) { case Integer i, Boolean b -> { ... } ... }
中,在->
右侧的块中不清楚哪个模式变量i
或b
将被初始化,因此这是一个编译时错误。代码片段switch(obj) { case Integer i, Boolean _ -> { ... } ... }
仍然导致编译时错误,因为仍然不清楚模式变量i
是否将被初始化。代码片段switch (obj) { case Integer _, Boolean _ -> { ... } ... }
不会导致编译时错误。
具有case
模式的case
标签可以具有可选的when
表达式,称为守卫,它表示与模式匹配的值的进一步测试。如果(i)它没有守卫,或者(ii)它具有值为true
的常量表达式(15.29)的守卫,则称case
标签为未守卫;否则为守卫。
switch标签及其case
常量和case
模式称为与switch块关联。
对于给定的switch块,以下两个条件都必须为真,否则会发生编译时错误:
- 与switch块关联的
case
常量中的任何两个值不能相同。
- 与switch块关联的
default
标签不能超过一个。
与case
标签关联的任何守卫必须具有类型boolean
或Boolean
。任何守卫中使用但未声明的变量必须是final
或有效最终的(4.12.4),不能被赋值(15.26)、递增([15.14.2])或递减([15.14.3]),否则会发生编译时错误。如果守卫是值为false
的常量表达式(15.29),则会发生编译时错误。
switch
语句或switch
表达式的switch块与选择器表达式的类型T是switch兼容的,如果以下所有条件都为真:
-
如果与switch块关联任何
null
常量,则T是引用类型。 -
如果T是枚举类型,则与switch块关联的每个
case
常量,如果是枚举常量的名称,则是类型T的枚举常量的名称。 -
如果T不是枚举类型,则与switch块关联的每个
case
常量,如果是枚举常量的限定名称,则是与T兼容的枚举常量的限定名称(5.2)。 -
与switch块关联的每个
case
常量,如果是常量表达式,则与T兼容,并且T是char
、byte
、short
、int
、Character
、Byte
、Short
、Integer
或String
之一。 -
与switch块关联的每个模式p,p适用于类型T(14.30.3)。
switch块不适用于
boolean
、long
、float
和double
类型。switch
语句或switch
表达式的选择器表达式不能具有这些类型之一。
switch
语句或switch
表达式的switch块必须与选择器表达式的类型兼容,否则会发生编译时错误。
在switch块中的switch标签被称为支配的,如果对于它适用的每个值,前面的switch标签也将适用。如果switch块中的任何switch标签被支配,则会发生编译时错误。确定switch标签是否被支配的规则如下:
-
如果在switch块中有一个带有case模式q的
case
标签被支配,那么如果在switch块中有一个前置的无保护的带有case模式p的case
标签,并且p支配q,则q被支配(14.30.3)。一个模式支配另一个模式的定义基于类型。例如,类型模式Object o支配类型模式String s,因此以下代码会导致编译时错误:
Object obj = ... switch (obj) { case Object o -> System.out.println("一个对象"); case String s -> // 错误 - 被支配的case标签 System.out.println("一个字符串"); }
带有case模式的有保护
case
标签被不带有保护的相同模式的case
标签支配。例如,以下代码会导致编译时错误:String str = ...; switch (str) { case String s -> System.out.println("一个字符串"); case String s when s.length() == 2 -> // 错误 - 被支配的case标签 System.out.println("长度为两个字符的字符串"); ... }
另一方面,带有case模式的有保护
case
标签不被认为支配具有相同case
模式的无保护case
标签。这允许以下常见的模式编程风格:Integer j = ...; switch (j) { case Integer i when i <= 0 -> System.out.println("小于或等于零"); case Integer i -> System.out.println("一个整数"); }
唯一的例外是当保护是一个具有值为
true
的常量表达式时,例如:Integer j = ...; switch (j) { case Integer i when true -> // 允许但为什么写这个? System.out.println("一个整数"); case Integer i -> // 错误 - 被支配的case标签 System.out.println("一个整数"); }
如果一个case标签具有多个模式,那么如果任何一个模式被前置的无保护case标签中出现的模式支配,则该case标签被支配;例如
Integer _
被前置的无保护case中的Number _
支配:Object o = ... switch (o) { case Number _ -> System.out.println("一个数字"); case Integer _, String _ -> // 错误 - 被支配的case模式:`Integer _` System.out.println("一个整数或一个字符串"); }
-
如果一个带有case常量c的
case
标签被支配,那么以下情况之一成立:-
c是原始类型S的常量表达式,并且在switch块中有一个前置的带有S的包装类为无条件模式的
case
标签。 -
c是引用类型T的常量表达式,并且在switch块中有一个前置的带有T类型为无条件模式的
case
标签。 -
c是类型T的枚举常量,并且在switch块中有一个前置的带有T类型为无条件模式的
case
标签。
例如,带有
Integer
类型模式的case
标签支配整数字面值的case
标签:Integer j = ...; switch (j) { case Integer i -> System.out.println("一个整数"); case 42 -> // 错误 - 被支配! System.out.println("42!"); }
对保护的分析——一般情况下是不可判定的。例如,即使如果选择器表达式的值为
42
,以下代码也会导致编译时错误:Integer j = ...; switch (j) { case Integer i when i != 42 -> System.out.println("一个不是42的整数"); case 42 -> // 错误 - 被支配! System.out.println("42!"); }
任何带有
case
常量的case
标签应该出现在带有case
模式的标签之前;例如:Integer j = ...; switch (j) { case 42 -> System.out.println("42"); case Integer i when i < 50 -> System.out.println("小于50的整数"); case Integer i -> System.out.println("一个整数"); }
-
如果在switch块中有一个带有case模式的case
标签被支配,那么如果在switch块中有一个前置的default
标签。
如果在switch块中有一个带有null
case常量的case
标签被支配,那么如果在switch块中有一个前置的default
标签。
如果使用,
default
标签应该出现在switch
块中的最后。出于历史原因,
default
标签可以出现在没有null
case
常量或case
模式的case
标签之前。int i = ...; switch(i) { default -> System.out.println("其他整数"); case 42 -> // 允许 System.out.println("42"); }
这种风格在新代码中不推荐使用。
如果在switch块中有一个带有null
、default
的case
标签被支配,那么如果在switch块中有一个前置的带有case
模式p的无保护的case
标签,其中p对于选择器表达式的类型是无条件的(14.30.3)。
如果在switch块中有一个带有null
、default
的case
标签被支配,那么如果在switch块中有一个前置的带有case
模式p的无保护的case
标签,其中p对于选择器表达式的类型是无条件的(14.30.3)。
带有对于选择器表达式类型是无条件的
case
模式的case
标签,如其名称所示,将匹配每个值,因此表现类似于default
标签。一个switch块不能有多个像default
一样行为的switch标签。
如果在switch块中存在一个带有case
模式p1,...,pn(n > 1)的case
标签,其中其中一个模式pi(1 ≤ i < n)支配另一个模式pj(i < j ≤ n),则会导致编译时错误。
如果在由switch标记的语句组成的switch块中,一个语句被标记为带有声明一个或多个模式变量的case
模式,并且:
-
在switch块中的前一个语句可以正常完成(14.22),或
-
该语句被标记为多个switch标签。
第一个条件防止一个语句组从一个语句组“穿透”到另一个语句组而不初始化模式变量。例如,如果一个由
case Integer i
标记的语句可以从前一个语句组到达,那么模式变量i
将不会被初始化:Object o = "Hello"; switch (o) { case String s: System.out.println("字符串:" + s ); // 没有break! case Integer i: System.out.println(i + 1); // 错误!可以到达 // 而不匹配模式`Integer i` default: }
由switch标记的switch块允许多个标签应用于一个语句组。第二个条件防止一个语句组根据一个标签执行而不初始化另一个标签的模式变量。例如:
Object o = "Hello World"; switch (o) { case String s: case Integer i: System.out.println(i + 1); // 错误!可以到达 // 而不匹配模式`Integer i` default: } Object obj = null; switch (obj) { case null: case String s: System.out.println(s); // 错误!可以到达 // 而不匹配模式`String s` default: }
这两个条件仅在
case
模式声明模式变量时适用。相比之下,以下示例没有问题:record R() {} record S() {} Object o = "Hello World"; switch (o) { case String s: System.out.println(s); // 没有break! case R(): System.out.println("它是R或字符串"); break; default: } Object ob = new R(); switch (ob) { case R(): case S(): // 多个case标签! System.out.println("要么是R要么是S"); break; default: } Object obj = null; switch (obj) { case null: case R(): // 多个case标签! System.out.println("要么是null要么是R"); break; default: }
14.11.1.2 在运行时确定哪个Switch标签适用
执行switch
语句(14.11.3)和评估switch
表达式(15.28.2)都需要确定与switch块相关联的switch标签是否适用于选择器表达式的值。这样进行:
-
如果值是空引用,则适用具有空
case
常量的case
标签。 -
如果值不是空引用,则我们按照以下方式确定适用于该值的开关块中的第一个(如果有)
case
标签:-
具有非空
case
常量c的case
标签适用于Character
、Byte
、Short
或Integer
类型的值,如果该值首先经过解箱转换(5.1.8),并且常量c等于解箱后的值。任何解箱转换必须正常完成,因为被解箱的值保证不是空引用。
相等性是根据
==
运算符定义的(15.21)。 -
具有非空
case
常量c的case
标签适用于不是Character
、Byte
、Short
或Integer
类型的值,如果常量c等于该值。相等性是根据
==
运算符定义的,除非该值是String
,在这种情况下,相等性是根据String
类的equals
方法定义的。 -
确定
case
模式p适用于值的过程首先通过检查值是否与模式p匹配进行(14.30.2)。确定具有
case
模式p1,...,pn(n ≥ 1)的case
标签适用于值的过程是通过确定适用的第一个(如果有)case
模式pi(1 ≤ i ≤ n)进行的。如果确定哪个
case
模式适用的过程突然完成,则确定哪个开关标签适用的过程因同样的原因突然完成。确定
case
模式p适用于值的过程首先通过检查值是否与模式p匹配进行(14.30.2)。如果模式匹配突然完成,则确定哪个
开关标签case
模式适用的过程因同样的原因突然完成。如果模式匹配成功且
case
标签模式是无保护的,则此case
标签模式适用。如果模式匹配成功且
case
标签模式是有保护的,则评估该保护条件。如果结果是Boolean
类型,则会进行解箱转换(5.1.8)。如果保护条件的评估或随后的解箱转换(如果有)因某种原因突然完成,则确定哪个
开关标签模式适用的过程因同样的原因突然完成。否则,如果结果值为
true
,则case
标签模式适用。 ::: -
case null, default
标签适用于每个值
-
-
如果值不是空引用,并且根据第2步的规则没有适用的
case
标签,则如果开关块关联有default
标签,则适用该标签。
单个
case
标签可以包含多个case
常量。如果其常量中的任何一个等于选择器表达式的值,则该标签适用于选择器表达式的值。例如,在以下代码中,如果枚举变量day
是所示枚举常量之一,则case
标签匹配:switch (day) { ... case SATURDAY, SUNDAY : System.out.println("It's the weekend!"); break; ... }
如果适用具有
case
模式的case
标签,则这是因为成功地将值与模式进行了模式匹配(14.30.2)。如果值成功匹配模式,则模式匹配的过程将初始化模式声明的任何模式变量。
case
标签未能匹配的情况下才考虑default
标签,即使其中一些标签出现在default
标签之后。但是,后续标签只能使用非null
的case常量( 14.11.1),并且作为一种风格,鼓励程序员将default
标签放在最后。
switch
语句的主体可以是一个语句,并且具有case
标签的语句不必立即包含在该语句中。考虑简单的循环:for (i = 0; i < n; ++i) foo();
n
已知为正数。在C或C++中可以使用称为 Duff's device的技巧来展开循环,但这不是Java编程语言中的有效代码:int q = (n+7)/8; switch (n%8) { case 0: do { foo(); // Great C hack, Tom, case 7: foo(); // but it's not valid here. case 6: foo(); case 5: foo(); case 4: foo(); case 3: foo(); case 2: foo(); case 1: foo(); } while (--q > 0); }
14.14 for
语句
14.14.2 增强的for
语句
for
语句的形式如下:
- EnhancedForStatement:
-
for
(
LocalVariableDeclaration:
Expression)
Statement - EnhancedForStatementNoShortIf:
-
for
(
LocalVariableDeclaration:
Expression)
StatementNoShortIf
- LocalVariableDeclaration:
- {VariableModifier} LocalVariableType VariableDeclaratorList
- VariableModifier:
- Annotation
final
- LocalVariableType:
- UnannType
var
- VariableDeclaratorList:
- VariableDeclarator {
,
VariableDeclarator}- VariableDeclarator:
- VariableDeclaratorId [
=
VariableInitializer]
- VariableDeclaratorId:
- Identifier [Dims]
- VariableDeclaratorId:
- Identifier [Dims]
- _
- Dims:
- {Annotation}
[
]
{{Annotation}[
]
}
Expression的类型必须是数组类型(10.1)或原始类型Iterable
的子类型,否则将出现编译时错误。
for
语句的头部
要么声明一个局部变量,其名称是
VariableDeclaratorId给定的标识符
,要么声明一个未命名的局部变量(6.1)。执行增强
for
语句时,在循环的每次迭代中,该局部变量被初始化为表达式生成的
Iterable
或数组的连续元素。
for
语句的头部声明的局部变量的作用域和遮蔽在
6.3和
6.4中指定。
6.5.6.1中指定。
for
语句的头部声明的局部变量的类型
T确定如下:
-
如果LocalVariableType是UnannType,并且UnannType或VariableDeclaratorId中没有出现括号对,则T是由UnannType表示的类型。
-
如果LocalVariableType是UnannType,并且UnannType或VariableDeclaratorId中出现了括号对,则T由10.2指定。
-
如果LocalVariableType是
var
,则让R从Expression的类型派生,如下:-
如果Expression具有数组类型,则R是数组类型的组件类型。
-
否则,如果Expression具有
Iterable<
X>
的子类型,其中X是某种类型,则R是X。 -
否则,Expression具有原始类型
Iterable
的子类型,R是Object
。
T是关于R提及的所有合成类型变量的上投影(4.10.5)。
-
增强的for
语句的确切含义,其标题声明一个局部变量Identifier,通过以下方式转换为基本for
语句:
-
如果Expression的类型是
Iterable
的子类型,则基本for
语句具有以下形式:for (I #i = Expression.iterator(); #i.hasNext(); ) { {VariableModifier} T Identifier = (TargetType) #i.next(); Statement }
其中:
-
如果Expression的类型是
Iterable<
X>
的子类型,其中X是某种类型参数,则I是类型java.util.Iterator<
X>
。否则,I是原始类型java.util.Iterator
。 -
#i
是一个自动生成的标识符,与增强的for
语句发生的位置的任何其他标识符(自动生成的或其他)不同。 -
{VariableModifier}如增强的
for
语句的标题中所示。 -
T是如上确定的局部变量的类型。
-
如果T是引用类型,则TargetType是T。否则,TargetType是I的类型参数的捕获转换的上界(5.1.10),如果I是原始的,则为
Object
。
-
-
否则,Expression必然具有数组类型S
[]
,基本for
语句具有以下形式:S[] #a = Expression; L1: L2: ... Lm: for (int #i = 0; #i < #a.length; #i++) { {VariableModifier} T Identifier = #a[#i]; Statement }
其中:
-
L1 ... Lm是增强的
for
语句之前的(可能为空的)标签序列。 -
#a
和#i
是自动生成的标识符,与增强的for
语句发生的位置的任何其他标识符(自动生成的或其他)不同。 -
{VariableModifier}如增强的
for
语句的标题中所示。 -
T是如上确定的局部变量的类型。
-
例如,以下代码:
List<? extends Integer> l = ... for (float i : l) ...
将被转换为:
for (Iterator<Integer> #i = l.iterator(); #i.hasNext(); ) { float #i0 = (Integer)#i.next(); ...
示例 14.14-1. 增强的for
和数组
以下程序计算整数数组的总和,展示了如何对数组使用增强的for
:
int sum(int[] a) {
int sum = 0;
for (int i : a) sum += i;
return sum;
}
示例 14.14-2. 增强的for
和拆箱转换
以下程序将增强的for
语句与自动拆箱结合起来,将直方图转换为频率表:
Map<String, Integer> histogram = ...;
double total = 0;
for (int i : histogram.values())
total += i;
for (Map.Entry<String, Integer> e : histogram.entrySet())
System.out.println(e.getKey() + " " + e.getValue() / total);
}
如果增强的for
语句的标题声明了一个未命名的局部变量,则其确切含义如上转换为基本for
语句,但使用未命名的局部变量代替Identifier。
14.20 try
语句
14.20.3 try
资源
try
资源语句使用在执行try
块之前初始化并在执行try
块后以与初始化相反的顺序自动关闭的变量(称为资源)。当资源被自动关闭时,catch
子句和finally
子句通常是不必要的。
- TryWithResourcesStatement:
-
try
ResourceSpecification Block [Catches] [Finally] - ResourceSpecification:
-
(
ResourceList [;
])
- ResourceList:
-
Resource {
;
Resource} - Resource:
- LocalVariableDeclaration
- VariableAccess
- VariableAccess:
- ExpressionName
- FieldAccess
- LocalVariableDeclaration:
- {VariableModifier} LocalVariableType VariableDeclaratorList
- VariableModifier:
- Annotation
final
- LocalVariableType:
- UnannType
var
- VariableDeclaratorList:
- VariableDeclarator {
,
VariableDeclarator}- VariableDeclarator:
- VariableDeclaratorId [
=
VariableInitializer]
- VariableDeclaratorId:
- Identifier [Dims]
- VariableDeclaratorId:
- Identifier [Dims]
- _
- Dims:
- {Annotation}
[
]
{{Annotation}[
]
}- VariableInitializer:
- Expression
- ArrayInitializer
请参阅8.3中的UnannType。
资源规范表示try
资源语句的资源,可以通过声明带有初始化表达式的局部变量或引用现有变量来实现。现有变量通过表达式名称(6.5.6)或字段访问表达式(15.11)引用。
在资源规范中声明的局部变量的规则在14.4中指定。此外,以下所有条件必须为真,否则将出现编译时错误:
-
VariableDeclaratorList由单个VariableDeclarator组成。
-
VariableDeclarator具有初始化程序。
-
VariableDeclaratorId没有括号对。
在资源规范中声明的局部变量的作用域和遮蔽在6.3和6.4中指定。
从嵌套类或接口,或lambda表达式中引用局部变量受限,如6.5.6.1中指定。
在资源规范中声明的局部变量的类型在14.4.1中指定。
在资源规范中声明的局部变量的类型,或在资源规范中引用的现有变量的类型,必须是AutoCloseable
的子类型,否则将出现编译时错误。
资源规范声明两个同名局部变量是编译时错误。
6.1)。
资源是final
的,即:
-
在资源规范中声明的局部变量如果没有显式声明为
final
,则会隐式声明为final
(4.12.4)。 -
在资源规范中引用的现有变量必须是在
try
资源语句之前明确定义的final
或有效final
变量(16),否则将出现编译时错误。
资源按从左到右的顺序初始化。如果一个资源初始化失败(即,其初始化表达式抛出异常),那么try
-with-resources语句到目前为止已经初始化的所有资源都将被关闭。如果所有资源成功初始化,try
块将正常执行,然后try
-with-resources语句中所有非空资源都将被关闭。
资源按照初始化顺序的相反顺序关闭。只有初始化为非空值的资源才会被关闭。一个资源的关闭异常不会阻止其他资源的关闭。如果之前由初始化程序、try
块或资源关闭引发了异常,则此类异常将被抑制。
一个资源规范指示多个资源的try
-with-resources语句被视为多个try
-with-resources语句,每个语句都有指示单个资源的资源规范。当一个指示有n个资源(n > 1)的try
-with-resources语句被转换时,结果是一个有n-1个资源的try
-with-resources语句。经过n次这样的转换后,会有n个嵌套的try
-catch
-finally
语句,整体转换完成。
14.20.3.1 基本try
-with-resources
一个没有catch
子句或finally
子句的try
-with-resources语句称为基本try
-with-resources语句。
如果一个基本try
-with-resources语句的形式为:
try (VariableAccess ...)
Block
那么资源首先通过以下转换转换为本地变量声明:
try (T #r = VariableAccess ...) {
Block
}
T
是由VariableAccess表示的变量的类型,#r
是一个自动生成的标识符,与try
-with-resources语句发生的地方的任何其他标识符(自动生成的或其他)都不同。然后根据本节的其余部分转换try
-with-resources语句。
一个形式为:
try ({VariableModifier} R Identifier = Expression ...)
Block
的基本try
-with-resources语句的含义由以下转换为本地变量声明和try
-catch
-finally
语句给出:
{
final {VariableModifierNoFinal} R Identifier = Expression;
Throwable #primaryExc = null;
try ResourceSpecification_tail
Block
catch (Throwable #t) {
#primaryExc = #t;
throw #t;
} finally {
if (Identifier != null) {
if (#primaryExc != null) {
try {
Identifier.close();
} catch (Throwable #suppressedExc) {
#primaryExc.addSuppressed(#suppressedExc);
}
} else {
Identifier.close();
}
}
}
}
{VariableModifierNoFinal}被定义为没有final
的{VariableModifier},如果存在的话。
#t
、#primaryExc
和#suppressedExc
是自动生成的标识符,与try
-with-resources语句发生的地方的任何其他标识符(自动生成的或其他)都不同。
另外,一个形式为:
try ({VariableModifier} R _ = Expression ...)
Block
的基本try
-with-resources语句的含义由以下转换为本地变量声明和try
-catch
-finally
语句给出:
{
final {VariableModifierNoFinal} R #i = Expression;
Throwable #primaryExc = null;
try ResourceSpecification_tail
Block
catch (Throwable #t) {
#primaryExc = #t;
throw #t;
} finally {
if (#i != null) {
if (#primaryExc != null) {
try {
#i.close();
} catch (Throwable #suppressedExc) {
#primaryExc.addSuppressed(#suppressedExc);
}
} else {
#i.close();
}
}
}
}
{VariableModifierNoFinal}被定义为没有final
的{VariableModifier},如果存在的话。
#t
、#primaryExc
、#suppressedExc
和#i
是自动生成的标识符,与try
-with-resources语句发生的地方的任何其他标识符(自动生成的或其他)都不同。
如果资源规范指示一个资源,则ResourceSpecification_tail为空(并且try
-catch
-finally
语句本身不是try
-with-resources语句)。
如果资源规范指示n > 1个资源,则ResourceSpecification_tail由资源规范中指示的第2个、第3个、...、第n个资源组成,顺序相同(并且try
-catch
-finally
语句本身是try
-with-resources语句)。
基本try
-with-resources语句的可达性和明确赋值规则由上述转换隐含指定。
在管理单个资源的基本try
-with-resources语句中:
-
如果资源的初始化因值V的
throw
而突然完成,则try
-with-resources语句因值V的throw
而突然完成。 -
如果资源的初始化正常完成,并且
try
块因值V的throw
而突然完成,则:-
如果资源的自动关闭正常完成,则
try
-with-resources语句因值V的throw
而突然完成。 -
如果资源的自动关闭因值V2的
throw
而突然完成,则try
-with-resources语句因值V的throw
而突然完成,V2被添加到V的抑制异常列表中。
-
-
如果资源的初始化正常完成,并且
try
块正常完成,并且资源的自动关闭因值V的throw
而突然完成,则try
-with-resources语句因值V的throw
而突然完成。
在管理多个资源的基本try
-with-resources语句中:
-
如果一个资源的初始化因值V的
throw
而突然完成,则:-
如果所有成功初始化的资源(可能为零)的自动关闭正常完成,则
try
-with-resources语句因值V的throw
而突然完成。 -
如果所有成功初始化的资源(可能为零)的自动关闭因值V1...Vn的
throw
而突然完成,则try
-with-resources语句因值V的throw
而突然完成,剩余的值V1...Vn被添加到V的抑制异常列表中。
-
-
如果所有资源的初始化正常完成,并且
try
块因值V的throw
而突然完成,则:-
如果所有初始化的资源的自动关闭正常完成,则
try
-with-resources语句因值V的throw
而突然完成。 -
如果一个或多个初始化的资源的自动关闭因值V1...Vn的
throw
而突然完成,则try
-with-resources语句因值V的throw
而突然完成,剩余的值V1...Vn被添加到V的抑制异常列表中。
-
-
如果每个资源的初始化都正常完成,并且
try
块正常完成,则:-
如果一个初始化资源的自动关闭因值V的
throw
而突然完成,并且所有其他初始化资源的自动关闭正常完成,则try
-with-resources语句因值V的throw
而突然完成。 -
如果多个初始化资源的自动关闭因值V1...Vn的
throw
而突然完成(其中V1是最右侧资源未能关闭的异常,Vn是最左侧资源未能关闭的异常),则try
-with-resources语句因值V1的throw
而突然完成,剩余的值V2...Vn被添加到V1的抑制异常列表中。
-
14.20.3.2 扩展try
-with-resources
至少有一个catch
子句和/或finally
子句的try
-with-resources语句称为扩展try
-with-resources语句。
扩展try
-with-resources语句的含义:
try ResourceSpecification
Block
[Catches]
[Finally]
由以下转换为嵌套在try
-catch
或try
-finally
或try
-catch
-finally
语句内的基本try
-with-resources语句给出:
try {
try ResourceSpecification
Block
}
[Catches]
[Finally]
转换的效果是将资源规范“放入”try
语句中。这允许扩展try
-with-resources语句的catch
子句捕获由任何资源的自动初始化或关闭引起的异常。
此外,所有资源将在finally
块执行时已关闭(或尝试关闭),符合finally
关键字的意图。
14.30 模式
14.30.1 模式的种类
假设已应用JEP 440(记录模式)和JEP 441(switch
模式匹配)导致的JLS更改(JLS:JEP440+441)。
类型模式用于测试一个值是否是模式中出现的类型的实例。 记录模式用于测试一个值是否是记录类类型的实例,并且如果是,则对记录组件值执行递归模式匹配。
- 模式:
- 类型模式
- 记录模式
- 类型模式:
- 局部变量声明
- 记录模式:
-
引用类型
(
[模式列表组件模式列表 ])
- 模式列表:
-
模式 {
,
模式 }
- 组件模式列表:
-
组件模式 {
,
组件模式 } - 组件模式:
- 模式
- 未命名模式
- 未命名模式:
- _
- 局部变量声明:
- {变量修饰符} 局部变量类型 变量声明符列表
- 变量修饰符:
- 注解
final
- 局部变量类型:
- 非注释类型
var
- 变量声明符列表:
- 变量声明符 {
,
变量声明符}- 变量声明符:
- 变量声明符标识符 [
=
变量初始化器]
- 变量声明符标识符:
- 标识符 [维度]
- 变量声明符标识符:
- 标识符 [维度]
- _
- 维度:
- {注解}
[
]
{{注解}[
]
}
查看非注释类型的8.3。
在记录模式的嵌套模式列表中不出现的模式称为顶层模式;否则称为嵌套模式。
类型模式声明正好一个模式变量。 此模式变量可以是一个未命名模式变量(用_
表示),否则
在局部变量声明中的标识符指定模式变量的名称。在局部变量声明中的标识符指定模式变量的名称。
在类型模式中声明的模式变量的规则在14.4中指定。此外,以下所有条件必须为真,否则将出现编译时错误:
-
顶层类型模式中的局部变量类型表示一个引用类型(而且不是
var
)。 -
变量声明符列表由单个变量声明符组成。
-
变量声明符没有初始化程序。
-
变量声明符标识符没有括号对。
在顶层类型模式中声明的模式变量的类型是由局部变量类型表示的引用类型。
在嵌套类型模式中声明的模式变量的类型如下确定:
-
如果局部变量类型是非注释类型,则模式变量的类型由非注释类型表示
-
如果局部变量类型是
var
,则类型模式必须嵌套在具有类型R的记录模式的组件模式列表中。让T是R中相应组件字段的类型。模式变量的类型是T相对于T提到的所有合成类型变量的上投影。考虑以下记录声明:
record R<T>(ArrayList<T> r){}
给定模式
R<String>(var r)
,模式变量r
的类型因此为ArrayList<String>
。
如果类型模式作为记录模式的模式列表中的元素出现,并且记录模式的类型为R,则类型模式被称为空匹配,并且对于类型U,类型模式对于类型U是无条件的(14.30.3)。
请注意,类型模式的这种编译时属性用于模式匹配过程(14.30.2),因此与类型模式关联以在运行时使用。
记录模式由一个引用类型和一个嵌套的组件模式列表组成。如果引用类型不是记录类类型(8.10),则会出现编译时错误。
如果引用类型是原始类型,则记录模式的类型将被推断,如[18.5.5]中所述。如果无法为记录模式推断类型,则会出现编译时错误。
否则,记录模式的类型为引用类型。
记录模式的嵌套组件模式列表的长度必须与由引用类型命名的记录类声明中的记录组件列表的长度相同;否则会出现编译时错误。
目前,不支持可变数量的记录模式。这可能在未来版本的Java编程语言中得到支持。
记录模式声明由在嵌套的组件模式列表中声明的模式中声明的模式变量(如果有)组成。
未命名模式是一种特殊模式,只能作为具有类型R的记录模式的组件模式列表中的元素出现。让T是R中相应组件字段的类型。未命名模式不声明任何模式变量,其类型被定义为T相对于T提到的所有合成类型变量的上投影。未命名模式始终是空匹配的。
14.30.2 模式匹配
模式匹配是在运行时针对模式测试值的过程。模式匹配与语句执行(14.1)和表达式评估(15.1)不同。 如果一个值成功匹配一个模式,则模式匹配过程将初始化模式声明的所有模式变量(如果有)。
模式匹配过程可能涉及表达式评估或语句执行。因此,如果表达式的评估或语句的执行完成异常,则说模式匹配突然完成。突然完成始终有一个关联的原因,这个原因总是一个具有给定值的throw
。如果没有突然完成,则说模式匹配正常完成。
确定值是否匹配模式以及初始化模式变量的规则如下:
-
空引用匹配一个
类型模式空匹配模式如果类型模式是空匹配(14.30.1);否则不匹配。如果空引用匹配,则如果有的话由
类型空匹配模式声明的模式变量将被初始化为空引用。如果空引用不匹配,则如果有的话由
类型空匹配模式声明的模式变量将不被初始化。 -
不是空引用的值匹配类型为T的类型模式,如果v可以被强制转换为T而不引发
ClassCastException
;否则不匹配。如果v匹配,则由类型模式声明的模式变量将被初始化为v。
如果v不匹配,则由类型模式声明的模式变量将不被初始化。
-
空引用不匹配记录模式。
在这种情况下,由记录模式声明的任何模式变量都不会被初始化。
-
不是空引用的值匹配类型为R且具有嵌套组件模式列表L的记录模式,如果(i)v可以被强制转换为R而不引发
ClassCastException
;并且(ii)v的每个记录组件与L中的相应模式匹配;否则不匹配。v的每个记录组件是通过调用与该组件对应的v的访问器方法来确定的。如果调用访问器方法的执行因原因S而突然完成,则通过抛出具有原因S的
MatchException
来突然完成模式匹配。出现在嵌套组件模式列表中的模式中声明的任何模式变量仅在所有列表中的模式都匹配时才被初始化。