Skip to content

Latest commit

 

History

History
3293 lines (1776 loc) · 1.57 MB

15-表达式.md

File metadata and controls

3293 lines (1776 loc) · 1.57 MB

第十五章 表达式

程序中的大多数工作由计算表达式完成,无论是为了副作用,例如对变量的赋值,还是为了他们的值,其可以在更大的表达式中用作参数或操作数,或者影响语句中的执行顺序,或者两种都是。

本章指定了表达式的含义和他们的计算规则。

15.1. 计算、指示和结果

当计算(执行)程序中的表达式时,结果指示三种情况之一:

    * 一个变量(4.12)(在 C 中,这被称为左值)

    * 一个值(4.2,4.3)

    * 什么也没(表达式被称为 void)

如果一个表达式指示一个变量,并且在进一步计算中需要使用一个值,则使用该变量的值。在此上下文中,如果表达式指示变量或值,则可以简单地称为表达式的值。

将值集转换(5.1.13)应用到每个产生值的表达式的结果,包括在使用 float 或 double 类型的变量的值时。

表达式什么也不表示,当且仅当它是调用不返回值的方法的方法调用(15.12),即,声明为 void(8.4)的方法。这种表达式仅可以用作表达式语句(14.8)或 lambda body(15.27.2)的单个表达式,因为表达式可以出现在的每个其他上下文都要求表达式表示某物。其是方法调用的表达式语句或 lambda body 也可以调用产生结果的方法,在这种情况下,暗中丢弃由此方法返回的值。

表达式的计算可能会产生副作用,因为表达式可以包含嵌入的赋值、增量运算符、减量操作符和方法调用。

表达式在以下任一情况下出现:

    * 某个(类或接口)类型的声明,其被声明在:字段初始化器、static 初始化器、实例初始化器、构造器声明、方法声明或注解中。

    * 在包声明或顶层类型声明上的注解。

15.2. 表达式的形式

表达式可以大致归类为下列句法形式之一:

    * 表达式名称(6.5.6)

    * 主要表达式(15.8 - 15.13)

    * 一元运算符表达式(15.14 - 15.16)

    * 二元运算符表达式(15.17 - 15.24,和 15.26)

    * 三元运算符表达式(15.25)

    * Lambda 表达式(15.27)

运算符之间的优先级语法产生式的层次结构管理。最低优先级的运算符是 lambda 表达式的箭头(->),后跟赋值运算符。因此,语法上所有表达式都包括在 LambdaExpression 和 AssignmentExpression 非终结符中:

Expression:
    LambdaExpression
    AssignmentExpression

当某些表达式出现在特定上下文中时,他们被认为是聚合表达式。以下形式的表达式可以是聚合表达式:

    * 括号化的表达式(15.8.5)

    * 类实力创建表达式(15.9)

    * 方法调用表达式(15.12)

    * 方法引用表达式(15.13)

    * 条件表达式(15.25)

    * Lambda 表达式(15.27)

确定这些形式中的某个表达式是否是聚合表达式的规则在单独的章节中给定,这些章节详述了这些表达式。

不是聚合表达式的那些表达式是独立表达式。独立表达式是以上在确定为不是聚合表达式时的那些表达式,以及所有其他形式的表达式。所有其他形式的表达式被称为,具有独立形式。

某些表达式具有在编译时可以确定的值。这些是常量表达式(15.28)。

15.3. 表达式的类型

如果表达式表示变量或值,则表达式具有在编译时已知的类型。可以完全地从表达式的内容确定独立表达式的类型;相反,聚合表达式的类型可能受表达式的目标类型(5(Conversions and Contexts))的影响。确定每种表达式的表达式类型的规则在下面分别解释。

表达式的值与表达式的类型赋值兼容(5.2),除非发生堆污染(4.12.2)。

同样,变量中存储的值与变量的类型总是赋值兼容,除非发生堆污染。

换言之,其类型是 T 的表达式的类型总是可赋值给 T 类型的变量。

请注意,确保其类型是被声明为 final 的类类型 F 的表达式具有一个值,其要么是个 null 引用,要么是一个其类是 F 本身的对象,因为 final 类型没有子类。

15.4. FP-strict 表达式

如果表达式的类型是 float 或 double,则有一个问题,从哪个值集(4.2.3)中得到表达式的值 。这由值集转换(5.1.13)规则控制;这些规则反过来依赖于表达式是否是 FP-strict。

每个常量表达式(15.28)是 FP-strict。

如果一个表达式不是常量表达式,则考虑所有包含该表达式的类声明、接口声明和方法声明。如果任何此类声明具有 strictfp 修饰符(8.1.1.3,8.4.3.5,9.1.1.2),则表达式是 FP-strict。

如果类、接口或方法,X,被声明为 strictfp,则 X 和 X 中的任何类、接口、方法、构造器、实例初始化器、static 初始化器或变量初始化器被称为是 FP-strict。

请注意,注解的元素值(9.7)总是 FP-strict,因为它总是常量表达式。

它遵循的是,表达式不是 FP-strict,当且仅当它不是常量表达式,它也不在任何具有 strictfp 修饰符的声明中出现时。

在 FP-strict 表达式中,所有中间值必须是浮点值集或双精度值集的元素,这意味着所有 FP-strict 表达式的结果必须是在使用单精度和双精度格式表示的操作数上由 IEEE 754 算法所预测的那些。

在非 FP-strict 表达式中,为实现使用扩展指数范围来表示中间值而授予了一些回旋余地;粗略地说,有效效应是,在互斥使用浮点值集或双精度值集可能会导致上溢或下溢的情况下,计算可能产生“正确答案”。

15.5. 表达式和运行时检查

如果表达式的类型是基元类型,则表达式的值是相同的基元类型。

如果表达式的值引用类型,则引用的对象的类,甚至该值是否是对象引用而不是 null,在编译时不一定是已知的。Java 编程语言中有几个地方,其中引用对象的实际类型以无法从表达式的类型中推断出来的方式影响程序的执行。他们如下:

    * 方法调用(15.12)。根据其是 o 类型的类或接口的一部分的方法选择用于调用 o.m(...) 的特定方法。对于实例方法,o 的运行时值引用的对象的类参与,因为子类可能重写已在父类中声明的特定方法,从而调用调用此重写方法。(重写方法可以也可以不选择进一步调用原始被重写的 m 方法。)

    * instanceof 操作符(15.20.2)。其类型是引用类型的表达式可以使用 instanceof 测试,以查明表达式的运行时值引用的对象的类与某个其他引用类型是否是赋值兼容(5.2)。

    * 强制转换(5.5,15.16)。操作数表达式的运行时值引用的对象的类可能与由强制转换指定的类型不兼容。对于引用类型,这可能需要抛出异常的运行时检查,如果引用的对象的类,在运行时确定时,与目标类型不是赋值兼容(5.2)。

    * 对引用类型(10.5,15.13,15.26.1)的数组组件的赋值。类型检查规则允许数组类型 S[] 被视为 T[] 的子类型,如果 S 是 T 的子类型,但这要求对数组组件的赋值进行类型检查,类似于对强制转换进行的检查。

    * 异常处理(14.20)。仅当抛出的异常对象的类是 catch 子句的形式参数的 instanceof 类型,异常才被捕获。

对象的类不是静态已知的情况可能会导致运行时错误。

此外,还有一些情况,其中静态已知类型在运行时可能不准确。这种情况可能出现在导致编译时未检查警告的程序中。为了响应无法静态确保安全的操作而给出这种警告,并且这种警告无法间接地受动态检查的影响,因为他们调用不可信赖的类型(4.7)。因此,稍后在程序执行课程中的动态检查可能会检测到不一致并导致运行时类型错误。

运行时类型错误仅可以出现在这些情况下:

    * 在强制转换中,当操作数表达式的值引用的对象的实际类与强制转换操作符(5.5,15.16)指定的目标类型不兼容时;在这种情况下,抛出 ClassCastException。

    * 在自动生成的强制转换中,引入以确保在不可信赖的类型(4.7)上的操作的有效性。

    * 在对引用类型的数组组件的赋值中,当用于赋值的值引用的对象的实际类与数组(10.5,15.13,15.26.1)的实际运行时组件类型不兼容时;在这种情况下,抛出 ArrayStoreException。

    * 当异常不被 try 语句(14.20)的任何 catch 子句捕获时;在这种情况下,遇到异常的控制线程首先尝试调用未捕获异常处理器,然后终止。

15.6. 计算的正常和突然完成

每个表达式都有正常的计算模式,其中采取特定的计算步骤。以下章节描述了每种表达式的正常计算模式。

如果采取的所有步骤都没抛出异常,则此表达式被称为正常完成。

但是,如果表达式的计算抛出了异常,则此表达式被称为突然完成。突然完成总是具有一个相关联的理由,其总是一个具有给定值的 throw。

运行时异常由如下预定义的操作符抛出:

    * 类实例创建表达式(15.9.4)、数组创建表达式(15.10.2)、方法引用表达式(15.13.3)、数组初始化器表达式(10.6)、字符串拼接操作符表达式(15.18.1)或 lambda 表达式(15.27.4)抛出 OutOfMemoryError,如果没有足够可用的内存。

    * 数组创建表达式(15.10.2)抛出 NegativeArraySizeException,如果任何维表达式的值小于零。

    * 数组访问表达式(15.10.4)抛出 NullPointerException,如果数组引用表达式的值是 null。

    * 数组访问表达式(15.10.4)抛出 ArrayIndexOutOfBoundException,如果数组索引表达式的值是负数或大于或等于数组的长度。

    * 字段访问表达式(15.11)抛出 NullPointerException,如果对象引用表达式的值是 null。

    * 调用实例方法的方法调用表达式(15.12)抛出 NullPointerException,如果目标引用是 null。

    * 强制转换表达式(15.16)抛出 ClassCastException,如果在运行时发现不允许的强制转换。

    * 整数除法(15.17.2)或整数取余(15.17.3)操作符抛出 ArithmeticException,如果右手侧操作数表达式的值是零。

    * 对引用类型(15.26.1)数组组件的赋值、方法调用表达式(15.12)或前缀或后缀自增(15.14.2,15.15.1)或自减操作符(15.14.3,15.15.2)可能都会抛出 OutOfMemoryError,作为装箱转换(5.1.7)的结果。

    * 对引用类型(15.26.1)数组组件的赋值抛出 ArrayStoreException,当被赋的值与数组(10.5)的组件类型不兼容时。

方法调用表达式也可以导致抛出异常,如果发生导致方法 body 的执行突然完成的异常。

类实例创建表达式也可以导致抛出异常,如果发生导致构造器执行突然完成的异常。

在表达式计算期间也可能出现各种链接和虚拟机错误。一般来说,此类错误很难预测并处理。

如果出现异常,则一个或多个表达式的计算在他们正常模式的所有步骤完成之前可被终止,这种表达式被称为突然完成。

如果表达式的计算需要子表达式的计算,则子表达式的突然完成总是导致表达式本身的间接的突然完成,以相同的理由,并且不执行正常计算模式中的所有随后的步骤。

术语“正常完成”和“突然完成”也适用于语句的执行(14.1)。语句可以因各种理由而突然完成,不仅仅是因为抛出异常。

15.7. 计算顺序

Java 编程语言保证,以特定的计算顺序计算出现的操作符的操作数,即,从左到右。

建议代码不要依赖此规范。当每个表达式包含至多一个副作用时,作为其最外层的操作,并且当代码不依赖于表达式从左向右计算的结果而产生的异常时,代码通常会更清晰。

15.7.1. 首先计算左侧的操作数

二元操作符左侧的操作数似乎在计算右侧操作数的任何部分之前被完全计算。

如果操作符是复合赋值操作符(15.26.2),则左侧操作数的计算包括,记住左侧操作数表示的变量,获取和保存该变量的值,以用于隐含的二元操作。

如果二元操作符的左侧操作数的计算突然完成,则右侧操作数的任何部分似乎都未被计算出来。

avatar

avatar

avatar

15.7.2. 在操作之前计算操作数

Java 编程语言保证,操作符的每个操作数(除条件操作符 &&、|| 和 ? : 之外)似乎在执行操作本身的任何部分之前被完全计算。

如果二元操作符是整数除法 /(15.17.2)或整数取余 %(15.17.3),则它的执行可能会抛出 ArithmeticException,但仅在计算二元操作符的两个操作数之后和仅当这些计算正常完成时抛出这一异常。

avatar

15.7.3. 计算遵循括号和优先级

Java 编程语言遵循由括号显式地和由操作符优先级隐式地指示的计算顺序。

Java 编程语言的实现可能不会利用代数恒等式,例如结合律,以将表达式重写为更方便的计算顺序,除非可以证明,代替表达式在值及其可观察的副作用上对于所有可能的涉及到的计算值都是等价的,即使在存在多线程执行(使用 17(Threads and Locks)中的线程执行模型)的情况下。

在浮点计算情况下,此规则也适用于无穷和非数字值。

例如,!(x<y) 不可以重写为 x>=y,因为如果 x 或 y 是 NaN,或两者都是 NaN时,这些表达式具有不同的值。

具体来说,看似数学结合的浮点计算不太可能是计算结合的。这种计算不能自然地重新排序。

例如,对 Java 编译器来说,将 4.0x0.5 重写为 2.0*x 是不正确的;虽然舍入在这里并不是一个问题,但有很大的 x 值,其使第一个表达式产生无穷(由于溢出),但第二个表达式产生有限的结果。

avatar

相反,整数加和乘在 Java 编程语言中被证明是结合的。

例如,a+b+c,其中 a、b 和 c 是局部变量(此简化假设避免涉及多线程和 volatile 变量问题),将总是产生相同的答案,不管作为 (a+b)+c 还是 a+(b+c) 计算;如果在代码附近出现了表达式 b+c,则智能的 Java 编译器可能能使用此通用子表达式。

15.7.4. 从左到右计算参数列表

在方法或构造器调用或类实例创建表达式中,参数表达式可以在括号中出现,以逗号分隔。每个参数表达式都看似在其右侧的任何参数表达式的任何部分之前完全计算。

如果参数表达式的计算突然完成,则其右侧的任何参数表达式的任何部分都看似不会被计算。

avatar

avatar

15.7.5. 其他表达式的计算顺序

某些表达式的计算顺序没有被这些一般规则完全覆盖,因为这些表达式会在必须指定的时候引发异常条件。请参见以下类型表达式的计算顺序的详细解释:

    * 类实例创建表达式(15.9.4)

    * 数组创建表达式(15.10.2)

    * 数组访问表达式(15.10.4)

    * 方法调用表达式(15.12.4)

    * 方法引用表达式(15.13.3)

    * 涉及数组组件的赋值(15.26)

    * lambda 表达式(15.27.4)

15.8. 主表达式

Primary:
    PrimaryNoNevArray
    ArrayCreationExpression

PrimarayNoNevArray:
    Literal
    ClassLiteral
    this
    TypeName . this
    ( Expression )
    ClassInstanceCreationExpression
    FieldAccess
    ArrayAccess
    MethodInvocation
    MethodReference

这部分的 Java 编程语言语法在两个方面是不寻常的。首先,我们可能期望简单名称,例如局部变量和方法参数的名称,作为主表达式。由于技术原因,在引入后缀表达式(15.14)后,名称将与主表达式组合在一起。

技术上的原因与允许 one-token 先行的 Java 程序的从左到右的解析有关。考虑表达式 (z[3]) 和 (z[])。第一个是带括号的数组访问(15.10.3),第二个是强制转换(15.16)的开头。在先行符号为 [ 的情况下,从左到右的解析将把 z 还原为非终结 Name。在强制转换上下文中,我们不希望将名称还原为 Primary,我们不希望将 名称还原为 Primary,但如果 Name 是 Primary 的替代项之一,则我们无法判断在不向前看两个标记的情况下是否要还原回跟着 [ 的标记(即,我们无法确定当前情况原来是带括号的数组访问还是强制转换)。此处提供的语法通过保持 Name 和 Primary 独立并运行某些其他语法规则(对于 ClassInstanceCreationException、MethodInvocation、ArrayAccess 和 PostfixExpression,而不是对于 FieldAccess,因为这直接使用标识符)来避免这一问题。此策略有效地推迟了 Name 是否应视为 Primary 这一问题,直到可以检查更多上下文为止。

第二个不寻常的特征避免了潜在的在表达式“new int[3][3]”中的语法歧义,此表达式在 Java 中总是意味着多维数组的单个创建,但如果没有适当的语法技巧,该表达式也可能被解释为与“(new int[3])[3]”相同的含义。

通过将预期的 Primary 定义划分为 Primary 和 PrimaryNoNewArray 来消除了这种歧义。(这可能拿来与将 Statement 拆分为 Statement 和 StatementNoShortIf 以避免“挂起 else”问题相比较。)

15.8.1. 词汇字面量

字面量(3.10)表示固定的、不变的值。

为了方便,以下来自 3.10 的产生式在这显示:

Literal:
    IntegerLiteral
    FloatingPointLiteral
    BooleanLiteral
    CharacterLiteral
    StringLiteral
    NullLiteral

字面量的类型按如下确定:

    * 以 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)。

    * 布尔字面量(3.10.3)的类型是 boolean(4.2.5)。

    * 字符字面量(3.10.4)的类型是 char(4.2.1)。

    * 字符串字面量(3.10.5)的类型是 String(4.3.3)。

    * null 字面量(3.10.7)的类型是 null 类型(4.1);它的值是 null 引用。

词汇字面量的计算总是正常完成。

15.8.2. 类字面量

类字面量是由类、接口、数组或基元类型的名称或伪类型 void,后跟一个 '.' 和标记 class,组成的表达式。

ClassLiteral:
    TypeName {[ ]} . class
    NumericType {[ ]} . class
    boolean {[ ]} . class
    void . class

C.class 的类型,其中 C 是类、接口或数组类型(4.3)的名称,是 Class。

p.class 的类型,其中 p 是基元类型(4.2)的名称,是 Class,其中 B 是在装箱转换(5.1.7)之后的类型 p 的表达式的类型。

void.class(8.4.5)的类型是 Class。

如果命名类型是类型变量(4.4)或参数化类型(4.5)或其元素类型是类型变量或参数化类型的数组,则这是一个编译时错误。

如果命名类型未表示这样一种类型,该类型是可访问的,且是在类字面量出现的点处的作用域中,则这是一个编译时错误。

类字面量计算为根据当前实例的类的定义类加载器(12.2)定义的命名类型(或 void)的 Class 对象。

15.8.3. this

关键字 this 仅可用在以下上下文中:

    * 在实例方法或 default 方法的 body 中(8.4.7,9.4.3)

    * 在类的构造器的 body中(8.8.7)

    * 在类的实力初始化器中(8.6)

    * 在类的实力变量的初始化器中(8.3.2)

    * 为了表示接收参数(8.4.1)

如果它出现在任何其他位置,则发生编译时错误。

关键字 this 可以用在 lambda 表达式中,仅当允许它出现在 lambda 表达式出现在的上下文中。否则,发生编译时错误。

当用作主表达式时,关键字 this 表示一个值,其引用一个对象,用此对象调用该实例方法或 default 方法(15.12),或引用正在被构造的对象。在 lambda body 中由 this 表示的值与在周围上下文中由 this 表示的值相同。

关键字 this 也用在显式构造器调用语句中(8.8.7.1)。

this 的类型是关键字 this 所出现在的类或接口类型 T。

default 方法提供了独一无二的在接口内访问 this 的功能。(所有其他接口方法都是 abstract 或 static 的,因此不提供对 this 的访问。)因此,this 有可能具有接口类型。

运行时,引用的实际对象的类可以是 T,如果 T 是类类型,或其是 T 的子类型的类。

avatar

15.8.4. 限定的 this

可以通过显式限定的关键字 this 来引用任何词法封闭的实例(8.1.3)。

让 T 是由 TypeName 表示的类型。让 n 是一个整数,这样,T 是限定的 this 表达式出现在的类或接口的第 n 个词法封闭类型声明。

TypeName.this 形式的表达式的值是 this 的第 n 个词法封闭实例。

表达式的类型是 T。

如果表达式出现在不是类 T 的内部类或 T 本身的类或接口中,则这是一个编译时错误。

15.8.5. 括号表达式

括号表达式是主表达式,其类型是包含的表达式的类型,其值在运行时在包含的表达式的值。如果包含的表达式表示一个变量,则该括号表达式也表示此变量。

括号的使用仅影响计算顺序,除了极端情况,即,(-2147483648) 和 (-9223372036854775808L) 是合法的,但 -(2147483648) 和 -(9223372036854775808L) 是不合法的。

这是因为十进制整数文本 2147483648 和 9223372036854775808L 只允许作为一元减号操作符(3.10.1)的操作数。

特别是,在表达式周围存在或缺少括号(除了上面提到的特殊情况之外)不以任何方式影响:

    * 用于 float 或 double 类型的表达式的值的值集(4.2.3)的选择。

    * 变量是否明确赋值,当 true 时明确赋值,当 false 时明确赋值,明确未赋值,当 true 时明确未赋值,或当 false 时明确未赋值(16(Definite Assignment))。

如果括号表达式出现在一个特定种类的目标类型 T(5(Conversions and Contexts))的上下文中,则它包含的表达式同样地出现在同一种目标类型 T 的上下文中。

如果包含的表达式是聚合表达式(15.2),则括号表达式也是聚合表达式。否则,它是一个独立表达式。

15.9. 类实例创建表达式

类实例创建表达式用于创建其为此类实例的新对象。

ClassInstanceCreationExpression:
    UnqualifiedClassInstanceCreationExpression
    ExpressionName . UnqualifiedClassInstanceCreationExpression
    Primary . UnqualifiedClassInstanceCreationExpression

UnqualifiedClassInstanceCreationExpression:
    new [TypeArguments] ClassOrInterfaceTypeToInstantiate ( [ArgumentList] ) [ClassBody]

ClassOrInterfaceTypeToInstantiate:
    {Annotation} Identifier {. {Annotation} Identifier} [TypeArgumentsOrDiamond]

TypeArgumentsOrDiamond:
    TypeArguments
    <>

为了方便,以下来自 15.12 的产生式在这显示:

ArgumentList:
    Expression {, Expression}

类实例创建表达式指定要实例化的类,如果正被实例化的类是泛型(8.1.2),则可能后跟类型参数(4.5.1)或尖括号(<>),后跟(可能为空)构造器实际值参数列表。

如果类的类型参数列表为空 — 尖括号形式 <> — 则推断类的类型参数。在尖括号的“<”和“>”之间加空格是合法的,尽管作为风格问题强烈不建议这样做。

如果构造器是泛型(8.8.4),则构造器的类型参数可以同样地推断或显式传递。如果显式传递,则构造器的类型参数直接跟在关键字 new 后面。

如果类实例创建表达式向构造器提供类型参数,但将尖括号形式的类型参数用于类,则这是一个编译时错误。

引入此规则是因为泛型类的类型参数的推断可能会影响泛型构造器的类型参数上的约束。

如果 TypeArguments 直接在 new 后面,或直接在 ( 前面,则如果任何类型参数是通配符(4.5.1),则这是一个编译时错误。

类实例创建表达式可以抛出的异常类型在 11.2.1 中指定。

类实例创建表达式具有两种形式:

    * 非限定的类实例创建表达式以关键字 new 开头。

      非限定的类实例创建表达式可用于创建类的实例,不管该类是顶层类(7.6)、成员类(8.5,9.5)、局部类(14.3)还是匿名类(15.9.5)。

    * 限定的类实例创建表达式以 Primary 表达式或 ExpressionName 开头。

      限定的类实例创建表达式能创建内部成员类及其匿名子类的实例。

非限定的和限定的类实例创建表达式都可以可选地以类 body 结尾。这种类实例创建表达式声明一个匿名类(15.9.5),并创建一个它的实例。

类实例创建表达式是聚合表达式(15.2),如果它将尖括号形式的类型参数用于类,并且它出现在赋值上下文或调用上下文(5.2,5.3)中。否则,它是一个独立表达式。

我们说,当通过类实例创建表达式创建类的实力时,类被实例化。类的实例化涉及确定要实例化的类(15.9.1),新创建的实例(15.9.2)的封闭实例(如果有),和要调用的用于创建新实例的构造器(15.9.3)。

15.9.1. 确定要实例化的类

如果类实例创建表达式以类 body 结尾,则要实例化的类是匿名类。然后:

    * 如果类实例创建表达式是非限定的:

      ClassOrInterfaceTypeToInstantiate 必须表示一个可访问的、非 final 的以及不是枚举类型的类;活着表示一个可访问的接口。否则,发生编译时错误。

      如果 ClassOrInterfaceTypeToInstantiate 以 <> 结尾,则发生编译时错误。

      如果 ClassOrInterfaceTypeToInstantiate 以 TypeArguments 结尾,则 ClassOrInterfaceTypeToInstantiate 必须表示一个格式良好的参数化类型(4.5),否则发生编译时错误。

      让 T 是由 ClassOrInterfaceTypeToInstantiate 表示的类型。如果 T 表示一个类,则声明一个 T 的匿名的直接子类。如果 T 表示一个接口,则声明一个 Object 的匿名的直接子类,其实现了 T。在这两种情况下,子类的 body 是在类实例创建表达式中给定的 ClassBody。

      被实例化的类是匿名子类。

    * 如果类实例创建表达式是限定的:

      ClassOrInterfaceTypeToInstantiate 必须明确地表示一个其是可访问的、非 final 的、非枚举类型的以及 Primary 表达式或 ExpressionName 的编译时类型的成员的内部类。否则,发生编译时错误。

      如果 ClassOrInterfaceTypeToInstantiate 以 <> 结尾,则发生编译时错误。

      如果 ClassOrInterfaceTypeToInstantiate 以 TypeArguments 结尾,则 ClassOrInterfaceTypeToInstantiate 必须表示一个格式良好的参数化类型,否则,发生编译时错误。

      让 T 是由 ClassOrInterfaceTypeToInstantiate 表示的类型。声明一个 T 的匿名直接子类。子类的 body 是在类实例创建表达式中给定的 ClassBody。

      被实例化的类是匿名子类。

如果类实例创建表达式未声明匿名类,则:

    * 如果类实例创建表达式是非限定的:

      ClassOrInterfaceTypeToInstantiate 必须表示一个可访问的、非 abstract 的以及非枚举类型的类。否则,发生编译时错误。

      如果 ClassOrInterfaceTypeToInstantiate 以 <> 结尾,但由 ClassOrInterfaceTypeToInstantiate 表示的类不是泛型,则发生编译时错误。

      如果 ClassOrInterfaceTypeToInstantiate 以 TypeArguments 结尾,则 ClassOrInterfaceTypeToInstantiate 必须表示一个格式良好的参数化类,否则,发生编译时错误。

      被实例化的类是由 ClassOrInterfaceTypeToInstantiate 表示的类。

    * 如果类实例创建表达式是限定的:

      ClassOrInterfaceTypeToInstantiate 必须明确地表示一个内部类,其是可访问的、非 abstract 的、非枚举类型以及 Primary 表达式或 ExpressionName 的编译时类型。

      如果 ClassOrInterfaceTypeToInstantiate 以 <> 结尾,由 ClassOrInterfaceTypeToInstantiate 表示的类不是泛型,则发生编译时错误。

      如果 ClassOrInterfaceTypeToInstantiate 以 TypeArguments 结尾,则 ClassOrInterfaceTypeToInstantiate 必须表示一个格式良好的参数化类,否则,发生编译时错误。

      被实例化的类是由 ClassOrInterfaceTypeToInstantiate 表示的类。

15.9.2. 确定封闭实例

让 C 是被实例化的类,让 i 是被创建的实例。如果 C 是内部类,则 i 具有一个直接封闭实例(8.1.3),按如下确定:

    * 如果 C 是一个匿名类,则:

        如果类实例创建表达式出现在 static 上下文中,则 i 不具有直接封闭实例。

        否则,i 的直接封闭实例是 this。

    * 如果 C 是局部类,则:

        如果 C 出现在 static 上下文中,则 i 不具有直接封闭实例。

        否则,如果类实例创建表达式出现在 static 上下文中,则发生编译时错误。

        否则,让 O 是 C 的直接封闭类。让 n 是一个整数,这样 O 是类实例创建表达式出现在其中的类的第 n 个词法封闭类型声明。

        i 的直接封闭实例是 this 的第 n 个词法封闭实例。

    * 如果 C 是内部成员类,则:

        如果类实例创建表达式是非限定的,则:

            如果类实例创建表达式出现在 static 上下文中,则发生编译时错误。

            否则,如果 C 是一个类的成员,此类封闭类实例创建表达式所出现在的类,则让 O 是直接封闭类,其中 C 是一个成员。让 n 是一个整数,这样 O 是类实例创建表达式出现在的类的第 n 个词法封闭类型声明。

            i 的直接封闭实例是 this 的第 n 个词法封闭实例。

            否则,发生编译时错误。

        如果类实例创建表达式是限定的,则 i 的直接封闭实例是其为 Primary 表达式或 ExpressionName 的值的对象。

如果 C 是匿名类,它的直接父类 S 是内部类,则 i 可能具有一个有关 S 的直接封闭实例,按如下确定:

    * 如果 S 是局部类,则:

        如果 S 出现在 static 上下文中,则 i 不具有有关 S 的直接封闭实例。

        否则,如果类实例创建表达式出现在 static 上下文中,则发生编译时错误。

        否则,让 O 是 S 的直接封闭类。让 n 是一个整数,这样 O 是类实例创建表达式出现在的类的第 n 个词法封闭类型声明。

        i 的有关 S 的直接封闭实例是 this 的第 n 个词法封闭实例。

    * 如果 S 是内部成员类,则:

        如果类实例创建表达式是非限定的,则:

            如果类实例创建表达式出现在 static 上下文中,则发生编译时错误。

            否则,如果 S 是一个类的成员,其封闭类实例创建表达式出现在的类,则让 O 是直接封闭类,其中 S 是一个成员。让 n 是一个整数,这样 O 是类实例创建表达式出现在的类的第 n 个词法封闭类型声明。

            i 的有关 S 的直接封闭实例是 this 的第 n 个词法封闭实例。

            否则,发生编译时错误。

        如果类实例创建表达式是限定的,则 i 的有关 S 的直接封闭实例是其为 Primary 表达式或 ExpressionName 的值的对象。

15.9.3. 选择构造器和其参数

让 C 是被实例化的类。为了创建 C 的实例,i,通过以下规则在编译时选择一个 C 的构造器:

首先,确定构造器调用的实际参数:

    * 如果 C 是具有直接父类 S 的匿名类,则:

        如果 S 不是内部类,或如果 S 是出现在 static 上下文中的局部类,则构造器的参数是类实例创建表达式的参数列表中的参数,如果有,以他们在表达式中出现的顺序。

        否则,构造器的第一个参数是 i 的有关 S 的直接封闭实例(15.9.2),构造器后面的参数是类实例创建表达式的参数列表中参数,如果有,以他们在类实例创建表达式中出现的顺序。

    * 如果 C 是局部类或 private 内部成员类,则构造器的参数是类实例创建表达式的参数列表中的参数,如果有,以他们在类实例创建表达式中出现的顺序。

    * 如果 C 是非 private 内部成员类,则构造器的第一个参数是 i 的直接封闭实例(8.8.1,15.9.2),它的构造器后面的参数是类实例创建表达式的参数列表中的参数,如果有,以他们在类实例创建表达式中出现的顺序。

    * 否则,构造器的参数是类实例创建表达式的参数列表中的参数,如果有,以他们在类实例创建表达式中出现的顺序。

其次,确定 C 的构造器和相应的返回类型和 throws 子句:

    * 如果类实例创建表达式使用 <> 来省略类类型参数,则为了重载解析和类型参数推断,定义一组方法 m1...mn。

      让 c1...cn 是类 C 的构造器。让 #m 是自动生成的不同于 C 中所有构造器和方法名称的名称。对于所有 j (1 ≤ j ≤ n),mj 是根据 cj 按如下定义:

        首先将实例化 cj 中的类型定义为替换 θj。

        让 F1...Fp 是 C 的类型参数,让 G1...Gq 是 cj 的类型参数(如果有)。让 X1...Xp 和 Y1...Yq 是具有不同名称的类型变量,其不在 C 的 body 的作用域中。

        θj 是 [F1:=X1, ..., Fp:=Xp, G1:=Y1, ..., Gq:=Yq]。

        mj 的修饰符是 cj 的那些。

        mj 的类型参数是 X1...Xp,Y1...Yq。如果有,每个参数的边界是适用于 C 或 cj 中的相应参数边界的 θj。

        mj 的返回类型是适用于 C<F1,...,Fp> 的 θj。

        mj 的名称是 #m。

        mj 的参数列表(可能为空)是适用于 cj 的参数类型的 θj。

        mj 的抛出类型列表(可能为空)是适用于 cj 的抛出类型的 θj。

        mj 的 body 是无关的。

      为了选择一个构造器,我们暂时认为 m1...mn 是 C 的成员。然后选择 m1...mn 中的一个,由类实例创建的参数表达式确定,使用 15.12.2 中指定的处理过程。

      如果没有可应用的和可访问的独一无二的最具体的方法,则发生编译时错误。

      否则,其中 mj 是选择的方法,cj 是选中的构造器。cj 的返回类型和 throws 子句与为 mj 确定的返回类型和 throws 子句相同(15.12.2.6)。

    * 否则,类实例创建表达式不使用 <> 来忽略类类型参数。

      让 T 是表达式中由后跟任何类类型参数的 C 的表示的类型。在 15.12.2 中指定的处理过程,修改为处理构造器,用于选择 T 的构造器之一和确定它的 throws 子句。

      如果没有可应用的和可访问的独一无二的最具体的构造器,则发生编译时错误(就像在方法调用中一样)。

      否则,返回类型是 T。

如果类实例创建表达式的参数与它的目标类型不兼容,则这是一个编译时错误,派生自调用类型的也一样(15.12.2.6)。

如果编译时声明适用于可变数量参数调用(15.12.2.4),则其中构造器的调用类型的最后一个形式参数类型是 Fn[],如果其是 Fn 的擦除的类型在调用点处是不可访问的,这是一个编译时错误。

类实力创建表达式的类型是选择的构造器的返回类型,如上所定义。

请注意,类实例创建表达式的类型可能是匿名类类型,在这种情况下,被调用的构造器是匿名构造器(15.9.5.1)。

15.9.4. 类实例创建表达式的运行时计算

运行时,类实例创建表达式的计算如下。

首先,如果类实例创建表达式是限定的类实例创建表达式,则将计算限定的主表达式。如果限定的表达式计算为 null,则抛出 NullPointerException,类实例创建表达式突然完成。如果限定的表达式突然完成,则类实例创建表达式因相同的理由而突然完成。

下一步,为新的类实例分配空间。如果没有足够的空间来为对象分配,则通过抛出 OutOfMemoryError,类实例创建表达式的计算突然完成。

新对象包含新实例的在指定类类型及其父类中声明的所有字段。随着每个新字段实例被创建,它被初始化为它的默认值(4.12.5)。

下一步,计算构造器的实际参数,从左到右。如果任何参数计算突然完成,则不再计算其右侧的任何参数表达式,类实例创建表达式因相同的理由而突然完成。

下一步,调用选定的指定类类型的构造器。这导致调用类类型的每个父类的至少一个构造器。这一过程可由显式的构造器调用语句(8.8)管理,并在 12.5 中详细指定。

类实例创建表达式的值是指定类的新创建对象的引用。每次计算此表达式,创建一个新的对象。

avatar

15.9.5. 匿名类声明

匿名类声明是由 Java 编译器自动从类实例创建表达式派生的。

匿名类从不是 abstract(8.1.1.1)。

匿名类总是隐式 final(8.1.1.2)。

匿名类总是内部类(8.1.3);它从不是 static(8.1.1,8.5.1)。

15.9.5.1. 匿名构造器

匿名类不能有显式声明的构造器。相反,为匿名类隐式声明一个匿名构造器。具有直接父类 S 的匿名类 C 的匿名构造器形式如下:

    * 如果 S 不是内部类,或如果 S 是出现在 static 上下文中的局部类,则对于声明 C 的类实例创建表达式的每个实际参数,匿名构造器都有一个形式参数。

      类实例创建表达式的实际参数用于确定 S 的构造器 cs,使用与相同的方法调用(15.12)规则。匿名构造器的每个形式参数的类型与 cs 相应的形式参数必须是等价的。

      构造器 body 由 super(...) 形式的显式构造器调用(8.8.7.1)组成,其中实际参数是构造器的形式参数,以声明他们的顺序。

    * 否则,C 的构造器的第一个形式参数表示有关 S 的 i 的直接封闭实例的值(15.9.2,15.9.3)。this 参数的类型是直接封闭 S 的声明的类类型。

      对于声明匿名类的类实例创建表达式的每个实际参数,构造器具有一个额外的形式参数。第 n 个形式参数对应第 n-1 个实际参数。

      类实例创建表达式的实际参数用于确定 S 的构造器 cs,使用与方法调用(15.12)相同的规则。匿名构造器的每个形式参数的类型与 cs 相应的形式参数必须是等价的。

      构造器 body 由 super(...) 形式的显式构造器调用(8.8.7.1)组成,其中 o 是构造器的第一个形式参数,实际参数是构造器后面的形式参数,以声明他们的顺序。

在所有情况下,匿名构造器的 throws 子句必须列举出由在匿名构造器中包含的显式父类构造器调用语句抛出的所有已检查异常,和由匿名类的任何实例初始化器或实例变量初始化器抛出的所有已检查异常。

请注意,匿名构造器可以引用不可访问的类型(例如,如果这种在父类构造器 cs 的签名中出现的类型)。这本身不会导致编译时或运行时出现任何错误。

15.10. 数组创建和访问表达式



15.10.1. 数组创建表达式

数组创建表达式用于创建新数组(10(Arrays))。

ArrayCreationExpression:
    new PrimitiveType DimExprs [Dims]
    new ClassOrInterfaceType DimExprs [Dims]
    new PrimitiveType Dims ArrayInitializer
    new ClassOrInterfaceType Dims ArrayInitializer

DimExprs:
    DimExpr {DimExpr}

DimExpr:
    {Annotation} [ Expression ]

为了方便,以下来自 4.3 的产生式在这显示:

Dims:
    {Annotation} [ ] {{Annotation} [ ]}

数组创建表达式创建一个新数组对象,其元素是由 PrimitiveType 或 ClassOrInterfaceType 指定的类型。

如果 ClassOrInterfaceType 不表示可具体化的类型(4.7),则这是一个编译时错误。否则,ClassOrInterfaceType 命名任何命名引用类型,即使是 abstract 类类型(8.1.1.1)或接口类型。

以上规则意味着,数组创建表达式中的元素类型不能是参数化类型,除非参数化类型的所有类型参数都是无边界通配符。

DimExpr 中每个维度表达式的类型必须是可转换(5.1.8)为整数类型的类型,否则发生编译时错误。

每个维度表达式都经历一元数字提升(5.6.1)。提升后的类型必须是 int,否则发生编译时错误。

数组创建表达式的类型是数组类型,其可由删除 new 关键字、每个 DimExpr 表达式和数组初始化器后的数组创建表达式的副本表示。

avatar

15.10.2. 数组创建表达式的运行时计算

运行时,数组创建表达式的计算行为表现如下:

    * 如果没有维度表达式,则必须有数组初始化器。将用由 10.6 中所述的数组初始化器提供的值初始化新分配的数组。数组初始化器的值变成数组创建表达式的值。

    * 否则,没有数组初始化器,且:

        首先,计算维度表达式,从左向右。如果任何表达式的计算突然完成,则不计算其右侧的表达式。

        下一步,检查维度表达式的值。如果任何 DimExpr 表达式的值小于零,则抛出 NegativeArraySizeException。

        下一步,为新数组分配空间。如果没有足够的空间分配数组,则通过抛出 OutOfMemoryError 使数组创建表达式的计算突然完成。

        然后,如果出现单个 DimExpr,则创建一个指定长度的一维数组,将数组的每个组件初始化为默认值(4.12.5)。

        否则,如果出现 n 个 DimExpr,则数组创建实际上执行一组深度为 n-1 的嵌套循环,以创建数组隐含的数组。

        多维数组不必在每一层都具有相同长度的数组。

avatar avatar

avatar avatar

如果数组创建表达式的计算发现,没有足够的内存来进行创建操作,则抛出 OutOfMemoryError。如果数组创建表达式不具有数组初始化器,则这一检查仅发生在所有维度表达式正常完成之后。如果数组创建表达式确实有数组初始化器,则当在变量初始化器表达式计算期间分配引用类型对象,或当为数组分配持有数组初始化器的值(可能嵌套)时,可以发生 OutOfMemoryError。

avatar

15.10.3. 数组访问表达式

数组访问表达式引用其为数组组件的变量。

ArrayAccess:
    ExpressionName [ Expression ]
    PrimaryNoNevArray [ Expression ]

数组访问表达式包含两个子表达式,数组引用表达式(左方括号之前)和索引表达式(方括号内)。

请注意,数组引用表达式可以是名称或任何不是数组创建表达式(15.10)的主表达式。

数组引用表达式的类型必须是数组类型(称其为 T[],其组件是 T 类型的数组),否则,发生编译时错误。

索引表达式经历一元数字提升(5.6.1)。提升的类型必须是 int,否则,发生编译时错误。

数组访问表达式的类型是应用到 T 的捕获转换(5.1.10)的结果。

数组访问表达式的结果是 T 类型的变量,即数组内由索引表达式的值选择的变量。

作为结果的变量,其是数组的组件,从不被认为是 final,即使数组引用表达式表示一个 final 变量。

15.10.4. 数组访问表达式的运行时计算

运行时,数组访问表达式的计算行为表现如下:

    * 首先,计算数组引用表达式。如果此计算突然完成,则数组访问因相同的理由而突然完成,不计算索引表达式。

    * 否则,计算索引表达式。如果此计算突然完成,则数组访问表达式因相同的理由而突然完成。

    * 否则,如果数组引用表达式的值是 null,则抛出 NullPointerException。

    * 否则,数组引用表达式的值实际引用一个数组。如果索引表达式的值小于零,或者大于或等于数组的长度,则抛出 ArrayIndexOutOfBoundsException。

    * 否则,数组访问的结果是 T 类型的变量,在数组内,由索引表达式的值选择的。

avatar

avatar

avatar avatar

15.11. 字段访问表达式

字段访问表达式可以访问对象或数组的字段,其为表达式或特殊关键字 super 的值的引用。

FieldAccess:
    Primary . Identifier
    super . Identifier
    TypeName . super . Idnetifier

使用与限定名称(6.5.6.2)相同的规则确定字段访问表达式的含义,但被表达式不能表示包、类类型或接口类型这一事实限制。

可以通过使用简单名称(6.5.6.1)来引用当前实例或当前类的字段。

15.11.1. 使用 Primary 进行字段访问

Primary 的类型必须是引用类型 T,否则,发生编译时错误。

按如下确定字段访问表达式的含义:

    * 如果标识符命名几个类型 T 中可访问(6.6)的成员字段,则字段访问是模糊不清的,并发生编译时错误。

    * 如果标识符未命名类型 T 中可访问的成员字段,则字段访问是未定义的,并发生编译时错误。

    * 否则,标识符命名单个类型 T 中可访问的成员字段,字段访问表达式的类型是捕获转换(5.1.10)之后的成员字段的类型。

运行时,按如下计算字段访问表达式的类型:(假定程序在有关明确赋值分析方面是正确的,即,每个空 final 变量在访问之前是明确赋值的)

    * 如果字段是 static:

        计算 Primary 表达式,丢弃结果。如果 Primary 表达式的计算突然完成,则字段访问表达式因相同的理由而突然完成。

        如果字段是非空 final 字段,则结果是其为 Primary 表达式的类型的类或接口中指定类变量的值。

        如果字段不是 final,或是空 final,字段访问出现在 static 初始化器或类变量初始化器中,则结果是一个变量,即,其为 Primary 表达式的类型的类中指定的类变量。

    * 如果字段不是 static:

        计算 Primary 表达式。如果 Primary 表达式的计算突然完成,则字段访问表达式因相同的理由而突然完成。

        如果 Primary 的值是 null,则抛出 NullPointerException。

        如果字段是非空 final,则结果是类型 T 中由 Primary 的值引用的对象中找到的命名成员字段的值。

        如果字段不是 final,或是空 final,字段访问出现在构造器或实例变量初始化器中,则结果是一个变量,即,类型 T 中由 Primary 的值引用的对象中找到的命名成员字段。

请注意,只有 Primary 表达式的类型,而不是运行时引用的实际对象的类,用于确定使用的字段。

avatar avatar

avatar

15.11.2. 使用 super 访问父类成员

super.Identifier 形式引用当前对象的名为 Identifier 的字段,但当前对象被看做当前类的父类的实例。

T.super.Identifier 形式引用对应 T 的词法封闭实例的名为 Identifier 的字段,但该实例被看做 T 的父类的实例。

使用关键字 super 的形式仅在实例方法、实例初始化器,或类的构造器,或类的实例变量初始化器中是合法的。如果他们出现在其他任何位置,则发生编译时错误。

这些情况与在类声明中可以使用关键字 this 的情况完全相同(15.8.3)。

如果使用 super 的形式出现在类 Object 的声明中,则这是一个编译时错误,因为 Object 没有父类。

假定字段访问表达式 super.f 出现在类 C 中,C 的直接父类是 S。如果 S 中的 f 从类 C(6.6)中是可访问的,则 super.f 看起来就好像它是类 S 的 body 中的 this.f 表达式一样。否则,发生编译时错误。

因此,super.f 可以访问在类 S 中可访问的字段 f,即使该字段被类 C 中字段 f 的声明隐藏了。

假定字段访问表达式 T.super.f 出现在类 C 中,由 T 表示的类的直接父类是其完全限定名称是 S 的类。如果 S 中的 f 从 C 中是可访问的,则 T.super.f 看起来就好像它是类 S 的 body 中的 this.f 表达式一样。否则,发生编译时错误。

因此,T.super.f 可以访问在类 S 中可访问的字段 f,即使该字段被类 T 中字段 f 的声明隐藏了。

如果当前类不是类 T 的内部类或 T 本身,则这是一个编译时错误。

avatar

15.12. 方法调用表达式

方法调用表达式用于调用类或实例方法。

MethodInvocation:
    MthodName ( [ArgumentList] )
    TypeName . [TypeArguments] Identifier ( [ArgumentList] )
    ExpressionName . [TypeArguments] Identifier ( [ArgumentList] )
    Primary . [TypeArguments] Identifier ( [ArgumentList] )
    super . [TypeArguments] Identifier ( [ArgumentList] )
    TypeName . super . [TypeArguments] Identifier ( [ArgumentList] )

ArgumentList:
    Expression {, Expression}

由于方法重载的可能性,编译时解析方法名称比解析字段名称更复杂。由于实例方法重写的可能性,运行时调用方法也比访问字段更复杂。

确定将被方法调用表达式调用的方法涉及几个步骤。以下三节描述了方法调用的编译时处理过程。方法调用表达式类型的确定在 15.12.3 中指定。

方法调用表达式可以抛出的异常类型在 11.2.1 中指定。

如果在 MethodInvocation 中的 ( 之前出现的最右侧的“.”左边的名称无法归类为 TypeName 或 ExpressionName(6.5.2),则这是一个编译时错误。

如果 Identifier 左侧的 TypeArguments 存在,则如果只要有类型参数是通配符(4.5.1),则这是一个编译时错误。

方法调用表达式是聚合表达式,如果以下所有都为 true:

    * 调用出现在赋值上下文或调用上下文中(5.2,5.3)。

    * 如果调用是限定的(即,除第一个之外任何形式的 MethodInvocation),则调用省略 Identifier 左侧的 TypeArguments。

    * 由以下子章节确定的调用方法是泛型(8.4.4),它的返回类型至少提及到了该方法的类型参数之一。

否则,方法调用表达式是独立表达式。

15.12.1. 编译时步骤 1:确定要搜索的类或接口

编译时处理方法调用的第一步是找出要调用的方法名称以及要搜索具有该名称的方法定义的类或接口。

方法名称由 MethodName 或 Identifier 指定,其直接出现在 MethodInvocation 的左圆括号前面。

对于要搜索的类或接口,有六种情况需要考虑,依赖于出现在 MethodInvocation 的左圆括号前面的形式:

    * 如果形式是 MethodName,即,一个 Identifier,则:

      如果 Identifier 出现在具有该名称的可见方法声明的作用域中(6.3,6.4.1),则:

        如果有一个该方法是其成员的封闭类型声明,让 T 是最里面的这个类型声明。要搜索的类或接口是 T。

        此搜索策略被称为“梳妆法则”。在查找封闭类和其父类层次结构中的方法之前,它实际上先查找嵌套的类的父类层次结构。参见 6.5.7.1 中的示例:

        否则,由于一个或多个单静态导入或按需静态导入声明,可见方法可能在作用域内。没有要搜索的类或接口,因为被调用的方法在后面确定(15.12.2.1)。

    * 如果形式是 TypeName . [TypeArguments] Identifier,则要搜索的类型是由 TypeName 表示的类型。

    * 如果形式是 ExpressionName . [TypeArguments] Identifier,则要搜索的类或接口是由 ExpressionName 表示的变量的声明类型 T,如果 T 是类或接口类型,或 T 的上边界,如果 T 是类型变量。

    * 如果形式是 Primary . [TypeArguments] Identifier,则让 T 是 Primary 表达式的类型。要搜索的类或接口是 T,如果 T 是类或接口类型,或 T 的上边界,如果 T 是类型变量。

      如果 T 不是引用类型,则这是一个编译时错误。

    * 如果形式是 super . [TypeArguments] Identifier,则要搜索的类是其声明包含该方法调用的类的父类。

      让 T 是直接封闭方法调用的类型声明。如果 T 是类 Object 或 T 是接口,则这是一个编译时错误。

    * 如果形式是 TypeName . super [TypeArguments] Identifier,则:

        如果 TypeName 既不表示类也不表示接口,则这是一个编译时错误。

        如果 TypeName 表示类 C,C,则要搜索的类是 C 的父类。

        如果 C 不是当前类的词法封闭类型声明,或如果 C 是类 Object,则这是一个编译时错误。

        让 T 是直接封闭方法调用的类型声明。如果 T 是类 Object,则这是一个编译时错误。

        否则,TypeName 表示要搜索的接口,I。

        让 T 是直接封闭方法调用的类型声明。如果 I 不是 T 的直接父接口,或如果存在某个其他直接父类或 T 的直接父接口,J,使 J 是 I 的子类型,则这是一个编译时错误。

avatar avatar

15.12.2. 编译时步骤 2:确定方法签名

第二步搜索前一步为成员方法确定的类型。这一步使用方法名称和参数表达式来定位可访问和可应用的访问,即,可以在给定参数上正确调用的声明。

可能有多个这种方法,这种情况下,选择最具体的一个。最具体的方法的描述符(签名加返回类型)是运行时用于执行方法分派的一个。

一个方法是可应用的,如果通过严格调用(15.12.2.2)、松散调用(15.12.2.3)或可变参数调用(15.12.2.4)之一它是可应用的。

某些包含隐式类型 lambda 表达式(15.27.1)或不精确方法引用(15.13.1)的参数表达式将被适用性测试忽略,因为在选择目标类型之前无法确定他们的含义。

尽管方法调用可能是聚合表达式,仅它的参数表达式 - 而不是调用的目标类型 - 影响可应用方法的选择。

通过确定潜在的可应用方法(15.12.2.1)来开始确定适用性过程。

该过程的其余部分分为三个阶段,以确保与 Java SE 5.0 之前的 Java 编程语言版本的兼容性。这些阶段是:

  1. 第一个阶段(15.12.2.2)执行不允许装箱或拆箱转换或可变参数方法调用的重载解析。如果此阶段没找到可应用的方法,则处理继续到第二阶段。

    这保证,Java SE 5.0 之前的 Java 编程语言中合法的任何调用都不被认为是不明确的,这是由于可变参数方法、隐式装箱和/或拆箱的引入。但是,可变参数方法(8.4.1)的声明可以改变为给定方法调用表达式选择的方法,因为可变参数方法在第一阶段被视为固定参数方法。例如,在已经声明 m(Object) 的类中声明 m(Object...) 导致 m(Object) 不再因某些调用表达式(例如 m(null))而被调用,因为 m(Object) 更具体。

  1. 第二阶段(15.12.2.3)执行重载解析,同时允许装箱和拆箱,但仍然阻止可变参数方法调用。如果此阶段期间找不到适用的方法,则处理继续到第三阶段。

    这可确保,从不通过可变参数方法调用选择方法,如果它通过固定参数方法调用是适用的。

  1. 第三阶段(15.12.2.4)允许将重载与可变参数方法、装箱和拆箱相结合。

在泛型方法(8.4.4)情况下,确定方法是否适用需要类型参数的分析。类型参数可以显式或隐式地传递。如果隐式地传递他们,则必须从参数表达式中推断出(18(Type Inference))类型参数的边界。

如果三个适用性测试阶段之一期间标识了几个可应用方法,则选择最具体的那个,如 15.12.2.5 节中所指定的那样。

为了检查适用性,一般来说,调用的参数的类型不能输入到分析中。这是因为:

    * 方法调用的参数可能是聚合表达式

    * 在缺少目标类型的情况下,无法确定聚合表达式的类型

    * 重载解析在知道参数的目标类型之前就已经完成

相反,适用性检查的输入是参数表达式的列表,其可用于检查与潜在的目标类型的兼容性,即使不知道表达式的最终类型。

请注意,重载解析与目标类型无关。这有两个原因:

    * 第一,它使用户模型更易于访问,更不同意出错。方法名称(即,与名称对应的声明)的含义对于程序含义如此重要,以致于依赖微妙的上下文提示。(相比之下,其他聚合表达式可能具有不同的行为,具体取决于目标类型;但行为的变化始终是有限的,且实质上是等价的,而无法对任意一组共享名称和数量的方法的行为做出这种保证。)

    * 第二,它允许其他属性 - 例如,方法是否为聚合表达式(15.12),或者如何对条件表达式(15.25)进行分类 - 以依赖方法名称的含义,即使在知道目标类型之前。

avatar avatar avatar

avatar

avatar avatar avatar avatar avatar

15.12.2.1. 识别潜在可用的方法

由编译时步骤 1(15.12.1)确定的类或接口用于搜索对此方法调用潜在可用的所有成员方法;继承自父类和父接口的成员也包括在这一搜索中。

此外,如果方法调用表达式的形式是 Method - 即,单个 Identifier - 则潜在可用方法的搜索也检查所有通过该方法调用出现在的编译单元的单静态导入声明和按需静态导入声明导入的,和在方法调用出现的点处未被遮蔽的成员方法。

成员方法对于方法调用时潜在可用的,当且仅当以下所有都为 true:

    * 成员的名称与方法调用中方法的名称相等。

    * 成员在方法调用出现在的类或接口中是可访问的(6.6)。

      成员方法在方法调用中是否可以访问取决于成员声明中的访问修饰符(公共、受保护、无修饰符(包访问或私有))以及方法调用出现的位置。

    * 如果成员是具有 n 个参数的固定参数方法,则方法调用的参数数量等于 n,对于所有 i(1 ≤ i ≤ n),方法调用的第 i 个参数与方法的第 i 个参数的类型是潜在兼容的,如下面定义的那样。

    * 如果成员是具有 n 个参数的可变参数方法,则对于所有 i(1 ≤ i ≤ n-1),方法调用的第 i 个参数与方法的第 i 个参数的类型是潜在兼容的;并且,其中方法的第 n 个参数具有类型 T[],以下之一为 true:

        方法调用的参数数量等于 n-1。

        方法调用的参数数量等于 n,方法调用的第 n 个参数与 T 或 T[] 是潜在兼容的。

        方法调用的参数数量是 m,其中 m > n,对于所有 i(n ≤ i ≤ m),方法调用的第 i 个参数与 T 是潜在兼容的。

    * 如果方法调用包含显式类型参数,成员是泛型方法,则类型参数的数量等于方法的类型参数的数量。

      这一条款意味着,非泛型方法可以适用于提供显式类型参数的方法调用。实际上,这被证明是可行的。在这种情况下,类型参数将被简单地忽略。

      这一规则源于兼容性和替代原则问题。因为接口或父类可以独立于他们的子类型而泛型化,我们可以用非泛型方法来重写泛型方法。但是,重写(非泛型)方法必须适用于泛型方法的调用,包括显式传递类型参数的调用。否则,子类型将无法替代其泛型化的父类型。

如果搜索无法找到至少一个可适用的方法,则发生编译时错误。

根据以下规则,表达式与目标类型是潜在兼容的:

    * lambda 表达式(15.27)与函数式接口类型(9.8)是潜在兼容的,如果以下所有都为 true:

        目标类型的函数类型的参数数量与 lambda 表达式的参数数量是相同的。

        如果目标类型的函数类型具有 void 返回,则 lambda body 是一个语句表达式(14.8)或 void 兼容的块(15.27.2)。

        如果目标类型的函数类型具有(非 void)返回类型,则 lambda body 是一个表达式或值兼容的块(15.27.2),

    * 方法引用表达式(15.13)与函数式接口类型是潜在兼容的,如果,其中类型的函数类型参数数量是 n,存在至少一个具有 n 个参数的方法引用表达式的可适用方法(15.13.1),并且以下之一为 true:

        方法引用表达式具有形式 Reference :: [TypeArguments] Identifier,至少一个可适用的方法是 i) static 并支持 n 个参数,或 ii) 非 static 并支持 n-1 个参数。

        方法引用表达式具有某种其他形式,至少一个可适用的方法是非 static 的。

    * lambda 表达式或方法引用表达式与类型变量是潜在兼容的,如果类型变量是候选人方法的类型参数。

    * 括号化表达式(15.8.5)与类型是潜在兼容的,如果它包含的表达式与该类型是潜在兼容的。

    * 条件表达式(15.25)与类型是潜在兼容的,如果其第二个和第三个操作数表达式每个与该类型都是潜在兼容的。

    * 类实例创建表达式、方法调用表达式或独立形式的表达式(15.2)与任何类型是潜在兼容的。

潜在适用性的定义超出了基本的参数数量检查,也考虑了函数式接口目标类型的存在和“形状”。在某些涉及类型参数推断的情况下,作为方法调用参数出现的 lambda 表达式无法在重载解析之前被正确地确定类型。这些规则仍然允许考虑 lambda 表达式,丢弃明显不正确的可能导致歧义错误的目标类型。

15.12.2.2. 阶段 1:识别适用于严格调用的匹配参数数量的方法

我们认为参数表达式与潜在可适用的方法的适用性相关,除非它具有以下形式之一:

    * 隐式确定类型的 lambda 表达式(15.27.1)。

    * 不精确的方法引用表达式(15.13.1)。

    * 如果 m 是泛型方法,方法调用不提供显式类型参数,则显式确定类型的 lambda 表达式或精确的方法引用表达式,其对应的目标类型(像派生自 m 的签名一样)是 m 的类型参数。

    * 显式确定类型的 lambda 表达式,其 body 是与适用性无关的表达式。

    * 显式确定类型的 lambda 表达式,其 body 是块,其中至少有一个结果表达式与适用性无关。

    * 括号化表达式(15.8.5),其包含的表达式与适用性无关。

    * 条件表达式(15.25),其第二个和第三个操作数与适用性无关。

让 m 是可适用的方法(15.12.2.1),其具有 n 个参数和形式参数类型 F1 ... Fn,让 e1, ..., en 是方法调用的实际参数表达式。则:

    * 如果 m 是泛型方法,方法调用不提供显式类型参数,则按 18.5.1 中所指定的推断方法适用性。

    * 如果 m 是泛型方法,方法调用提供了显式类型参数,则让 R1 ... Rp(p ≥ 1)是 m 的类型参数,让 Bl 是 Rl1 ≤ l ≤ p)的声明边界,让 U1, ..., Up 是在方法调用中给定的显式类型参数。然后如果以下两种情况都为 true,则 m 适用于严格调用:

        对于 1 ≤ i ≤ n,如果 ei 与适用性有关,则 ei 在严格调用上下文中与 Fi[R1:=U1, ..., Rp:=Up] 是兼容的。

        对于 1 ≤ l ≤ p,Ul <: Bl[R1:=U1, ..., Rp:=Up]。

    * 如果 m 不是泛型方法,则如果对于 1 ≤ i ≤ n,ei 在严格调用上下文中与 Fi 是兼容的或 ei 与适用性是无关的,则 m 适用于严格调用。

如果找不到适用于严格调用的方法,则适用方法的搜索继续道阶段 2(15.12.2.3)。

否则,从适用于严格调用的方法中选择最具体的方法(15.12.2.5)。

在解析目标类型之前,隐式确定类型的表达式或不精确的方法引用表达式的含义是如此模糊,以致于包含这些表达式的参数不被认为与适用性有关;简单地忽略他们(除他们预期的参数数量之外),直到重载解析完成。

15.12.2.3. 阶段 2:识别适用于松散调用的匹配参数数量的方法

让 m 是可适用的方法,其具有 n 个参数和形式参数类型 F1, ..., Fn,让 e1, ..., en 是方法调用的实际参数表达式。则:

    * 如果 m 是泛型方法,方法调用不提供显式类型参数,则按 18.5.1 中所指定的来推断方法的适用性。

    * 如果 m 是泛型方法,方法调用提供显式类型参数,则让 R1 ... Rp(p ≥ 1)是 m 的类型参数,让 Bl 是 Rl( 1 ≤ l ≤ p)的声明边界,让 U1 ... Up 是在方法调用中给定的显式类型参数。则如果以下两种情况都为 true,则 m 适用于松散调用:

        对于 1 ≤ i ≤ n,如果 ei 与适用性(15.12.2.2)有关,则 ei 在松散调用上下文中与 Fi[R1:=U1, ..., Rp:=Up] 是兼容的。

        对于 1 ≤ l ≤ p,Ul <: Bl[R1:=U1, ..., Rp:=Up]。

    * 如果 m 不是泛型方法,则 m 适用于松散调用,如果对于 1 ≤ i ≤ n,ei 在松散调用上下文中与 Fi 是兼容的,或 ei 与适用性无关。

如果找不到适用于松散调用上下文的方法,则可适用方法的搜索继续到阶段 3(15.12.2.4)。

否则,在适用于松散调用的方法中选择一个最具体的方法(15.12.2.5)。

15.12.2.4. 阶段 3:识别适用于可变参数数量调用的方法

其中可变参数数量方法具有形式参数,让方法的第 i 个可变参数数量的参数类型定义如下:

    * 对于 i ≤ n-1,第 i 个可变参数数量的参数类型是 Fi。

    * 对于 i ≥ n,第 i 个可变参数数量的参数类型是 Fn。

让 m 是可适用的具有可变参数的方法(15.12.2.1),让 T1, ..., Tk 是 m 的前 k 个可变参数的参数类型,让 e1, ..., ek 是方法调用的实际参数表达式。则:

    * 如果 m 是泛型方法,方法调用不提供显式类型参数,则按 18.5.1 中所指定的推断方法的适用性。

    * 如果 m 是泛型方法,方法调用提供显式类型参数,则让 R1 ... Rp(p ≥ 1)是 m 的类型参数,让 Bl 是声明的 Rl( 1 ≤ l ≤ p)的边界,让 U1 ... Up 是在方法调用中给定的显式类型参数。则 m 适用于可变参数数量调用,如果:

        对于 1 ≤ i ≤ k,如果 ei 与适用性(15.12.2.2)有关,则 ei 在松散调用上下文中与 Ti[R1:=U1, ..., Rp:=Up] 是兼容的。

        对于 1 ≤ l ≤ p,Ul <: Bl[R1:=U1, ..., Rp:=Up]。

    * 如果 m 不是泛型方法,则 m 适用于可变参数数量调用,如果对于 1 ≤ i ≤ k,ei 在松散调用上下文中与是兼容的,或 ei 与适用性无关。

如果找不到适用于可变参数数量调用的方法,则发生编译时错误。

否则,从适用于可变参数数量调用的方法中选出最具体的方法(15.12.2.5)。

15.12.2.5. 选出最具体的方法

如果多个成员方法是可访问的且适用于方法调用的,则选出一个方法来提供用于运行时方法分派的描述符是必需的。Java 编程语言使用选出最具体的方法规则。

非正式的直觉是,如果第一个方法处理的任何调用都可以传递给另一个而不发生编译时错误,则一个方法比另一个更具体。在显式确定类型的 lambda 表达式参数(15.27.1)或可变参数数量调用(15.12.2.4)等情况下,某些灵活性允许使一个签名适应于另一个。

对于一个具有参数表达式 e1, ..., ek 的调用,一个可适用的方法 m1 比另一个可适用的方法 m2 更具体,如果以下任一情况为 true:

    * m2 是泛型,对于参数表达式 e1, ..., ek,通过 18.5.4 推断出 m1 比 m2 更具体。

    * m2 不是泛型,m1 和 m2 适用于严格或松散调用,其中 m1 具有形式参数类型 S1, ..., Sn,m2 具有形式参数类型 T1, ..., Tn,对于所有 i( 1 ≤ i ≤ n, n = k),参数 ei,类型 Si 比 Ti 更具体。

    * m2 不是泛型,m1 和 m2 适用于可变参数数量调用,其中 m1 的前 k 个可变参数的参数类型是 S1, ..., Sk,m2 的前 k 个可变参数的参数类型是 T1, ..., Tk,对于所有 i( 1 ≤ i ≤ k),参数 ei,类型 Si 比 Ti 更具体。此外,如果 m2 有 k+1 个参数,则 m1 的第 k+1 个可变参数参数类型是 m2 的第 k+1 个可变参数参数类型的子类型。

以上条件是仅有的情况,其中一个方法可以比另一个更具体。

如果 S <: T(4.10),则对于任何表达式,类型 S 比类型 T 更具体。

对于表达式 e,函数式接口类型 S 比函数式接口类型 T 更具体,如果 T 不是 S 的子类型,并且以下之一为 true(其中 U1 ... Uk 和 R1 是 S 的捕获的函数类型的参数类型和返回类型,V1 ... Vk 和 R2 是 T 的函数类型的参数类型和返回类型):

    * 如果 e 是显式确定类型的 lambda 表达式(15.27.1),则以下之一为 true:

        R2 是 void。

        R1 <: R2。

        R1 和 R2 是函数式接口类型,有至少一个结果表达式,对于 e 的每个结果表达式,R1 比 R2 更具体。

        (具有块 body 的 lambda 表达式的结果表达式在 15.27.2 中定义;具有表达式 body 的 lambda 表达式的结果表达式仅仅是 body 本身。)

        R1 是基元类型,R2 是引用类型,有至少一个结果表达式,e 的每个结果表达式都是基元类型的独立表达式(15.2)。

        R1 是引用类型,R2 是基元类型,有至少一个结果表达式,e 的每个结果表达式要么是引用类型的独立表达式,要么是聚合表达式。

    * 如果 e 是精确的方法引用表达式(15.13.1),则 i) 对于所有 i( 1 ≤ i ≤ k),Ui 与 Vi 相同,ii) 以下之一为 true:

        R2 是 void。

        R1 <: R2。

        R1 是基元类型,R2 是引用类型,方法引用的编译时声明具有其是基元类型的返回类型。

        R1 是引用类型,R2 是基元类型,方法引用的编译时声明具有其是引用类型的返回类型。

    * 如果 e 是括号化表达式,则将这些条件之一递归地应用到包含的表达式。

    * 如果 e 是条件表达式,则对于第二个和第三个操作数每个来说,递归地应用这些条件之一。

当且仅当 m1 比 m2 更具体,m2 比 m1 更不具体时,方法 m1 在严格意义上比另一个方法 m2 更具体。

如果方法是可访问和适用的,并且从严格意义上说,没有其他适用的、可访问的方法比它更具体,则这个方法对于方法调用来说是最具体的。

如果仅有一个最具体的方法,则该方法是事实上最具体的方法;它必须比任何其他可访问、适用的方法更具体。然后它受 15.12.3 中指定的一些进一步编译时检查的影响。

可以没有最具体的方法,因为有两个或更多个最具体的方法。在这种情况下:

    * 如果所有最具体的方法都具有重写等价的签名(8.4.2),则:

        如果这些最具体的方法中仅有一个是有形的(即,非 abstract 或 default),则它是最具体的方法。

        否则,如果所有最具体的方法都是 abstract 或 default,所有最具体的方法的签名都具有相同的擦除(4.6),则在具有最具体的返回类型的最具体方法子集中任意选择一个最具体的方法。

        这种情况下,最具体的方法被认为是 abstract。而且,该最具体的方法被认为抛出一个已检查异常,当且仅当该异常或其擦除被声明在每个最具体方法的 throws 子句中。

    * 否则,调用调用是模糊不清的,发生编译时错误。

15.12.2.6. 方法调用类型

最具体的可访问、适用的方法的调用类型是方法类型(8.2),其表示调用参数的目标类型、调用的结果(返回类型或 void)和调用的异常类型。按如下确定:

    * 如果选中的方法是泛型,方法调用不提供显式类型参数,则按 18.5.2 中所指定的那样推断调用类型。

    * 如果选中的方法是泛型,方法调用提供了显式类型参数,让 Pi 是方法的类型参数,让 Ti 是由方法调用提供的显式类型参数( 1 ≤ i ≤ p)。则:

        如果未检查的转换对于要适用的方法是必需的,则通过将替代 [P1:=T1, ..., Pp:=Tp] 应用到方法类型的参数类型来获取调用类型的参数类型,通过方法类型的返回类型和抛出类型的擦除来给定调用类型的返回类型和抛出类型。

        如果未检查转换对要适用的方法不是必需的,则通过将替代 [P1:=T1, ..., Pp:=Tp] 应用到方法类型来获取调用类型。

    * 如果选定的方法不是泛型,则:

        如果未检查的转换对于要适用的方法是必需的,则调用类型的参数类型是方法类型的参数类型,通过方法类型的返回类型和抛出类型的擦除来给定返回类型和抛出类型。

        否则,如果选定的方法是类 Object(4.3.2)的 getClass 方法,调用类型与方法类型相同,除返回类型是 Class<? extends |T|>,其中 T 是搜索的类型,按 15.12.1 确定,|T| 表示 T 的擦除(4.6)之外。

        否则,调用类型与方法类型相同。

15.12.3. 编译时步骤 3:选定的方法是否合适?

如果关于方法调用有一个最具体的方法声明,则它被称为方法调用的编译时声明。

如果方法调用的参数与其目标类型以及派生自编译时声明的调用类型不兼容,则这是一个编译时错误。

如果编译时声明适用于可变参数调用,则其中方法的调用类型的最后一个形式参数类型是 Fn[],如果类型,其是 Fn 的擦除,在调用点处是不可访问的,则这是一个编译时错误。

如果编译时声明是 void,则方法调用必须是顶层表达式(即,表达式语句或 for 语句的 ForInit 或 ForUpdate 部分中的 Expression),否则发生编译时错误。这种方法调用不产生任何值,因此仅用在不需要值的情况下。

此外,编译时声明是否合适依赖于左圆括号之前的方法调用表达式的形式,如下:

    * 如果形式是 MethodName - 即,仅是一个 Identifier - 编译时声明是实例方法,则:

        如果方法调用出现在 static 上下文(8.1.3)中,则这是一个编译时错误。

        否则,让 C 是编译时声明是其成员的直接封闭类。如果方法调用未直接被 C 或 C 的内部类封闭,则发生编译时错误。

    * 如果形式是 TypeName . [TypeArguments] Identifier,则编译时声明必须是 static,否则发生编译时错误。

    * 如果形式是 ExpressionName . [TypeArguments] Identifier 或 Primary . [TypeArguments] Identifier,则编译时声明不能是在接口中声明的 static 方法,否则发生编译时错误。

    * 如果形式是 super . [TypeArguments] Identifier,则:

        如果编译时声明是 abstract,则这是一个编译时错误。

        如果方法调用出现在 static 上下文中,则这是一个编译时错误。

    * 如果形式是 TypeName . super . [TypeArguments] Identifier,则:

        如果编译时声明是 abstract,则这是一个编译时错误。

        如果方法调用出现在 static 上下文中,则这是一个编译时错误。

        如果 TypeName 表示类 C,则如果方法调用未直接被 C 或 C 的内部类封闭,则发生编译时错误。

        如果 TypeName 表示接口,让 T 是直接封闭方法调用的类型声明。如果存在一个不同于编译时声明的方法,其重写(9.4.1)了来自 T 的直接父类或直接父接口的编译时声明,则发生编译时错误。

        在父接口重写祖父接口中声明的方法的情况下,此规则通过将祖父简单地添加到子接口的直接父接口列表中来防止子接口“跳过”重写。访问祖父功能的适当方式是通过直接父接口,并且只有当该接口选择暴露所需的行为时。(或者,开发者可以自由地定义自己额外的父接口,其暴露了所需的 super 方法调用行为。)

编译时参数类型和编译时结果按如下确定:

    * 如果方法调用的编译时声明不是签名多态方法,则编译时参数类型是编译时声明的形式参数的类型,编译时结果是为编译时声明选择的结果(15.12.2.6)。

    * 如果方法调用的编译时声明是签名多态方法,则:

        编译时参数类型是实际参数表达式的静态类型。参数表达式,其为 null 字面量 null(3.10.7),被视为具有静态类型 Void。

        编译时结果按如下确定:

            如果方法调用表达式是表达式语句,则编译时结果是 void。

            否则,如果方法调用表达式是强制转换表达式(15.16)的操作数,则编译时结果是强制转换表达式(4.6)的类型的擦除。

            否则,编译时结果是签名多态方法声明的返回类型,Object。

方法是签名多态的,如果以下所有都为 true:

    * 它声明在 java.lang.invoke.MethodHandle 类中。

    * 它采用单个声明类型为 Object[] 的可变参数(8.4.1)。

    * 它具有返回类型 Object。

    * 它是 native 的。

在 Java SE 8 中,唯一的签名多态方法是类 java.lang.invoke.MethodHandle 的 invoke 和 invokeExact 方法。

然后,以下编译时信息与方法调用的运行时使用相关联:

    * 方法名称。

    * 方法调用(13.1)的限定类型。

    * 参数的数量和编译时参数类型,按顺序。

    * 编译时结果,或 void。

    * 调用模式,计算如下:

        如果方法声明的限定类型是类,则:

            如果编译时声明具有 static 修饰符,则调用模式是 static。

            否则,如果编译时声明具有 private 修饰符,则调用模式是 nonvirtual。

            否则,如果方法调用左圆括号之前的部分是 super . Identifier 或 TypeName . super . Identifier 形式,则调用模式是 super。

            否则,调用模式是 virtual。

        如果方法调用的限定类型是接口,则调用模式是 interface。

如果编译时声明的调用类型的结果是 void,则通过将捕获转换(5.1.10)应用到编译时声明的调用类型的返回类型来获取方法调用表达式的类型。

15.12.4. 方法调用的运行时计算

运行时,方法调用需要五步。首先,计算目标引用。第二,计算参数表达式。第三,检查要调用的方法的可访问性。第四,定位要执行的方法的实际代码。第五,创建新的活动帧,如有必要执行同步,将控制转移到方法代码。

15.12.4.1. 计算目标引用(如有必要)

有六种情况需要考虑,这依赖于方法调用的形式:

    * 如果形式是 MethodName - 即,仅是一个 Identifier - 则:

        如果调用模式是 static,则没有目标引用。

        否则,让 T 是方法是其成员的封闭类型声明,让 n 是一个整数,这样 T 是其声明直接包含方法调用的类的第 n 个词法封闭类型声明。目标引用是 this 的第 n 个词法封闭实例。

        如果 this 的第 n 个词法封闭实例不存在,则这是一个编译时错误。

    * 如果形式是 TypeName . [TypeArguments] Identifier,则没有目标引用。

    * 如果形式是 ExpressionName . [TypeArguments] Identifier,则:

        如果调用模式是 static,则没有目标引用。计算 ExpressionName,然后丢弃结果。

        否则,目标引用是由 ExpressionName 表示的值。

    * 如果形式是 Primary . [TypeArguments] Identifier,则:

        如果调用模式是 static,则没有目标引用。计算 Primary 表达式,然后丢弃结果。

        否则,计算 Primary 表达式,结果用作目标引用。

      在这两种情况下,如果 Primary 表达式的计算突然完成,则看起来就像没有计算任何参数表达式一样,方法调用因相同的理由而突然完成。

    * 如果形式是 super . [TypeArguments] Identifier,则目标引用是 this 的值。

    * 如果形式是 TypeName . super . [TypeArguments] Identifier,则如果 TypeName 表示类,则目标引用是 TypeName.this 的值;否则,目标引用是 this 的值。

avatar

avatar

15.12.4.2. 计算参数

计算参数列表的过程略有不同,这取决于被调用的方法是固定参数方法还是可变参数方法(8.4.1)。

如果正被调用的方法是可变参数方法 m,则它必须具有 n>0 个形式参数。m 的最后一个形式参数必须具有有关某个 T 的类型 T[],m 必须是正在被调用的具有 k ≥ 0 个实际参数表达式。

如果 m 是正在被调用的具有 k ≠ n 个实际参数表达式,或,如果 m 是正在被调用的具有 k = n 个实际参数表达式,第 k 个参数表达式的类型是与 T[] 是不兼容的,则计算参数列表(e1, ..., en-1, en, ..., ek)就如他们被写作(e1, ..., en-1, new |T[]| { en, ..., ek }),其中 |T[]| 表示 T[] 的擦除(4.6)。

前面的段落是用来用擦除泛型来处理在 Java 虚拟机中出现的参数化类型和数组类型。也就是说,如果可变数组参数的元素类型是非具体化的,例如 List,则必须特别注意数组创建表达式(15.10),因为创建的数组的元素类型必须是具体化的。通过擦除参数列表中最后一个表达式的数组类型,可以确保我们获取具体化的元素类型。然后,由于数组创建表达式出现在调用上下文(5.3)中,因此可能发生从具有具体化元素类型的数组类型到具有非具体化元素类型的数组类型的未检查转换,特别是可变参数。Java 编译器需要在此转换上给出一个编译时未检查警告。Oracle 的 Java 编译器参考实现将此处的未检查警告识别为信息更丰富的泛型数组创建。

现在计算参数表达式(可能重写为上面描述的),以产生参数值。每个参数值都准确对应该方法的 n 个形式参数之一。

如果有,按从左到右的顺序计算参数表达式。如果任何参数表达式的计算突然完成,则其右侧的任何参数表达式都不会被计算,方法调用因相同的理由而突然完成。对于 1 ≤ j ≤ n,计算第 j 个参数表达式的结果是第 j 个参数值。然后按上面所述,计算使用参数值继续。

15.12.4.3. 检查类型和方法的可访问性

让 C 是包含方法调用的类,让 T 是是方法调用(13.1)的限定类型,让 m 是编译时确定的方法的名称(15.12.3)。

Java 编程语言的实现必须确保,作为链接的一部分,方法 m 仍然存在于类型 T 中。如果这不为 true,则发生 NoSuchMethodError(其是 IncompatibleClassChangeError)。

如果调用模式是 interface,则实现还必须检查目标引用类型仍然实现指定的接口。如果目标引用类型未实现该接口,则发生 IncompatibleClassChangeError。

实现还必须确保,在链接期间,类型 T 和方法 m 是可访问的:

    * 对于类型 T:

        如果 T 与 C 在同一包下,则 T 是可访问的。

        如果 T 与 C 在不同包下,T 是 public,则 T 是可访问的。

        如果 T 与 C 在不同包下,T 是 protected,则当且仅当 C 是 T 的子类时,T 是可访问的。

    * 对于方法 m:

        如果 m 是 public,则 m 是可访问的。(接口的所有成员都是 public(9.2)。)

        如果 m 是 protected,则当且仅当 T 与 C 在同一包下或 C 是 T 的子类时,m 是可访问的。

        如果 m 具有包访问,则当且仅当 T 与 C 在同一包下时,m 是可访问的。

        如果 m 是 private,则当且仅当 C 是 T 或 C 封闭 T 或 T 封闭 C,或 T 和 C 都被第三个类封闭时,m 是可访问的。

如果 T 或 m 不是可访问的,则发生 IllegalAccessError(12.3)。

15.12.4.4. 定位要调用的方法

方法查找的策略依赖于调用模式。

如果调用模式是 static,则不需要目标引用,不允许重写。类 T 的方法 m 是要被调用的那个。

否则,实例方法是要调用的,并且有一个目标引用。如果目标引用是 null,则在这个点抛出 NullPointerException。否则,目标引用被称为引用目标对象,将被用作被调用的方法中的关键字 this 的值。然后考虑其他四种可能的调用模式。

如果调用模式是 nonvirtual,则不允许重写。类 T 的方法 m 是要被调用的那个。

否则,如果调用模式是 virtual,T 和 m 共同地表示签名多态方法(15.12.3),则目标对象是 java.lang.invoke.MethodHandle 的实例。方法句柄封装了类型,其是与编译时(15.12.3)与方法调用相关联的信息相匹配的。此匹配的详情在 The Java Virtual Machine Specification, Java SE 8 Edition 和 Java SE 平台 API 中给定。如果匹配成功,则直接或间接地调用由方法句柄封装的目标方法,同时不执行 15.12.4.5 中的过程。

否则,调用模式是 interface、virtual 或 super,可能发生重写。使用动态方法查找。动态查找从类 S 开始,确定如下:

    * 如果调用模式是 interface 或 virtual,则 S 最初是目标对象的实际运行时类 R。

      即使目标对象是数组实例,也是如此。(请注意,对于调用模式 interface,R 必须实现 T;对于调用模式 virtual,R 必须是 T 或 T 的子类。)

    * 如果调用模式是 super,则 S 最初是方法调用的限定类型(13.1)。

根据需要,动态方法查找使用以下过程来为方法 m 搜索类 S,然后类 S 的父类和父接口。

让 X 是方法调用的目标类型的编译时类型。则:

  1. 如果类 S 包含名为 m 具有方法调用所需的与编译时确定的相同的描述符(相同数量的参数、相同的参数类型和相同的返回类型),则:

      如果调用模式是 super 或 interface,则这是要调用的方法,过程终止。

      如果调用模式是 virtual,S 中的声明重写了 X.m(8.4.8.1),则 S 中声明的方法是要调用的方法,过程终止。

  1. 否则,如果 S 具有子类,使用 S 的直接父类代替 S 递归地执行步骤 1 和 2 的查找过程;如果有,要被调用的方法就是此查找过程递归调用的结果。

  2. 如果通过前面两步找不到方法,则搜索 S 的父接口以寻找合适的方法。

      一组候选者方法被认为具有以下属性:i) 每个方法声明在(直接或间接)S 的父接口中;ii) 每个方法具有方法调用所需的名称和描述符;iii) 每个方法是非 static;iv) 对于每个方法,其中方法的声明接口是 I,没有其他方法通过声明在 I 的子接口中的 (iii) 来满足 (i)。

      如果这组包含 default 方法,则这种方法是要调用的方法。否则,将选择集合中的 abstract 方法作为要调用的方法。

动态方法查找可能导致以下错误发生:

    * 如果要调用的方法是 abstract,则抛出 AbstractMethodError。

    * 如果要调用的方法是 default,在以上步骤 3 的候选者集合中出现了多个 default 方法,则抛出 IncompatibleClassChangeError。

    * 如果调用模式是 interface,选择的方法不是 public,则抛出 IllegalAccessError。

以上过程(如果毫无错误地终止)将找到一个非 abstract、可访问的方法去调用,假设程序中的所有类和接口都被一致地编译。但是,如果不是这种情况,则可能发生如上所述的各种错误;这些情况下额外的有关 Java 虚拟机的行为的详情通过 The Java Virtual Machine Specification, Java SE 8 Edition 给定。

动态查找过程,虽然在这里进行了显式描述,通常会隐式地实现,例如,作为每类方法分派表的构造和使用的副作用,或用于高效分派的其他每类结构的构造。

avatar avatar

avatar avatar

15.12.4.5. 创建帧,同步,转移控制

某个类 S 中的方法 m 已被是被为要被调用的方法。

现在创建一个新的活动帧,其中包含目标引用(如果有)和参数值(如果有),以及用于要调用的方法的局部变量和堆栈和实现所需要的任何其他簿记信息(栈指针、程序计数器、前一个活动帧的引用诸如此类等等)。如果没有足够的内存来创建这样的活动帧,则抛出 StackOverflowError。

新创建的活动帧称为当前活动帧。这样做的效果是,将参数值赋给相应新创建的方法参数变量,如果有目标引用,则使目标引用作为 this 可用。在将每个参数值赋给相应的参数变量之前,它受调用转换(5.3)的影响,其包括任何所需的值集转换(5.1.13)。

如果正在被调用的方法的类型的擦除(4.6)在签名上与方法调用(15.12.3)的编译时声明的类型的擦除有所不同,则如果任何一个参数值是对象,其不是方法调用的编译时声明中的相应形式参数类型的擦除的子类或子接口的实例,则抛出 ClassCastExcpetion。

如果方法 m 是 native 方法,但必须的本地的、实现依赖的二进制代码未加载或无法以其他方式动态链接,则抛出 UnsatisfiedLinkError。

如果方法 m 不是 synchronized,则将控制转移到要被调用的方法 m 的 body。

如果方法 m 是 synchronized,则在转移控制之前必须锁定一个对象。在当前线程可以获取一个锁之前,不能采取进一步操作。如果有一个目标引用,则必须锁定目标对象;否则类方法 m 必须锁定类 S 的 Class 对象。然后将控制转移到要被调用的方法 m 的 body 中。当方法的 body 的执行完成时,自动解锁对象,无论正常还是突然完成。锁定和解锁行为就好像方法的 body 被嵌入到一个 synchronized 语句中(14.19)一样。

avatar

15.13. 方法引用表达式

方法引用表达式用于引用引用方法调用,而实际上不执行该调用。某些形式的方法引用表达式也允许类实例创建(15.9)或数组创建(15.10)被视作就像它是一个方法调用一样。

MethodReference:
    ExpressionName :: [TypeArguments] Identifier
    ReferenceType :: [TypeArguments] Identifier
    Primary :: [TypeArguments] Identifier
    super :: [TypeArguments] Identifier
    TypeName . super :: [TypeArguments] Identifier
    ClassType :: [TypeArguments] new
    ArrayType :: new

如果 TypeArguments 存在于 :: 的右侧,则如果任何一个类型参数是通配符(4.5.1),则发生编译时错误。

如果方法引用表达式具有形式 ExpressionName :: [TypeArguments] Identifier 或 Primary :: [TypeArguments] Identifier,则如果 ExpressionName 或 Primary 的类型不是引用类型,则这是一个编译时错误。

如果方法引用表达式具有形式 super :: [TypeArguments] Identifier,让 T 是直接封闭方法引用表达式的类型声明。如果 T 是类 Object 或 T 是接口,则这是一个编译时错误。

如果方法引用表达式具有形式 TypeName . super :: [TypeArguments] Identifier,则:

    * 如果 TypeName 表示类,C,则如果 C 不是当前类的词法封闭类,或如果 C 是类 Object,则这是一个编译时错误。

    * 如果 TypeName 表示接口,I,则让 T 是直接封闭方法引用表达式的类型声明。如果 I 不是 T 的直接父接口,或如果存在 T 的某个其他直接父类或父接口,以使 J 是 I 的子类型,则这是一个编译时错误。

    * 如果 TypeName 表示类型变量,则发生编译时错误。

如果方法引用表达式具有形式 super :: [TypeArguments] Identifier 或 TypeName . super :: [TypeArguments] Identifier,则如果表达式出现在 static 上下文中,则这是一个编译时错误。

如果方法引用表达式具有形式 ClassName :: [TypeArguments] Identifier,则:

    * ClassType 必须表示可访问的、非 abstract 的、非枚举类型的类,否则发生编译时错误。

    * 如果 ClassType 表示参数化类型(4.5),则如果其类型参数之一为通配符,则这是一个编译时错误。

    * 如果 ClassType 表示原始类型(4.8),则如果在 :: 后面存在 TypeArguments,则这是一个编译时错误。

如果方法引用表达式具有形式 ArrayType :: new,则 ArrayType 必须表示具体化的类型,否则发生编译时错误。

实例方法(15.12.4.1)的目标引用可以由方法引用表达式使用 ExpressionName、Primary 或 super 提供,或者随后当调用方法时提供。新内部类实例(15.9.2)的直接封闭实例由 this(8.1.3)的词法封闭实例提供。

当类型的多个成员方法具有相同的名称时,或当类具有多个构造器时,根据函数式接口类型,表达式将其作为目标,选择适当的方法或构造器,如 15.13.1 中所述。

如果方法或构造器是泛型,则可以推断或显式提供适当的类型参数。类似地,也可以显式提供或推断方法引用表达式提及的泛型类型的类型参数。

方法引用表达式总是聚合表达式(15.2)。

如果程序中的方法引用表达式出现在了除赋值上下文(5.2)、调用上下文(5.3)或强制转换上下文(5.5)以外的位置,则这是一个编译时错误。

方法引用表达式的计算产生函数式接口类型(9.8)的实例。方法引用计算不会导致相应方法的执行;相反,这可能随后发生,当调用适当的函数式接口的方法时。

avatar

不可能指定要匹配的特定签名,例如,Arrays::sort(int[])。相反,函数式接口提供用作重载解析算法(15.12.2)的输入的参数类型。这应能满足绝大多数的使用情况;当因更精确的控制而产生罕见的需求时,可以使用 lambda 表达式。

在分隔符(List::size)前面的类名称中使用产生了作为类型参数括号的 < 和作为小于操作符的 < 之间无法区分的解析问题。理论上,这并不比允许强制转换表达式中的类型参数更糟糕;但是,不同之处在于,只有当遇到一个 ( 记号时,才会出现强制转换的情况;随着方法引用表达式的添加,则每个表达式的开头都可能是参数化类型。

15.13.1. 方法引用的编译时声明

方法引用的编译时声明是表达式引用的那个方法。在特殊情况下,编译时声明实际上不存在,而是一个表示类实例创建或数组创建的概念方法。编译时声明的选择取决于表达式所针对的函数类型,就像方法调用的编译时声明取决于调用的参数(15.12)。

编译时声明的搜索反映了 15.12.1 和 15.12.2 中的方法调用的处理过程,如下所示:

    * 首先,确定要搜索的类型:

        如果方法引用表达式具有形式 Expression :: [TypeArguments] Identifier 或 Primary :: [TypeArguments] Identifier,则要搜索的类型是 :: 记号前面的表达式的类型。

        如果方法引用表达式具有形式 ReferenceType :: [TypeArguments] Identifier,则要搜索的类型是应用于 ReferenceType 的捕获转换(5.1.10)的结果。

        如果方法引用表达式具有形式 super :: [TypeArguments] Identifier,则要搜索的类型是其声明包含该方法引用的类的父类类型。

        如果方法引用表达式具有形式 TypeName . super :: [TypeArguments] Identifier,则如果 TypeName 表示类,则要搜索的类型是命名类的父类类型;否则,TypeName 表示接口,相应的其声明包含该方法引用的类或接口的父接口类型是要搜索的类型。

        对于其他两种形式(涉及 :: new),引用的方法是概念性的,没有要搜索的类型。

    * 第二,给定一个定向的具有 n 个参数的函数类型,定义了一组可适用的方法。

        如果方法引用表达式具有形式 ReferenceType :: [TypeArguments] Identifier,则可适用的方法是要搜索的类型的具有适当名称、可访问的、数量(n 或 n-1)和类型参数数量(派生自 [TypeArguments])的成员方法,如 15.12.2.1 中所述。

        考虑了两种不同的数量,n 和 n-1,以说明此方法引用 static 方法或实例方法的可能性。

        如果,方法引用表达式具有形式 ClassType :: [TypeArguments] new,则可适用的方法是一组对应 ClassType 的构造器的概念方法。

        如果 ClassType 是原始类型,但不是该原始类型的非 static 成员,则候选的概念成员方法是在 15.9.3 中为使用 <> 来忽略类的类型参数的类实例创建表达式所指定的那些。

        否则,候选的概念成员方法是 ClassType 的构造器,看起来就好像他们是具有返回类型 ClassType 的方法一样。在这些候选者中,选择具有适当访问性、数量(n)和类型参数数量(派生自 [TypeArguments])的方法,如 15.12.2.1 中所述。

        如果方法引用表达式具有形式 ArrayType :: new,则考虑单个概念方法。该方法具有单个类型 int 的参数,返回 ArrayType,且没有 throws 子句。如果 n = 1,则这是唯一可适用的方法;否则,没有可适用的方法。

        对于其他所有形式,可适用的方法是要搜索的类型的具有适当名称(由 Identifier 给定)、可访问性、数量(n)和类型参数数量(派生自 [TypeArguments])的成员方法,如 15.12.2.1 中所述。

    * 最后,如果没有可适用的方法,则没有编译时声明。

      否则,给定一个定向的具有参数类型 P1, ..., Pn 的函数类型和一组可适用的方法,按如下选择编译时声明:

        如果方法引用表达式具有形式 ReferenceType :: [TypeArguments] Identifier,则执行两个最具体适用方法的搜索。每个搜索在 15.12.2.2 中通过 15.12.2.5 指定,具有以下分类。每个搜索可能产生一个方法或,在 15.12.2.2 中通过 15.12.2.5 指定的错误的情况下,没有结果。

        在第一个搜索中,方法引用看起来就好像,它是一个具有 P1, ..., Pn 类型的参数表达式的调用;类型参数,如果有,通过方法引用表达式给定。

        在第二个搜索中,如果 P1, ..., Pn 不是空的,P1 是 ReferenceType 的子类型,则方法引用表达式看起来就好像,它是一个具有 P2, ..., Pn 类型的参数表达式的方法调用表达式。如果 ReferenceType 是原始类型,并且存在此类型的参数化,G<...>,其是 P1 的子类型,则要搜索的类型是应用到 G<...> 的捕获转换(5.1.10)的结果;否则,要搜索的类型与第一个搜索的类型相同。接着,类型参数,如果有,通过方法引用表达式给定。

        如果第一个搜索产生一个 static 方法,而在第二个搜索期间,没有适用于 15.12.2.2、15.12.2.3 或 15.12.2.4 的非 static 方法,则编译时声明是第一个搜索的结果。

        否则,如果在第一个搜索期间没有适用于 15.12.2.2、15.12.2.3 或 15.12.2.4 的 static 方法,而第二个搜索产生了一个非 static 方法,则编译时声明是第二个搜索的结果。

        否则,没有编译时声明。

        对于其他所有形式的方法引用表达式,执行一个最具体适用方法的搜索。此搜索在 15.12.2.2 中通过 15.12.2.5 指定,具有以下分类。

        方法引用看起来就好像,它是一个具有 P1, ..., Pn 类型的参数表达式的调用;类型参数,如果有,通过方法引用表达式给定。

        如果搜索结果是 15.12.2.2 中通过 15.12.2.5 所述的错误,或如果最具体适用的方法是 static 的,则没有编译时声明。

        否则,编译时声明是最具体适用的方法。

如果方法引用表达式具有形式 ReferenceType :: [TypeArguments] Identifier,编译时声明是 static,ReferenceType 不是简单的或限定的名称(6.2),则这是一个编译时错误。

如果方法引用表达式具有形式 super :: [TypeArguments] Identifier 或 TypeName . super :: [TypeArguments] Identifier,编译时声明是 abstract 的,则这是一个编译时声明。

如果方法引用表达式具有形式 TypeName . super :: [TypeArguments] Identifier,TypeName 表示接口,存在一个不同于编译时声明的方法,其重写(8.4.8,9.4.1)了来自其声明直接封闭方法引用表达式的类型的直接父类或直接父接口的编译时声明,则这是一个编译时声明。

如果方法引用表达式是形式 ClassType :: [TypeArguments] new,在确定如 15.9.2 中所述的 ClassType 的封闭实例时,将发生编译时错误,则这是一个编译时错误(将方法引用表达式视为,它是一个非限定的类实例创建表达式)。

可以以不同的方式解释 ReferenceType :: [TypeArguments] Identifier 形式的方法引用表达式。如果 Identifier 引用实例方法,则隐式的 lambda 表达式相比于引用 static 方法的 Identifier,有一个额外的参数。ReferenceType 可能同时有两种适用的方法,因此上面描述的搜索算法分别识别他们,因为每种情况下都有不同的参数类型。

一个有歧义的示例:

avatar

无法通过提供一个比适用的 static 方法更具体的实例方法来解决此歧义:

avatar

搜索足够聪明,可以忽略所有适用方法(从两种搜索)都是实例方法的多义性:

avatar

为了方便,当泛型类型的名称用于引用实例方法(其中接收者称为第一个参数)时,目标类型用于确定类型参数。这促进像 Pair::first 一样的用法代替 Pair<String,Integer>::first。类似地,像 Pair::new 一样的方法引用被视为“菱形”实例创建(new Pair<>())。因为“菱形”是隐式的,所以这种形式不实例化原始类型;实际上,无法表达原始类型构造器的引用。

对于某些方法引用表达式,仅有一个可能的具有唯一一个可能的调用类型(15.12.2.6)的编译时声明,而不用管定向的函数类型。这种方法引用表达式被称为是精确的。不精确的方法引用表达式被称为不精确的。

以 Identifier 结束的方法引用表达式是精确的,如果它满足以下所有:

    * 如果方法引用表达式具有形式 ReferenceType :: [TypeArguments] Identifier,则 ReferenceType 不表示原始类型。

    * 要搜索的类型仅具有一个具有名称 Identifier 的成员方法,其对于方法引用表达式所出现在的类或接口是可访问的。

    * 此方法不是可变参数的(8.4.1)。

    * 如果此方法是泛型(8.4.4),则方法引用表达式提供 TypeArguments。

形式 ClassType :: [TypeArguments] new 的方法引用表达式是精确的,如果它满足以下所有:

    * 由 ClassType 表示的类型不是原始的,或不是原始类型的非 static 成员类型。

    * 由 ClassType 表示的类型仅具有一个构造器,其对于方法引用表达式所出现在的类或接口是可访问的。

    * 此构造器不是可变参数的。

    * 如果此构造器是泛型,则方法引用表达式提供 TypeArguments。

形式 ArrayType :: new 的方法引用表达式总是精确的。

15.13.2. 方法引用的类型

方法引用表达式兼容具有目标类型 T 的赋值上下文、调用上下文或强制转换上下文,如果 T 是函数式接口类型(9.8),表达式与从 T 派生的基础目标类型的函数类型是一致的。

基础目标类型按如下派生自 T:

    * 如果 T 是通配符参数化的函数式接口类型,则基础目标类型是 T 的非通配符参数化(9.9)。

    * 否则,基础目标类型是 T。

方法引用表达式与函数类型是一致的,如果以下两者都为 true:

    * 函数类型识别对应于引用的单个编译时声明。

    * 以下之一为 true:

        函数类型的结果是 void。

        函数类型的结果是 R,应用到选定的编译时声明的调用类型(15.12.2.6)的返回类型的捕获转换(5.1.10)的结果是 R'(其中 R 是可以用于推断 R' 的目标类型),R 和 R' 都不是 void,R' 与 R 在赋值上下文中是兼容的。

如果未检查转换对于要适用的编译时声明是必需的,且此转换将在调用上下文中导致未检查警告,则发生编译时未检查警告。

如果未检查转换对于兼容函数类型的返回类型 R 的返回类型 R' 是必需的,如上所述,且此转换将导致赋值上下文中的未检查警告,则发生编译时未检查警告。

如果方法引用表达式与目标类型 T 兼容,则表达式的类型,U,是派生自 T 的基础目标类型。

如果由 U 或 U 的函数类型提及的任何类或接口从方法引用表达式所出现在的类或接口中是不可访问的,则这是一个编译时错误。

对于 U 的每个非 static 成员方法 m,如果 U 的函数类型具有 m 签名的子签名,则其方法类型是 U 的函数类型的概念方法被称为重写 m,可能出现 8.4.8.3 中指定的任何编译时错误和未检查警告。

对于在编译时声明的调用类型的 throws 子句中列举的每个已检查异常类型 X,必须在 U 的函数类型的 throws 子句中提及 X 或 X 的父类,否则发生编译时错误。

驱动兼容性定义的关键思想是,当且仅当等价的 (x,y,z) -> exp.<T1,T2>method(x,y,z) 是兼容的时,方法引用也是兼容的。(这是非正式的,有些问题使我们很难或不可能根据重写来正式定义语义。)

这些兼容性定义提供了方便的功能,将一个函数式接口转换为另一个:

avatar

可以优化实现,以便当将 lambda 派生的对象传递并转换为各种类型时,这不会导致在核心的 lambda body 周围有许多曾适应逻辑。

与 lambda 表达式不同,方法引用可以与泛型函数类型一致(即,具有类型参数的函数类型)。这是因为 lambda 需要能够声明类型参数,但没有语法支持;而对于方法引用,不需要这样的声明。例如,以下程序是合法的:

avatar

15.13.3. 方法引用的运行时计算

运行时,方法引用表达式的计算类似于类实例创建表达式的计算,就如正常完成产生一个对对象的引用。方法引用表达式的计算不同于方法本身的调用:

首先,如果方法引用表达式以 ExpressionName 或 Primary 开头,则计算此子表达式。如果子表达式的计算为 null,则引发 NullPointerException,方法引用表达式突然完成。如果子表达式突然完成,则方法引用表达式因相同的理由而突然完成。

接下来,分配并初始化一个具有以下属性的类的新实例,或引用一个具有以下属性的类的现存的实例。如果创建新实例,但没有足够的空间来分配对象,则方法引用表达式的计算通过抛出 OutOfMemory 来突然完成。

方法引用表达式的值是对具有以下属性的类的实例的引用:

    * 类实现定向的函数式接口类型,并且如果目标类型是交集类型,则在交集中提及每个其他接口类型。

    * 其中方法引用表达式具有类型 U,对于 U 的每个非 static 成员方法 m:

      如果 U 的函数类型具有 m 签名的子签名,则类声明一个重写 m 的调用方法。此调用方法的 body 调用引用的方法,创建一个类实例,或创建一个数组,如下所述。如果调用方法的结果不是 void,则 body 返回方法调用或对象创建的结果,在任何必需的赋值转换(5.2)之后。

      如果被重写的方法的类型的擦除在其签名上不同于 U 的函数类型的擦除,则在方法调用或对象创建之前,调用方法的 body 检查,每个参数值是 U 的函数类型中相应的参数类型的擦除的子类或子接口的实例;如果不是,则抛出 ClassCastException。

    * 类不会重写函数式接口类型或以上提及的其他接口类型的其他方法,尽管它可能重写 Object 类的方法。

调用方法的 body 依赖于方法引用表达式的形式,如下:

    * 如果形式是 ExpressionName :: [TypeArguments] Identifier 或 Primary :: [TypeArguments] Identifier,则调用方法的 body 具有其是方法引用表达式的编译时声明的编译时声明的方法调用表达式的效果。

        调用模式是派生自 15.12.3 中所述的编译时声明。

        目标引用是 ExpressionName 或 Primary 的值,在计算方法引用表达式时确定。

        方法调用表达式的参数是调用方法的形式参数。

    * 如果形式是 ReferenceType :: [TypeArguments] Identifier,则调用方法的 body 同样地有其是方法引用表达式的编译时声明的编译时声明的方法调用表达式的效果。方法调用表达式的运行时计算如 15.12.4.3、15.12.4.4 和 15.12.4.5 中所述,其中:

        调用模式派生自 15.12.3 中所述的编译时声明。

        如果编译时声明是实例方法,则目标引用是调用方法的第一个形式参数。否则,没有目标引用。

        如果编译时声明是实例方法,则方法调用表达式的参数(如果有)是调用方法的第二个和随后的形式参数。否则,方法调用表达式的参数是调用方法的形式参数。

    * 如果形式是 super :: [TypeArguments] Identifier 或 TypeName . super :: [TypeArguments] Identifier,则调用方法的 body 具有其是方法引用表达式的编译时声明的编译时声明的方法调用表达式的效果。方法调用表达式的运行时计算如 15.12.4.3、15.12.4.4 和 15.12.4.5 所述,其中:

        调用模式是 super。

        如果方法引用表达式以命名类的 TypeName 开头,则目标引用是在计算方法引用的点处的 TypeName . this 的值。否则,目标引用是在计算方法引用的点处的 this 的值。

        方法调用表达式的参数是调用方法的形式参数。

    * 如果形式 ClassType :: [TypeArguments] new,则调用方法的 body 具有 new [TypeArguments] ClassType(A1, ..., An) 形式的类实例创建表达式的效果,其中 A1, ..., An 是调用方法的形式参数,其中:

        新对象的封闭实例,如果有,是派生自方法引用表达式的位置,如 15.9.2 中所述。

        要调用的构造器是对应于方法引用(15.13.1)的编译时声明的构造器。

    * 如果形式是 Type[]k :: new(k ≥ 1),则调用方法的 body 具有与 new Type [ size ] []k-1 形式的数组创建表达式相同的效果,其中 size 是调用方法的单个参数。(符号 []k 表示 k 个括号对序列。)

如果调用方法的 body 具有方法调用表达式的效果,则编译时参数类型和方法调用的编译时结果按 15.12.3 中所述确定。为了确定编译时结果这一目的,方法调用表达式是表达式语句,如果调用方法的结果是 void,和 return 语句的 Expression,如果调用方法的结果是非 void。

当方法引用的编译时声明是签名多态时,此确定的效果是:

    * 方法调用的参数类型是相应参数的类型。

    * 方法调用要么是 void,要么具有 Object 返回类型,这取决于封闭方法调用的的调用方法是 void 还是具有返回类型。

方法引用表达式计算的时序比 lambda 表达式(15.27.4)的更复杂。当方法引用表达式具有一个在 :: 分隔符前面的表达式(而不是类型)时,将直接计算该子表达式。存储计算的结果,直到调用对应的函数式接口类型的方法;这时,结果用作调用的目标引用。这意味着,仅当程序遇到方法引用表达式时才计算 :: 分隔符前面的表达式,并且不会在随后的函数式接口类型的调用中对其进行重新计算。

将在这进行的 null 处理和方法调用期间它的处理作比较,是很有趣的。当计算方法调用表达式时,可能将限定调用的 Primary 计算为 null,而不会引发 NullPointerExcpetion。当调用的方法是 static 时,会发生这种情况(尽管调用的语法推荐实例方法)。由于禁止由 Primary 限定的方法引用表达式的可适用方法是 static 的(15.13.1),所以方法引用表达式的计算更简单 - null Primary 总是引发 NullPointerException。

15.14. 后缀表达式

后缀表达式包括后缀 ++ 和 -- 操作符的使用。名称不被认为是主表达式(15.8),但在语法中单独处理以避免某些歧义。他们只有在这是可以互换的,在后缀表达式的优先级的层级上。

PostfixExpression:
    Primary
    ExpressionName
    PostIncrementExpression
    PostDecrementExpression

15.14.1. 表达式名称

在 6.5.6 中给定计算表达式名称的规则。

15.14.2. 后缀自增操作符 ++

后跟 ++ 操作符的后缀表达式是后缀自增表达式。

PostIncrementExpression:
    PostfixExpression ++

后缀表达式的结果必须是其类型为可转换(5.1.8)为数字类型的变量,否则发生编译时错误。

后缀自增表达式的类型是变量的类型。后缀自增表达式的结果不是变量,而是值。

运行时,如果操作数表达式的计算突然完成,则后缀自增表达式因相同的理由而突然完成,并且不会发生自增。否则,将值 1 加到变量的值上,和存储回变量上。在增加之前,先在值 1 和变量的值上执行二进制数字提升(5.6.2)。如有必要,通过缩小基本转换(5.1.3)窄化总和,和/或在存储之前总和受变量类型的装箱转换(5.1.7)的影响。后缀自增表达式的值是存储新值之前变量的值。

请注意,以上提及的二进制数字提升可能包括拆箱转换(5.1.8)和值集转换(5.1.13)。如有必要,在存储到变量之前先对总和应用值集转换。

声明为 final 的变量无法增加,因为当这种 final 变量的访问用作表达式时,结果是一个值,不是一个变量。因此,它无法被用作后缀自增表达式的操作数。

15.14.3. 后缀自减操作符 --

后跟 -- 操作符的后缀表达式是后缀自减表达式。

PostDecrementExpression:
    PostfixExpression --

后缀表达式的结果必须是其类型可转换为数字类型的变量,否则发生编译时错误。

后缀自减表达式的类型是变量的类型。后缀自减表达式的结果不是变量,而是一个值。

运行时,如果操作数表达式的计算突然完成,则后缀自减表达式因相同的理由而突然完成,并且不会发生自减。否则,从变量的值减去值 1,差数存储回变量中。在减去之前,先在值 1 和变量的值上执行二进制数字提升(5.6.2)。如有必要,通过缩小基本转换(5.1.3)对差数进行窄化,和/或在存储之前差数受变量类型的装箱转换(5.1.7)的影响。后缀自减表达式的值是存储新值之前的变量的值。

请注意,上面提及的二进制数字提升可能包括拆箱转换(5.1.8)和值集转换(5.1.13)。如有必要,在存储到变量之前对差数应用值集转换。

声明为 final 的变量无法自减,因为当将这种 final 变量的访问用作表达式时,结果是一个值,不是变量。因此,它无法用作后缀自减操作符的操作数。

15.15. 一元操作符

操作符 +、-、++、--、~、! 和强制转换操作符(15.16)被称为一元操作符。

UnaryExpression:
    PreIncrementExpression
    PreDecrementExpression
    + UnaryExpression
    - UnaryExpression
    UnaryExpressionNotPlusMinus

PreIncrementExpression:
    ++ UnaryExpression

PreDecrementExpression:
    -- UnaryExpression

UnaryExpressionNotPlusMinus:
    PostfixExpression
    ~ UnaryExpression
    ! UnaryExpression
    CastExpression

为了方便,以下来自 15.16 的产生式在这显示:

CastExpression:
    ( PrimitiveType ) UnaryExpression
    ( ReferenceType {AdditionalBound} ) UnaryExpressionNotPlusMinus
    ( ReferenceType {AdditionalBound} ) LambdaExpression

具有一元操作符的表达式从右向左分组,因此 -~x 表示与 -(~x) 相同。

这部分语法包含一些技巧,以避免两个潜在的句法歧义。

第一个潜在的歧义会出现在类似 (p)+q 的表达式中,对 C 或 C++ 程序员来说,其看起来就像在 q 上的一元 + 操作的到类型 p 的强制转换,或两个数目 p 和 q 的二元加法。在 C 和 C++ 中,解析器通过在解析时执行有限的语义分析,以便它知道 p 是类型名称还是变量名称,来处理此问题。

Java 采用了不同的方式。+ 操作符的结果必须是数字,并且在数字值上进行强制转换所涉及的所有类型名称都是关键字。因此,如果 p 是一个命名基元类型的关键字,则 (p)+q 只能作为一元表达式的强制转换来理解。但是,如果 p 不是命名基元类型的关键字,则 (p)+q 只能作为二元算术操作来理解。类似的结论也适用于 - 操作符。上面所示的语法将 CastExpression 分成两种情况来进行区分。非终结 UnaryExpression 包括所有一元操作符,但是非终结 UnaryExpressionNotPlusMinus 不包括所有也可能是二元操作符,其在 Java 中是 + 和 -,的一元操作符的使用。

第二个潜在的歧义是,表达式 (p)++,对 C 和 C++ 程序员来说,似乎是括号化表达式的后缀自增或强制转换的开头,例如,在 (p)++q 中。与前面一样,C 和 C++ 解析器知道,p 是类型名称还是变量名称。但是解析期间仅使用一个标记提前量同时不进行语义分析的解析器将无法判断,当 ++ 是提前量标记时,是否应将 (p) 视为 Primary 表达式,还是单独保留以供以后考虑作为 CastExpression 的一部分。

在 Java 中,++ 操作符的结果必须是数字,并且在数值上进行的强制转换所涉及的所有类型名称都是已知的关键字。因此,如果 p 是命名基元类型的关键字,则 (p)++ 只能作为前缀自增表达式的强制转换来理解,而且有一个操作数,例如 q,跟在 ++ 后面。但是,如果 p 不是命名基元类型的关键字,则 (p)++ 只能作为 p 的后缀自增来理解。类似的结论也适用于 -- 操作符。因此,非终结 UnaryExpressionNotPlusMinus 也不包括前缀操作符 ++ 和 -- 的使用。

15.15.1. 前缀自增操作符 ++

前面跟着 ++ 操作符的一元表达式是前缀自增表达式。

一元表达式的结果必须是可转换(5.1.8)为数字类型的类型的变量,否则发生编译时错误。

前缀自增表达式的类型是变量的类型。前缀自增表达式的结果不是变量,而是值。

运行时,如果操作表达式的计算异常完成,则前缀自增表达式因相同的理由而异常完成,并且不发生自增。否则,值 1 被加到变量的值上,且将和存储回变量中。在增加之前,先在值 1 和变量的值上执行二进制数字提升(5.6.2)。如有必要,和被窄化基元转换(5.1.3)窄化,且/或在存储之前受变量的类型的装箱转换(5.1.7)的影响。前缀自增表达式的值是存储新值之后的变量的值。

请注意,以上提及的二进制数字提升可能还包括拆箱转换(5.1.8)和值集转换(5.1.13)。如有必要,在存储到变量之前先应用值集转换。

声明为 final 的变量无法自增,因为当这种 final 变量的访问用作表达式时,结果是一个值,不是一个变量。因此,它无法用作前缀自增操作符的操作数。

15.15.2. 前缀自减操作符 --

前面跟着 -- 操作符的一元表达式是前缀自减表达式。

一元表达式的结果必须是其类型可转换(5.1.8)为数字类型的变量,否则发生编译时错误。

前缀自减表达式的类型是变量的类型。前缀自减表达式的结果不是变量,而是一个值。

运行时,如果操作数表达式的计算突然完成,则前缀自减表达式因相同的理由而突然完成,并且不会发生自减。否则,从变量的值中减去值 1,差数存储回变量中。在减去之前,先在值 1 和变量的值上执行二进制数值提升(5.6.2)。如有必要,通过缩小基本转换(5.1.3)对差数进行窄化,和/或差数在存储之前受变量类型的装箱转换(5.1.7)的影响。前缀自减表达式的值是存储新值之后的变量的值。

请注意,上面提及的二进制数值提升可能包括拆箱转换(5.1.8)和值集转换(5.1.13)。如有必要,在存储到变量之前先对差数应用格式转换。

声明为 final 的变量无法自减,因为当这种 final 变量的访问用作表达式时,结果是一个值,不是变量。因此,它无法用作前缀自减表达式的操作数。

15.15.3. 一元加操作符 +

一元 + 操作符的操作数表达式的类型必须是可转换(5.1.8)为基元数字类型的类型,否则发生编译时错误。

在操作数上执行一元数值提升(5.6.1)。一元加表达式的类型是操作数提升后的类型。一元加表达式的结果不是变量,而是一个值,即使操作数表达式的结果是变量。

运行时,一元加表达式的值是操作数提升后的值。

15.15.4. 一元减操作符 -

一元 - 操作符的操作数表达式的类型必须是可转换(5.1.8)为基元数字类型的类型,否则发生编译时错误。

在操作数上执行一元数值提升(5.6.1)。

一元减表达式的类型是操作数提升后的类型。

请注意,一元数值提升执行值集转换(5.1.13)。无论提升后的操作数值来自哪个值集,都将执行一元否操作,从相同的值集中得到结果。然后该结果受进一步值集转换的影响。

运行时,一元减表达式的值是操作数提升后的值的算术否。

对于整数值,否与从零减去相同。Java 编程语言将二进制补码表示用于整数,并且二进制补码值得范围不是对称的,因此最大负 int 或 long 的否产生相同的最大负数。这种情况下发生溢出,而不抛出异常。对于所有整数值 x,-x 等于 (~x) + 1。

对于浮点值,否与从零减去不相同,因为如果 x 是 +0.0,则 0.0-x 是 +0.0,但 -x 是 -0.0。一元减只是反转浮点数字的符号。特别有趣的情况:

    * 如果操作数是 NaN,则结果是 NaN。(回忆一下,NaN 没有符号(4.2.3))

    * 如果操作数是无穷,则结果是相反符号的无穷。

    * 如果操作数是零,则结果是相反符号的零。

15.15.5. 按位补码操作符 ~

一元 ~ 操作符的操作数表达式的类型必须是可转换(5.1.8)为基元整数类型的类型,否则发生编译时错误。

在操作数上执行一元数值提升(5.6.1)。一元按位求补码表达式的类型是提升后的操作数类型。

运行时,一元按位求补码表达式的值是提升后的操作数的值的按位补码。在所有情况下,~x 等于 (-x)-1。

15.15.6. 逻辑补码操作符 !

一元 ! 操作符操作数的表达式类型必须是 boolean 或 Boolean,否则发生编译时错误。

一元逻辑补码表达式的类型是 boolean。

运行时,如有必要,操作数受拆箱转换(5.1.8)的影响。一元逻辑补码表达式的值是 true,如果(可能被转换的)操作数值是 false,和 false,如果(可能被转换的)操作数值是 true。

15.16. 强制转换表达式

运行时,强制转换表达式将数字类型的值转换为另一个数字类型的类似值;或编译时确认,表达式的类型是 boolean;或运行时检查,引用值引用一个其类与指定的引用类型或引用类型列表兼容的对象。

圆括号和他们包含的类型或类型列表被称为强制转换运算符。

CastExpression:
    ( PrimitiveType ) UnaryExpression
    ( ReferenceType {AdditionalBound} ) UnaryExpressionNotPlusMinus
    ( ReferenceType {AdditionalBound} ) LambdaExpression

为了方便,以下来自 4.4 的产生式在这显示:

AdditionalBound: & InterfaceType

如果强制转换操作符包含一个类型列表 - 即,后跟一个或多个 AdditionalBound 术语的 ReferenceType - 则以下所有必须是 true,否则发生编译时错误:

    * ReferenceType 必须表示类或接口类型。

    * 所有列举的类型的擦除(4.6)必须是两两不同。

    * 没有两个列出的类型可以是同一泛型接口的不同参数化的子类型。

通过强制转换表达式引入的强制转换上下文(5.5)的目标类型是在强制转换操作符中出现的 PrimitiveType 或 ReferenceType(如果没有后跟 AdditionalBound 术语),或由在强制转换操作符中出现的 ReferenceType 和 AdditionalBound 术语表示的交集类型。

强制转换表达式的类型是将捕获转换(5.1.10)应用到此目标类型的结果。

强制转换可以用于显式地“标记”具有特定目标类型的 lambda 表达式或方法引用表达式。为了提供适当的灵活性,目标类型必须是一个表示交集类型的类型列表,前提是交集会引起函数式接口(9.8)。

强制转换表达式的结果不是变量,而是一个值,即使操作数表达式的结果是变量。

强制转换操作符对 float 或 double 类型值得值集(4.2.3)选择没有任何影响。因此,非 FP-strict(15.4)的表达式中到 float 类型的强制转换不一定会导致将其值转换为浮点值集的元素,非 FP-strict 的表达式中到 double 类型的强制转换不一定会导致将其值转换为双精度浮点值集的元素。

如果,根据强制转换(5.5)规则,操作数的编译时类型无法强制转换为由强制转换操作符指定的类型,则这是一个编译时错误。

否则,运行时,通过强制转换将操作数的值转换(如有必要)为强制转换操作符指定的类型。

如果运行时发现强制转换是不允许的,则抛出 ClassCastException。

某些强制转换会导致编译时错误。编译时可以证明某些强制转换在运行时总是正确的。例如,将类类型的值转换为其父类的值总是正确的;这种强制转换在运行时应该不需要特殊操作。最后,某些强制转换在编译时不能被证明是始终正确的或不正确的。这种强制转换需要运行时测试。有关详情信息,请参见 5.5。

15.17. 乘法操作符

操作符 *、/ 和 % 被称为乘法操作符。

MultiplicativeExpression:
    UnaryExpression
    MultiplicativeExpression * UnaryExpression
    MultiplicativeExpression / UnaryExpression
    MultiplicativeExpression % UnaryExpression

乘法操作符具有相同的优先级,语法上是左结合的(他们从左到右分组)。

乘法操作符的每个操作数的类型必须是可转换(5.1.8)为基元数字类型的类型,否则发生编译时错误。

在操作数上执行二进制数值提升(5.6.2)。

请注意,二进制数值提升执行值集转换,还可能执行拆箱转换(5.1.8)。

乘法表达式的类型是其操作数提升后的类型。

如果提升后的类型是 int 或 long,则执行整数算术。

如果提升后的类型是 float 或 double,则执行浮点算术。

15.17.1. 乘法操作符 *

二元 * 操作符执行乘法,生成其操作数的乘积。

如果操作数表达式没有副作用,则乘法是可交换的操作。

当操作数都是相同的类型时,整数乘法是可结合的。

浮点乘法不是可结合的。

如果整数乘法溢出,则结果是在某个足够大的二进制补码格式中表示的数学乘积的低位 bits。因此,如果发生溢出,则结果的符号与两个操作数值的数学乘积的符号可能不同。

浮点乘法的结果由 IEEE 754 算术规则确定:

    * 如果任一操作数是 NaN,则结果是 NaN。

    * 如果结果不是 NaN,则如果两个操作数具有相同的符号,结果是正的,如果两个操作数具有不同的符号,结果是负的。

    * 无穷乘以零产生 NaN。

    * 无穷乘以有穷值产生带符号的无穷。由以上声明的规则确定符号。

    * 剩余情况下,只要不涉及无穷或 NaN,计算精确的算术乘积。然后选择一个浮点值集:

        如果乘法表达式是 FP-strict(15.4)的:

            如果乘法表达式的类型是 float,则选择浮点值集。

            如果乘法表达式的类型是 double,则选择双精度浮点值集。

        如果乘法表达式不是 FP-strict 的:

            如果乘法表达式的类型是 float,则可以选择浮点值集或浮点扩展指数值集,看实现的选择。

            如果乘法表达式的类型是 double,则可以选择双精度浮点值集或双精度扩展指数值集,看实现的选择。

      接下来,必须从选定的值集中选择一个值来表示乘积。

      如果乘积的量级太大而无法表示,我们称这为操作溢出;然后结果是一个具有适当符号的无穷。

      否则,将乘积舍入到选定的值集中使用 IEEE 754 rount-to-nearest 模式的最接近值。Java 编程语言要求支持 IEEE 754(4.2.4)定义的渐进下溢。

尽管可能发生上溢、下溢或丢失信息,但乘法操作符 * 的计算从不抛出运行时异常。

15.17.2. 除法操作符 /

二元 / 操作符执行除法,产生其操作数的商。左侧操作数是被除数,右侧操作数是除数。

整数除法向 0 舍入。即,二进制数值提升(5.6.2)后的为整数的操作数 n 和 d 产生的商是整数值 q,其量级尽可能大,同时满足 |d ⋅ q| ≤ |n|。而且,当 |n| ≥ |d| 且 n 和 d 具有相同的符号时,q 是正数,而当 |n| ≥ |d| 且 n 和 d 具有相反的符号时,q 是负数。

有一种不满足此规则的情况:如果被除数是其类型最大量级的负整数,除数是 -1,则发生整数溢出,结果等于被除数。尽管溢出,这种情况不会抛出任何异常。另一方面,如果整数除法中除数的值是 0,则抛出 ArithmeticException。

由 IEEE 754 算术规则确定浮点除法的结果:

    * 如果任一操作数是 NaN,则结果是 NaN。

    * 如果结果不是 NaN,则如果两个操作数具有相同的符号,则结果的符号是正的,如果两个操作数具有不同的符号,则是负的。

    * 无穷除以无穷产生 NaN。

    * 无穷除以有穷值产生带符号的无穷。由以上声明的规则确定符号。

    * 有穷值除以无穷产生带符号的零。由以上声明的规则确定符号。

    * 零除以零产生 NaN;零除以任何其他有穷值产生带符号的零。由以上声明的规则确定符号。

    * 非零有穷值除以零产生带符号的无穷。由以上声明的规则确定符号。

    * 剩余情况下,只要不涉及无穷或 NaN,计算精确的算术商。然后选择一个浮点值集:

        如果除法表达式是 FP-strict(15.4)的:

            如果除法表达式的类型是 float,则选择浮点值集。

            如果除法表达式的类型是 double,则选择双精度浮点值集。

        如果除法表达式不是 FP-strict 的:

            如果除法表达式的类型是 float,则可以选择浮点值集或浮点扩展指数值集,看实现的选择。

            如果除法表达式的类型是 double,则可以选择双精度浮点值集或双精度浮点扩展指数值集,看实现的选择。

      接下来,必须从选定的值集中选择一个值来表示商。

      如果商的量级太大而无法表示,则我们称操作溢出;然后结果是具有适当符号的无穷。

      否则,使用 IEEE 754 round-to-nearest 模式将商舍入到选定值集中最接近的值。Java 编程语言要求支持 IEEE 754(4.2.4)定义的渐进下溢。

尽管可能发生上溢、下溢或丢失信息,浮点除法操作符 / 的计算从不抛出运行时异常。

15.17.3. 取余操作符 %

二元 % 操作符被称为从隐含的除法中产生其操作数的余数;左侧操作数是被除数,右侧操作数是除数。

在 C 和 C++ 中,取余操作只接受整数,但在 Java 编程语言中,它还接受浮点操作数。

二进制数值提升(5.6.2)后整数操作数的取余操作产生一个结果值,(a/b)*b+(a%b) 等于 a。

即使在特殊情况下,被除数是其类型最大量级的负整数,而除数是 -1(余数是 0),此恒等式仍然保持。

仅当被除数是负数时,取余操作的结果可以是负数,仅当被除数是正数时,取余操作的结果可以是正数,这仍然遵循此规则。而且,结果的量级总是小于除数的量级。

如果整数取余操作除数的值是 0,则抛出 ArithmeticException。

avatar

由 % 操作符计算的浮点取余操作的结果与 IEEE 754 定义的取余操作产生的结果不同。IEEE 754 取余操作从舍入除法计算余数,而不是截断除法,因此它的行为与通常的整数取余操作的行为不相似。相反,Java 编程语言在浮点操作上定义 %,以类似于整数取余操作的行为的方式来表现行为;这可以与 C 库函数 fmod 进行比较。可以通过库例程 Math.IEEEremainder 来计算 IEEE 754 取余操作。

由 IEEE 754 算术规则来确定浮点取余操作的结果:

    * 如果任一操作数是 NaN,则结果是 NaN。

    * 如果结果不是 NaN,则结果的符号等于被除数的符号。

    * 如果被除数是无穷,或除数是零,或另种情况都是,则结果是 NaN。

    * 如果被除数是有穷数,除数是无穷,则结果等于被除数。

    * 如果被除数是零,除数是有穷数,则结果等于被除数。

    * 剩余情况下,只要不涉及无穷、零和 NaN,被除数 n 除以除数 d 的浮点余数 r 由算术关系 r = n - (d ⋅ q) 定义,其中仅当 n/d 是负数时,q 是负整数,仅当 n/d 是正数时,q 是正整数,其量级尽可能大,不超出 n 和 d 的实际算术商的量级。

浮点取余操作 % 的计算从不抛出运行时异常,即使右侧操作数是零。不可能发生上溢、下溢或丢失信息。

avatar

15.18. 加法操作符

操作符 + 和 - 被称为加法操作符。

AdditiveExpression:
    MultiplicativeExpression
    AdditiveExpression + MultiplicativeExpression
    AdditiveExpression - MultiplicativeExpression

加法操作符具有相同的优先级,语法上是左结合的(他们从左到右分组)。

如果 + 操作符的任一操作数的类型都是 String,则该操作是字符串拼接。

否则,+ 操作符的每个操作数的类型都必须是可转换(5.1.8)基元数字类型的类型,否则发生编译时错误。

在每种情况下,二元 - 操作符的每个操作数的类型必须是可转换(5.1.8)为基元数字类型的类型,否则发生编译时错误。

15.18.1. String 拼接操作符 +

如果只有一个操作数表达式是 String 类型,则运行时在另一个操作数上执行字符串转换,以产生一个字符串。

字符串拼接的结果是 String 对象的引用,其是两个操作数字符串的拼接。在新创建的字符串中,左侧操作数的字符在右侧操作数的字符前面。

除非表达式是常量表达式(15.28),否则新创建(12.5)String 对象。

实现可以选择在一个步骤中执行转换和拼接,以避免创建然后丢弃中间 String 对象。为了提高重复字符串拼接的性能,Java 编译器可能使用 StringBuffer 类或类似的技术来减少由表达式计算创建的中间 String 对象的数量。

对于基元类型,实现也可能通过直接将基元类型转换为字符串来避免包装其对象创建的优化。

avatar avatar

avatar avatar

15.18.2. 数字类型的加法操作符(+ 和 -)

二元 + 操作符执行加法,当应用到两个数字类型的操作数上时,产生操作数的总和。

二元 - 操作符执行减法,产生两个数字操作数的差值。

在操作数上执行二进制数值提升(5.6.2)。

请注意,二进制数值提升执行值集转换(5.1.13),也可能执行拆箱转换(5.1.8)。

数字操作数上加法表达式的类型是其操作数提升后的类型。

如果提升后的类型是 int 或 long,则执行整数算术。

如果提升后的类型是 float 或 double,则执行浮点算术。

加法是可交换的操作,如果操作数表达式没有副作用。

整数加法是可结合的,当操作数都是相同的类型时。

浮点加法不是可结合的。

如果整数加法溢出,则结果是在某个足够大的二进制补码格式中表示的算术总和的低位 bits。如果发生溢出,则结果的符号与两个操作数值得算术总和的符号不同。

使用 IEEE 754 算术规则确定浮点加法的结果:

    * 如果任一操作数是 NaN,则结果是 NaN。

    * 两个相反符号的无穷的总和是 NaN。

    * 两个相同符号的无穷的总和是该符号的无穷。

    * 无穷和有穷值得总和等于无穷操作数。

    * 两个相反符号的零的总和是正零。

    * 两个相同符号的零的总和是该符号的零。

    * 零和非零有穷值得总和等于非零操作数。

    * 两个相同量级、相反符号的非零有穷值的总和是正零。

    * 剩余情况下,只要不涉及无穷、零和 NaN,操作数具有相同的符号或具有不同的量级,计算精确的算术总和。然后选择浮点值集:

        如果加法表达式是 FP-strict(15.4)的:

            如果加法表达式的类型是 float,则选择浮点值集。

            如果加法表达式的类型是 double,则选择双精度浮点值集。

        如果加法表达式不是 FP-strict 的:

            如果加法表达式的类型是 float,则可以选择浮点值集或浮点扩展指数值集,看实现的选择。

            如果加法表达式的类型是 double,则可以选择双精度浮点值集或双精度扩展指数值集,看实现的选择。

    接下来,必须从选定的值集中选择一个值,以表示总和。

    如果总和的量级太大而无法表示,则我们称操作溢出;然后结果是具有适当符号的无穷。

    否则,使用 IEEE 754 round-to-nearest 模式将总和舍入到选定的值集中的最接近值。Java 编程语言要求支持 IEEE 754(4.2.4)定义的逐渐下溢。

二元 - 操作符执行减法,当应用到两个数字类型的操作数上时,产生其操作数的差值;左侧操作数是被减数,右侧操作数是减数。

对于整数和浮点减法,始终是 a-b 产生与 a+(-b) 相同的结果的情况。

尽管可能发生溢出、下溢或丢失信息,数字加法操作符的计算从不抛出运行时异常。

5.19. 移位操作符

操作符 <<(左移)、>>(右移)和 >>>(无符号右移)被称为移位操作符。移位操作符左侧的操作数是要被移位的值,右侧操作数指定移位距离。

ShiftExpression:
    AdditiveExpression
    ShiftExpression << AdditiveExpression
    ShiftExpression >> AdditiveExpression
    ShiftExpression >>> AdditiveExpression

移位操作符在句法上是左结合的(他们从左到右分组)。

分别在每个操作数上执行一元数值提升(5.6.1)。(不在操作数上执行二进制数值提升(5.6.2)。)

如果移位操作符的每个操作数的类型在一元数值提升之后不是基元整数类型,则这是一个编译时错误。

移位表达式的类型是左侧操作数提升后的类型。

如果左侧操作数提升后的类型是 int,则右侧操作数只有最低五个 bits 用作移位距离。就像右侧操作数受具有掩码值 0x1f(0b11111)的按位逻辑 AND 操作符 &(15.22.1)的影响一样。因此实际使用的移位距离总是在 0 到 31 之间,包括 31。

如果左侧操作数提升后的类型是 long,则右侧操作数只有最低六个 bits 用作移位距离。就像右侧操作数受具有掩码值 0x3f(0b111111)的按位逻辑 AND 操作符 &(15.22.1)一样。因此实际使用的移位距离总是在 0 到 63 之间,包括 63。

运行时,在左操作数值的二进制补码表示上执行移位操作。

n<<s 的值是 n 左移 s 个 bit 位置;这等于(即使发生溢出)乘以 2 的 s 次幂。

n>>s 的值是 带有符号扩展的 n 右移 s 个 bit 位置。结果值是 floor(n / 2s)。对于 n 的非负值,这等于截断整数除法,用整数除法操作符 / 除以 2 的 s 次幂一样。

n>>>s 的值是具有零扩展的 n 右移 s 个 bit 位置,其中:

    * 如果 n 是正数,则结果与 n>>s 相同。

    * 如果 n 是负数,左侧操作数的类型是 int,则结果等于表达式 (n>>s) + (2<<~s) 的值。

    * 如果 n 是负数,左侧操作数的类型是 long,则结果等于表达式 (n>>s) + (2L<<~s) 的值。

增加项 (2<<~s) 或 (2L<<~s) 抵消了传播的符号位。

请注意,由于移位操作符右侧操作数的隐式掩盖,当移位 int 值时,作为移位距离的 ~s 等于 31-s,当移位 long 值时,等于 63-s。

15.20. 比较操作符

数字比较操作符 <、>、<= 和 >= 以及 instanceof 操作符被称为比较操作符。

RelationalExpression:
    ShiftExpression
    RelationalExpression < ShiftExpression
    RelationalExpression > ShiftExpression
    RelationalExpression <= ShiftExpression
    RelationalExpression >= ShiftExpression
    RelationalExpression instanceof ReferenceType

比较操作符在句法上是左结合的(他们从左到右分组)。

然而,这一事实实际上是没有用的。例如,a<b<c 解析为 (a<b)<c,这总是编译时错误,因为 a<b 的类型总是 boolean,< 不是用在 boolean 值上的操作符。

比较表达式的类型总是 boolean。

15.20.1. 数字比较操作符 <、<=、> 和 >=

数字比较操作符每个操作数的类型必须是可转换(5.1.8)为基元数字类型的类型,否则发生编译时错误。

在操作数上执行二进制数值提升(5.6.2)。

请注意,二进制数值提升执行值集转换(5.1.13),还可能执行拆箱转换(5.1.8)。

如果操作数提升后的类型是 int 或 long,则执行带符号整数比较。

如果提升后的类型是 float 或 double,则执行浮点比较。

对浮点值采用精确的比较,不管他们的表示值来自哪个值集。

浮点比较的结果,根据 IEEE 754 规范确定,是:

    * 如果任一操作数是 NaN,则结果是 false。

    * 除 NaN 以外的所有值都是有序的,负无穷小于所有有穷值,正无穷大于所有有穷值。

    * 正零和负零被认为是相等的。

      例如,-0.0<0.0 是 false,而 -0.0<=0.0 是 true。

      但是,请注意,方法 Math.min 和 Math.max 严格上将负零视为小于正零。

根据浮点数字的这些注意事项,以下规则仍支持除 NaN 之外的整数操作数或浮点操作数:

    * 由 < 操作符产生的值是 true,如果左侧操作数的值小于右侧操作数的值,否则是 false。

    * 由 <= 操作符产生的值是 true,如果左侧操作数的值小于或等于右侧操作数的值,否则是 false。

    * 由 > 操作符产生的值是 true,如果左侧操作数的值大于右侧操作数的值,否则是 false。

    * 由 >= 操作符产生的值是 true,如果左侧操作数的值大于或等于右侧操作数的值,否则是 false。

15.20.2. 类型比较操作符 instanceof

instanceof 操作符的 RelationalExpressioin 操作数的类型必须是引用类型或 null 类型;否则,发生编译时错误。

如果 instanceof 操作符后面提及的 ReferenceType 不表示可具体化的(4.7)的引用类型,则这是一个编译时错误。

如果将到 ReferenceType 的 RelationalExpression 的强制转换作为编译时错误拒绝,则 instanceof 比较表达式同样产生编译时错误。在这种情况下,instanceof 表达式的结果永远不可能为 true。

运行时,instanceof 操作符的结果是 true,如果 RelationalExpression 的值不是 null,引用可以在不引起 ClassCastException 的情况下强制转换为 ReferenceType。否则,结果是 false。

avatar

15.21. 相等操作符

操作符 ==(等于)和 !=(不等于)被称为相等操作符。

EqualityExpression:
    RelationalExpression
    EqualityExpression == RelationalExpression
    EqualityExpression != RelationalExpression

相等操作符在句法上是左结合的(他们从左到右分组)。

但是,这一事实基本上是没有用的。例如,a==b==c 解析为 (a==b)==c。a==b 的结果类型总是 boolean,因此 c 必须是 boolean 类型,否则发生编译时错误。因此,a==b==c 无法测试 a、b 和 c 是否都是相等的。

相等操作符是可交换的,如果操作数表达式没有副作用。

除了较低的优先级之外,相等操作符与关系操作符是类似的。因此,当 a<b 和 c<d 具有相同的真伪值时,a<b==c<d 为 true。

相等操作符可以用于比较两个可转换(5.1.8)数字类型的操作数,或两个 boolean 或 Boolean 类型的操作数,或是引用类型或 null 类型的两个操作符。所有其他情况都会导致编译时错误。

相等表达式的类型总是 boolean。

在所有情况下,a!=b 产生与 !(a==b) 相同的值。

15.21.1. 数字相等操作符 == 和 !=

如果相等操作符的操作数都是数字类型,或一个是数字类型,另一个是可转换(5.1.8)数字类型的类型,则在操作数上执行二进制数值提升(5.6.2)。

请注意,二进制数值提升执行值集转换(5.1.13),还可能执行拆箱转换(5.1.8)。

如果操作数提升后的类型是 int 或 long,则执行整数相等测试。

如果提升后的类型是 float 或 double,则执行浮点相等测试。

在浮点值上采取精确的比较,不管表示他们的值来自哪个值集。

根据 IEEE 754 标准规则,执行浮点相等测试:

    * 如果任一操作数是 NaN,则 == 的结果是 false,而 != 的结果是 true。

      事实上,当且仅当 x 的值是 NaN 时,测试 x!=x 是 true。

      方法 Float.isNaN 和 Double.isNaN 也可以用于测试值是否是 NaN。

    * 正零和负零被认为是相等的。

      例如,-0.0==0.0 是 true。

    * 否则,根据相等操作符两个不同的浮点值被视为不相等的。

      具体来说,有一个值表示正无穷,有一个值表示负无穷;每个比较仅与自身相等,每个比较与所有其他值不相等。

根据浮点数字的这些注意事项,以下规则仍支持除 NaN 之外的整数操作数或浮点操作数:

    * 由 == 操作符产生的值是 true,如果左侧操作数的值等于右侧操作数的值;否则,结果是 false。

    * 由 != 操作符产生的值是 true,如果左侧操作数的值不等于右侧操作数的值;否则,结果是 false。

15.21.2. Boolean 相等操作符 == 和 !=

如果相等操作符的操作数都是 boolean 类型,或如果一个操作数的类型是 boolean,另一个是 Boolean 类型,则操作是 boolean 相等。

boolean 相等操作数是可结合的。

如果某个操作数是 Boolean 类型,则它受拆箱转换(5.1.8)的影响。

== 的结果是 true,如果操作数(在任何必需的拆箱转换之后)都是 true 或都是 false;否则,结果是 false。

!= 的结果是 false,如果操作数都是 true 或都是 false;否则,结果是 true。

因此,当应用于 boolean 操作数时,!= 行为表现与 ^(15.22.2)一样。

15.21.3. 引用相等操作符 == 和 !=

如果相等操作符的操作数都是引用类型或 null 类型,则操作是对象相等。

如果无法通过强制转换(5.5)将任一操作数的类型转换为另一个的类型,则这是一个编译时错误。两个操作数的运行时值必然不相等(忽略两个值都是 null 的情况)。

运行时,== 的结果是 true,如果操作数的值都是 null 或都引用相同的对象或数组;否则,结果是 false。

!= 的结果是 false,如果操作数的值都是 null 或都引用相同的对象或数组;否则,结果是 true。

虽然 == 可以用来比较 String 类型的引用,但这种相等测试确定两个操作数是否引用同一个 String 对象。如果操作数是不同的 String 对象,则结果是 false,即使他们包含相同的字符序列(3.10.5)。可以通过方法调用 s.equals(t) 来测试两个字符串 s 和 t 内容的相等性。

15.22. 按位和逻辑操作符

按位操作符和逻辑操作符包括 AND 操作符 &、互斥 OR 操作符 ^ 和包括 OR 操作符 |。

AndExpression:
    EqualityExpression
    AndExpression & EqualityExpression

ExclusiveOrExpression:
    AndExpression
    ExclusiveOrExpression ^ AndExpression

InclusiveOrExpression:
    ExclusiveOrExpression
    InclusiveOrExpression | ExclusiveOrExpression

这些操作符具有不同的优先级,& 具有最高的优先级,| 具有最低的优先级。

这些操作符中的每个在句法上是左结合的(每个从左到右分组)。

每个操作符是可交换的,如果操作数表达式没有副作用。

每个操作符是可结合的。

按位和逻辑操作符可用于比较两个数字类型的操作数或两个 boolean 类型的操作数。所有其他情况导致编译时错误。

15.22.1. 整数按位操作符 &、^ 和 |

当操作符 &、^ 或 | 的操作数都是可转换(5.1.8)为基元整数类型的类型时,首先在操作数上执行二进制数值提升(5.6.2)。

按位操作符表达式的类型是操作数提升后的类型。

对于 &,结果值是操作数值的按位 AND。

对于 ^,结果值是操作数值的按位互斥 OR。

对于 |,结果值是操作数值的按位包含 OR。

avatar

15.22.2. 布尔逻辑操作符 &、^ 和 |

当 &、^ 或 | 的操作数都是 boolean 或 Boolean 类型时,按位操作符表达式的类型是 boolean。在所有情况下,如有必要,操作数受拆箱转换(5.1.8)的影响。

对于 &,结果值是 true,如果操作数的值都是 true;否则,结果是 false。

对于 ^,结果是 true,如果操作数的值是不同的;否则,结果是 false。

对于 |,结果是 false,如果操作数的值都是 false;否则,结果是 true。

15.23. 条件与操作符 &&

条件与操作符 && 就像 &(15.22.2)一样,但是仅当左侧操作数的值是 true 时才计算它的右侧操作数。

ConditionalAndExpression:
    InclusiveOrExpression:
    ConditionalAndExpression && InclusiveOrExpression

条件与操作符在句法上是左结合的(它从左到右分组)。

条件与操作符对于副作用和结果值都是可完全结合的。即,对于任何表达式 a、b 和 c,表达式 ((a) && (b)) && (c)) 的计算产生相同的结果,具有以相同的顺序出现的相同副作用,就像表达式 (a) && ((b) && (c)) 的计算一样。

条件与操作符的每个操作数必须是 boolean 或 Boolean 类型,否则发生编译时错误。

条件与表达式的类型总是 boolean。

运行时,首先计算左侧操作数表达式,如果结果是类型 Boolean,则它受拆箱转换(5.1.8)的影响。

如果产生的值是 false,则条件与表达式的值是 false,并且不再计算右侧操作数表达式。

如果左侧操作数的值是 true,则计算右侧表达式;如果结果是类型 Boolean,则它受拆箱转换(5.1.8)的影响。产生的值变成条件与表达式的值。

因此,&& 在 boolean 操作数上计算与 & 相同的结果。他们仅在有条件地计算右侧操作数表达式而不是总是上有所不同。

15.24. 条件或操作符 ||

条件或操作符 || 就像 |(15.22.2)一样,但是仅当其左侧操作数的值是 false 时才计算其右侧操作数。

ConditionalOrExpression:
    ConditionalAndExpression
    ConditionalOrExpression || ConditionalAndExpression

条件或操作符在句法上是左结合的(它从左到右分组)。

条件或操作符对于副作用和结果值是可完全结合的。即,对于任何表达式 a、b 和 c,表达式 ((a) || (b)) || (c) 产生相同的值,具有以相同的顺序出现的相同副作用,就像表达式 (a) || ((b) || (c)) 的计算一样。

条件或操作符的每个操作数必须是 boolean 或 Boolean 类型,否则发生编译时错误。

条件或表达式的类型总是 boolean。

运行时,首先计算左侧操作数表达式;如果结果是类型 Boolean,则它受拆箱转换(5.1.8)的影响。

如果产生的值是 true,则条件或表达式的值是 true,并且不再计算右侧操作数表达式。

如果左侧操作数的值是 false,则计算右侧表达式;如果结果是类型 Boolean,则它受拆箱转换(5.1.8)的影响。产生的值变成条件或表达式的值。

因此,|| 在 boolean 或 Boolean 操作数上计算与 | 相同的结果。他们仅在有条件地计算右侧操作数表达式而不是总是上有所不同。

15.25. 条件操作符 ? :

条件操作符 ? : 使用表达式的布尔值来确定应该计算另两个表达式中的哪一个。

ConditionalExpression:
    ConditionalOrExpression
    ConditionalOrExpression ? Expression : ConditionalExpression
    ConditionalOrExpression ? Expression : LambdaExpression

条件操作符在句法上是右结合的(它从右到左分组)。因此,a?b:c?d:e?f:g 意味着与 a?b:(c?d:(e?f:g)) 相同。

条件操作符具有三个操作数表达式。? 出现在第一个和第二个表达式之间,: 出现在第二个和第三个表达式之间。

第一个表达式必须是 boolean 或 Boolean 类型,否则发生编译时错误。

如果第二个和第三个操作数表达式任一一个是 void 方法调用,则这是一个编译时错误。

实际上,根据表达式语句(14.8)的语法,不允许条件表达式出现在 void 方法调用可以出现在的任何位置。

有三种条件表达式,根据第二个和第三个操作数表达式分类:布尔条件表达式、数字条件表达式和引用条件表达式。分类规则如下:

    * 如果第二个和第三个操作数表达式都是布尔表达式,则条件表达式是布尔条件表达式。

      为了对条件进行分类,下面的表达式是布尔表达式:

        独立形式(15.2)的具有类型 boolean 或 Boolean 的表达式。

        括号化布尔表达式(15.8.5)。

        类 Boolean 的类实例创建表达式(15.9)。

        方法调用表达式(15.12),为其选定的最具体的方法(15.12.2.5)具有返回类型 boolean 或 Boolean。

        请注意,对于泛型方法,这是在实例化方法的类型参数之前的类型。

        布尔条件表达式。

    * 如果第二个和第三个操作数表达式都是数字类型,则条件表达式是数字条件表达式。

      为了对条件进行分类,下面的表达式是数字表达式:

        独立形式(15.2)的具有可转换为数字类型(4.2,5.1.8)的类型的表达式。

        括号化数字表达式(15.8.5)。

        可转换为数字类型的类的类实例创建表达式(15.9)。

        方法调用表达式,为其选定的最具体的方法(15.12.2.5)具有可转换为数字类型的返回类型。

        数字条件表达式。

    * 否则,条件表达式是引用条件表达式。

确定条件表达式的类型的处理过程依赖于条件表达式的种类,如下面部分的轮廓图所述。

下面的表格通过给定其第二个和第三个操作数的所有可能类型的条件表达式的类型来总结这些规则。bnp(..) 意味着应用二进制数值提升。当一个操作数是 int 类型的常量表达式时,使用形式“T | bnp(..)”,并且在类型 T 中是可表示的,如果操作数在类型 T 中不是可表示的,则使用二进制提升时。操作数类型 Object 意味着除 null 类型之外的任何引用类型和八个包装器类 Boolean、Byte、Short、Character、Integer、Long、Float、Double。

avatar

avatar

avatar

avatar

![avatar][15.25-5]

运行时,首先计算条件表达式的第一个操作数表达式。如有必要,在结果上执行拆箱转换。

然后使用产生的 boolean 值选择第二个或第三个操作数表达式任意一个:

    * 如果第一个操作数的值是 true,则选定第二个操作数表达式。

    * 如果第一个操作数的值是 false,则选定第三个操作数表达式。

然后计算选定的操作数表达式,产生的值转换为由以上声明的规则确定的条件表达式的类型。

转换可能包括装箱或拆箱转换(5.1.7,5.1.8)。

不为条件表达式的特定计算计算未选择的操作数表达式。

15.25.1. 布尔条件表达式

布尔条件表达式是独立表达式(15.2)。

布尔条件表达式的类型按如下确定:

    * 如果第二个和第三个操作数都是 Boolean 类型,则条件表达式是 Boolean 类型。

    * 否则,条件表达式是 boolean 类型。

15.25.2. 数字条件表达式

数字条件表达式是独立表达式(15.2)。

数字条件表达式的类型按如下确定:

    * 如果第二个和第三个操作数具有相同的类型,则该类型就是条件表达式的类型。

    * 如果第二个和第三个操作数之一是基元类型 T,另一个类型是将装箱转换(5.1.7)应用于 T 的结果,则条件表达式的类型是 T。

    * 如果操作数之一是 byte 或 Byte 类型,另一个是 short 或 Short 类型,则条件表达式的类型是 short。

    * 如果操作数之一是 T 类型,其中 T 是 byte、short 或 char,另一个操作数是其值在类型 T 中可表示的 int 类型的常量表达式(15.28),则条件表达式的类型是 T。

    * 如果操作数之一是 T 类型,其中 T 是 Byte、Short 或 Character,另一个操作数是 int 类型的常量表达式,其值在为将拆箱转换应用于 T 的结果的类型 U 中是可表示的,则条件表达式的类型是 U。

    * 否则,将二进制数值提升(5.6.2)应用于操作数类型,条件表达式的类型是第二个和第三个操作数提升后的类型。

      请注意,二进制数值提升执行值集转换(5.1.13),还可能执行拆箱转换(5.1.8)。

15.25.3. 引用条件表达式

引用条件表达式是聚合表达式,如果它出现在赋值上下文或调用上下文(5.2,5.3)中。否则,它是独立表达式。

只要聚合引用条件表达式出现在具有目标类型 T 的特定种类的上下文中,它的第二个和第三个操作数表达式同样出现在具有目标类型 T 的相同种类的上下文中。

聚合引用条件表达式的类型与其目标类型相同。

独立引用条件表达式的类型按如下确定:

    * 如果第二个和第三个操作数具有相同的类型(其可能是 null 类型),则该类型是条件表达式的类型。

    * 如果第二个和第三个操作数之一的类型是 null 类型,另一个操作数的类型是引用类型,则条件表达式的类型是该引用类型。

    * 否则,第二个和第三个操作数分别是类型 S1 和 S2。让 T1 是将装箱转换应用于 S1 后产生的类型,让 T2 是将装箱转换应用于 S2 后产生的类型。条件表达式的类型是将捕获转换(5.1.10)应用于 lub(T1, T2) 后的结果。

因为引用条件表达式可以是聚合表达式,所以他们可以将上下文“传递”到他们的操作数。这允许 lambda 表达式和方法引用表达式作为操作数出现:

avatar

它还允许使用额外的信息来改进泛型方法调用的类型检查。在 Java SE 8 之前,此赋值时类型良好的:

avatar

但这个不是:

avatar

以上规则允许两个赋值都被认为是类型良好的。

请注意,引用条件表达式不必为了成为聚合表达式而将聚合表达式包含为操作数。它是一个聚合表达式,仅凭借它出现在的上下文。例如,在以下代码中,条件表达式是聚合表达式,每个操作数被视为在以 Class<? super Integer> 为目标的赋值上下文中:

avatar

如果条件表达式不是聚合表达式,则会发生编译时错误,因为它的类型将是 lub(Class, Class) = Class<? extends Number>,其与选择的返回类型不兼容。

15.26. 赋值操作符

有 12 个赋值操作符;所有在句法上是右结合的(他们从右到左分组)。因此,a=b=c 意味着 a=(b=c),其将 c 的值赋给 b,然后将 b 的值赋给 a。

AssignmentExpression:
    ConditionalExpression
    Assignment

Assignment:
    LeftHandSide AssignmentOperator Expression

LeftHandSide:
    ExpressionName
    FieldAccess
    ArrayAccess

AssignmentOperator:
    (one of)
    = *= /= %= += -= <<= >>= >>>= &= ^= |=

赋值操作符的第一个操作数的结果必须是变量,否则发生编译时错误。

此操作数可以是命名变量,例如局部变量或当前对象或类的字段,也可以是计算出的变量,如同是由字段访问(15.11)或数组访问(15.10.3)产生的。

赋值表达式的类型是捕获转换(5.1.10)之后的变量的类型。

运行时,赋值表达式的结果是赋值发生后变量的值。赋值表达式的结果本身不是变量。

声明为 final 的变量无法被赋值(除非它是明确未赋值的(16(Definite Assignment))),因为当这种 final 变量的访问用作表达式时,结果是一个值,而不是变量,因此它无法用作赋值操作符的第一个操作符。

15.26.1. 简单赋值操作符 =

如果右侧操作数的类型无法通过赋值转换(5.2)转换为变量的类型,则这是编译时错误。

运行时,以三种方法之一计算表达式。

如果左侧操作数表达式是字段访问表达式 e.f(15.11),可能封闭在一个或多个括号对中,则:

    * 首先,计算表达式 e。如果 e 的计算突然完成,则赋值表达式因相同的理由而突然完成。

    * 接下来,计算右手边操作数。如果右手边表达式的计算突然完成,则赋值表达式因相同的理由而突然完成。

    * 然后,如果由 e.f 表示的字段不是 static,上面 e 的计算的结果是 null,则抛出 NullPointerException。

    * 否则,将如上面计算的右手边操作数的值赋给由 e.f 表示的变量。

如果左侧操作数是数组访问表达式(15.10.3),可能封闭在一个或多个括号对中,则:

    * 首先,计算左侧操作数数组访问表达式的数组引用子表达式。如果此计算突然完成,则赋值表达式因相同的理由而突然完成;不再计算索引子表达式(左侧操作数数组访问表达式的)和右侧操作数,不发生赋值。

    * 否则,计算左侧操作数数组访问表达式的索引子表达式。如果此计算突然完成,则赋值表达式因相同的理由而突然完成,不再计算右侧操作数,不发生赋值。

    * 否则,计算右侧操作数。如果此计算突然完成,则赋值表达式因相同的理由而突然完成,不发生赋值。

    * 否则,如果数组引用子表达式的值是 null,则不发生赋值,并抛出 NullPointerException。

    * 否则,数组引用子表达式的值实际上引用一个数组。如果索引子表达式的值小于零,或者大于或等于数组的 length,则不发生赋值,并抛出 ArrayIndexOutOfBoundsException。

    * 否则,索引子表达式的值用于选择由数组引用子表达式引用的数组的组件。

      此组件是一个变量;称其类型为 SC。而且,让 TC 是编译时确定的赋值操作符的左侧操作数的类型。然后有两种可能:

        如果 TC 是基元类型,则 SC 必须与 TC 相同。

        右侧操作数的值转换为选定的数组组件的类型,受适当标准值集(不是扩展指数值集)的值集转换的影响,并且将转换的结果存储到数组组件中。

        如果 TC 是引用类型,则 SC 可以与 TC 不同,而是 extends 或 implements TC 的类型。

        让 RC 是运行时由右侧操作数的值引用的对象的类。

        Java 编译器能够在编译时证明,数组组件肯定是类型 TC(例如,TC 可以是 final)。但是如果 Java 编译器无法在编译时证明,数组组件肯定是类型 TC,则运行时必须执行检查,以保证类 RC 与数组组件的实际类型 SC 是兼容的。

        此检查类似窄化强制转换(5.5,15.16),除如果检查失败,抛出 ArrayStoreException 而不是 ClassCastException 之外。

        如果类 RC 无法赋给类型 SC,则不发生赋值,并抛出 ArrayStoreException。

        否则,将右侧操作数的引用值存储到选定的数组组件中。

否则,必须进行第三步:

    * 首先,计算左侧操作数以产生一个变量。如果此计算突然完成,则赋值表达式因相同的理由而突然完成;不再计算右侧操作数,不发生赋值。

    * 否则,计算右侧操作数。如果此计算突然完成,则赋值表达式因相同的理由而突然完成,不发生赋值。

    * 否则,右侧操作数的值转换为左侧变量的类型,受适当标准值集的值集转换(5.1.13)的影响(不是扩展指数值集),将转换的结果存储到变量中。

15.26.2. 复合赋值操作符

形式 E1 op= E2 的复合赋值表达式等价于 E1 = (T) ((E1) op (E2)),其中 T 是 E1 的类型,除了 E1 仅计算一次之外。

avatar

运行时,以两种方式中的一个计算表达式。

如果左侧操作数表达式不是数组访问表达式,则:

    * 首先,计算左侧操作数以产生一个变量。如果此计算突然完成,则赋值表达式因相同的理由而突然完成;不再计算右侧操作数,不发生赋值。

    * 否则,保存左侧操作数的值,然后计算右侧操作数。如果此计算突然完成,则赋值表达式因相同的理由而突然完成,不发生赋值。

    * 否则,使用保存的左侧变量的值和右侧操作数的值来执行由符合赋值操作符指示的二元操作。如果此操作突然完成,则赋值表达式因相同的理由而突然完成,不发生赋值。

    * 否则,二元操作的结果转换为左侧变量的类型,受适当标准值集的值集转换(5.1.13)的影响,将转换的结果存储到变量中。

如果左侧操作数表达式是数组访问表达式(15.10.3),则:

    * 首先,计算左侧操作数数组访问表达式的数组引用子表达式。如果此计算突然完成,则赋值表达式因相同的理由而突然完成;不再计算索引子表达式(左侧操作数数组访问表达式的)和右侧操作数,不发生赋值。

    * 否则,计算左侧操作数数组访问表达式的索引子表达式。如果此计算突然完成,则赋值表达式因相同的理由而突然完成,不再计算右侧操作数,不发生赋值。

    * 否则,如果数组引用子表达式的值是 null,则不发生赋值,并抛出 NullPointerException。

    * 否则,数组引用子表达式的值实际上引用一个数组。如果索引子表达式的值小于零,或者大于或等于数组的 length,则不发生赋值,并抛出 ArrayIndexOutOfBoundsException。

    * 否则,使用索引子表达式的值来选择由数组引用子表达式的值引用的数组的组件。保存此组件的值,然后计算右侧操作数。如果此计算突然完成,则赋值表达式因相同的理由而突然完成,不发生赋值。

      对于简单赋值操作符,右侧操作数的计算发生在数组引用子表达式和索引子表达式的检查之前,但对于复合赋值操作符,右侧操作数的计算发生在发生在这些检查之后。

    * 否则,考虑前一步中选定的数组组件,其值已保存。此组件是一个变量;称其类型为 S。而且,让 T 是编译时确定的赋值操作符的左侧操作数的类型:

        如果 T 是基元类型,则 S 必须与 T 相同。

        使用保存的数组组件的值和右侧操作数的值来执行由复合赋值操作符指示的二元操作。

        如果此操作突然完成(唯一的可能是整数除以零 - 请参见 15.17.2),则赋值表达式因相同的理由而突然完成,不发生赋值。

        否则,二元操作的结果转换为选定的数组组件的类型,受适当的标准值集的值集转换(5.1.13)的影响(不是扩展指数值集),将转换的结果存储到数组组件中。

    * 如果 T 是引用类型,则它必须是 String。因为类 String 是一个 final 类,S 也必须是 String。

      因此,有时简单复制操作符所需要的运行时检查,对复合赋值操作符来说,是完全不需要的。

      使用保存的数组组件的值和右侧操作数的值来执行由复合赋值操作符(其必须是 +=)指示的二元操作(字符串拼接)。如果此操作突然完成,则赋值表达式因相同的理由而突然完成,不发生赋值。

      否则,将二元操作的 String 结果存储到数组组件中。

avatar

15.27. lambda 表达式

lambda 表达式就像一个方法:它提供一组形式参数和一个根据这些参数表示的 body - 一个表达式或块。

LambdaExpression:
    LambdaParamenters -> LambdaBody

lambda 表达式总是聚合表达式(15.2)。

如果程序中的 lambda 表达式出现在赋值上下文(5.2)、调用上下文(5.3)或强制转换上下文(5.5)以外的地方,则这是编译时错误。

lambda 表达式的计算产生一个函数式接口(9.8)的实例。lambda 表达式计算不会导致表达式 body 的执行;相反,这可能随后在调用适当的函数式接口的方法时发生。

avatar

此语法具有在简单 lambda 表达式周围最小化括号干扰的优点,这在 lambda 表达式是方法参数或 body 是另一个 lambda 表达式时尤其有用。它还清楚地区分了它的表达式和语句形式,避免了歧义或“;”标记的过度依赖。当需要一些额外的括号来直观地区分完整的 lambda 表达式或其 body 表达式任意一个时,很自然地支持括号(就像别的操作符优先级无法区分的情况一样)。

此语法有一些解析挑战。Java 编程语言总是需要任意的提前量来区分 '(' 标记之后的类型和表达式:后跟的内容是强制转换还是括号化表达式。当泛型在类型中重用二元操作符 '<' 和 '>' 时,这回变得更糟。lambda 表达式引入了一种新的可能:跟在 '(' 后面的标记可以描述类型、表达式或 lambda 参数列表。某些标记直接表示参数列表(注解、final);其他情况下,有某些必须解释为参数列表(一行两个名称,一个未嵌套在 '<' 和 '>' 里面的 ',')的模式;有时,无法做出决定,直到在 ')' 后面遇到一个 '->'。思考如何有效地解析这个的最简单方法是使用状态机:每个状态都表示可能解释(类型、表达式或参数)的子集,当机器转移到集合为单例的状态时,解析器知道它是哪种情况。然而,这仍然不能非常完美地映射到固定提前量的语法。

没有特殊的 nullary 形式:具有零个参数的 lambda 表达式表示为 () -> ...。明显的特例语法,-> ...,无法正常工作,因为它在参数列表和强制转换:(x) -> ... 之间引入了歧义。

lambda 表达式无法声明类型参数。虽然在语义上这样做是有意义的,但自然的语法(参数列表前面有类型参数列表)引入了麻烦的歧义。例如,考虑:

avatar

这可能是具有一个参数(一个泛型 lambda 强制转换为类型 x)的 foo 的调用,或者它可能是具有两个参数的 foo 的调用,两个参数是比较的结果,第二个比较 z 与 lambda 表达式。(严格来讲,lambda 表达式作为关系操作符 > 的操作数是毫无意义的,但这是构建语法的一个脆弱假设。)

有一个涉及强制转换的歧义解析的先例,它实质上禁止跟在非基元类型强制转换(15.15)后面的 - 和 + 的使用,但为了将该方法扩展到泛型 lambda 将涉及对语法的侵入性更改。

15.27.1. lambda 参数

lambda 表达式的形式参数可以具有声明类型或推断类型任意一种。无法混合这些形式:lambda 表达式无法声明某些参数的类型,而推断其他剩下的。只有具有声明类型的参数可以有修饰符。

LambdaParameters:
    Identifier
    ( [FormalParameterList] )
    ( InferredFormalParameterList )

InferredFormalParameterList:
    Identifier {, Identifier}

为了方便,以下来自 4.3、8.3 和 8.4.1 的产生式在这显示。

FormalParameterList:
    ReceiverParameter
    FormalParameters , LastFormalParameter
    LastFormalParameter

FormalParameters:
    FormalParameter {, FormalParameter}
    ReceiverParameter {, FormalParameter}

FormalParameter:
    {VariableModifier} UnannType VariableDeclaratorId

LastFormalParameter:
    {VariableModifier} UnannType {Annotation} ... VariableDeclaratorId
    FormalParameter

VariableModifier:
    (one of)
    Annotation final

VariableDeclaratorId:
    Identifier [Dims]

Dims:
    {Annotation} [ ] {{Annotation} [ ]}

在 lambda 表达式的 FormalParameters 中不允许接收者参数,如 8.4.1 中所述。

其形式参数具有声明类型的 lambda 表达式被称为显式类型化的,而其形式参数具有推断类型的 lambda 表达式被称为隐式类型化的。零参数的 lambda 表达式是显式类型化的。

如果形式参数具有推断类型,则这些类型派生(15.27.3)自 lambda 表达式所针对的函数式接口类型。

具有声明类型的形式参数的语法与方法声明(8.4.1)的参数的语法相同。

形式参数的声明类型取决于它是否是可变参数:

    * 如果形式参数不是可变参数,则如果 UnannType 和 VariableDeclaratorId 中没有括号对,声明类型由 UnannType 表示,否则由 10.2 指定。

    * 如果形式参数是可变参数,则声明类型由 10.2 指定。(请注意,可变参数不允许“混合表示”。)

以下 lambda 参数列表之间不进行区分:

avatar

与重写规则一致,可以使用任意一个,不管函数式接口的抽象方法是固定数量还是可变数量。由于从不直接调用 lambda 表达式,因此在函数式接口使用 int[] 的地方引入 int... 对周围的程序没有影响。在 lambda body 中,可变参数就像数组类型的参数一样处理。

在 9.7.4 和 9.7.5 中指定形式参数声明上注解修饰符的规则。

如果 final 作为形式参数声明的修饰符多次出现,则这是编译时错误。

使用混合数组表示(10.2)来表示可变参数,是编译时错误。

在 6.3 和 6.4 中指定形式参数的作用域和遮蔽。

lambda 表达式声明两个具有相同名称的形式参数,是编译时错误。(即,他们的声明提及了相同的 Identifier。)

如果 lambda 参数具有名称 _(即,单下划线字符),则这是编译时错误。

不建议在任何上下文中使用变量名 _。Java 编程语言将来的版本可能会将此名称保留位关键字,并/或赋予它特殊的语义。

如果接收者参数(8.4.1)出现在 lambda 表达式的 FormalParameters 中,则这是编译时错误。

如果在 lambda 表达式的 body 中给声明为 final 的形式参数赋值,则这是编译时错误。

当调用 lambda 表达式时(通过方法调用表达式(15.12)),在 lambda body 执行之前,实参表达式的值初始化新创建的参数变量,每个声明或推断的类型。在 VariableDeclaratorId 或 InferredFormalParameterList 中出现的 Identifier 在 lambda body 中可用作简单名称,以引用形式参数。

float 类型的 lambda 参数总是包含一个浮点值集(4.2.3)元素;类似地,double 类型的 lambda 参数总是包含一个双精度浮点值集的元素。不允许 float 类型的 lambda 参数包含不是浮点值集元素的浮点扩展指数值集元素,也不允许 double 类型的 lambda 表达式包含不是双精度浮点值集元素的双精度扩展指数值集元素。

当推断 lambda 表达式的参数类型时,可以以不同的方式解释 lambda body,这取决于它出现在的上下文。具体地说,body 中表达式的类型、body 抛出的已检查异常和 body 中代码的类型正确性都取决于参数的类型推断。这意味着,参数类型的推断必须出现在试图对 lambda 表达式的 body 进行类型检查之前。

15.27.2. lambda body

lambda body 是单个表达式或一个块(14.2)。像方法 body 一样,lambda body 描述将在调用出现处执行的代码。

LambdaBody:
    Expression
    Block

与匿名类声明中出现的代码不同,名称的含义以及在 lambda body 中出现的 this 和 super 关键字,以及引用声明的可访问性,与周围上下文相同(除 lambda 参数引入新的名称之外)。

lambda 表达式 body 中 this(显式和隐式)的透明性 - 将它视为与周围的上下文相同 - 允许实现更大的灵活性,并防止 body 中未限定的名称的含义依赖于重载解析。

实际上,lambda 表达式需要考虑自身(递归地调用自身或调用它的其他方法)是不常见的,而更常见的是使用名称来引用封闭类中以其他方式遮蔽的事物(this,toString())。如果 lambda 表达式需要引用自身(就像通过 this),则应该使用方法引用或匿名内部类来代替。

块 lambda body 是 void 兼容的,如果块中的每个返回语句具有形式 return;。

块 lambda body 是值兼容的,即使它无法正常完成(14.21),块中的每个返回语句具有形式 return Expression;。

如果块 lambda body 既不是 void 兼容也不是值兼容的,则这是编译时错误。

在值兼容的块 lambda body,结果表达式是任何可以产生调用的值的表达式。特别地,对于 body 包含的每个 return Expression ; 形式的语句,Expression 是一个结果表达式。

avatar

void/值兼容的处理和 body 中名称的含义共同作用于在给定上下文中最小化特定目标类型的依赖性,这对于实现和程序员理解都是有用的。虽然重载解析依赖目标类型期间可以为不同的类型分配表达式,但未限定的名称的含义和 lambda body 的基本结构不会改变。

请注意,void/值兼容定义不是严格的结构属性:“可以正常完成”取决于常量表达式的值,这些可能包括引用常变量的名称。

任何在 lambda 表达式中使用的但未声明的局部变量、形式参数或异常参数必须要么被声明为 final,要么是事实上 final(4.12.4),否则在尝试使用的地方发生编译时错误。

变量使用的类似规则适用于内部类的 body(8.1.3)。事实上 final 变量的限制禁止访问动态更改的局部变量,其捕获可能会引发并发问题。与 final 限制相比,它减少了程序员的教权主义负担。

事实上 final 变量的限制包括标准循环变量,但不包括增强 for 循环变量,对于循环(14.14.2)的每次迭代,它被视为不同的。

avatar avatar avatar

15.27.3. lambda 表达式的类型

lambda 表达式在具有目标类型 T 的赋值上下文、调用上下文或强制转换上下文是兼容的,如果 T 是一个函数式接口类型(9.8),并且表达式与派生自 T 的基础目标类型的函数类型是相等的。

基础目标类型派生自 T,如下所示:

    * 如果 T 是通配符参数化的函数式接口类型,lambda 表达式是显式类型化的,则如 18.5.3 中所述推断基础目标类型。

    * 如果 T 是通配符参数化的函数式接口类型,lambda 表达式是隐式类型化的,则基础目标类型是 T 的非通配符参数化(9.9)。

    * 否则,基础目标类型是 T。

lambda 表达式与函数类型一致,如果以下所有为 true:

    * 函数类型没有类型参数。

    * lambda 参数的数量与函数类型的参数类型的数量相同。

    * 如果 lambda 表达式是显式类型化的,则它的形式参数类型与函数类型的参数类型相同。

    * 如果假定 lambda 参数具有与函数类型的参数类型相同的类型,则:

        如果函数类型的结果是 void,则 lambda body 是语句表达式(14.8)和 void 兼容的块任意一个。

        如果函数类型的结果是(非 void)类型 R,则要么 i) lambda body 是与 R 在赋值上下文中兼容的表达式,要么 ii) lambda body 是值兼容的块,每个结果表达式(15.27.2)与 R 在赋值上下文中是兼容的。

如果 lambda 表达式与目标类型 T 是兼容的,则表达式的类型,U,是派生自 T 的基础目标类型。

如果由 U 或 U 的函数类型提及的任何类或接口从 lambda 表达式出现在的类或接口中是不可访问的,则这是编译时错误。

对于 U 的每个非 static 成员方法 m,如果 U 的函数类型具有 m 签名的子签名,则其方法类型是 U 的函数类型的概念方法被视为重写 m,可能发生 8.4.8.3 中指定的任何编译时错误或未检查警告。

可以在 lambda 表达式的 body 中抛出的已检查异常可以导致编译时错误,如 11.2.3 所述。

显式类型化的 lambdas 的参数类型需要精确匹配函数类型的那些。虽然这有可能更灵活 - 允许装箱和逆变,例如 - 这种一般性似乎是不必要的,并且与在类声明中的重写工作的方式不一致。程序员在编写 lambda 表达式时应该确切知道作为目标的函数类型是什么,因此他应该知道什么签名必须被重写。(与此相反,方法引用的情况并非如此,因此当使用他们时允许更大的灵活性。)此外,参数类型的更大灵活性会增加类型推断和重载解析的复杂性。

请注意,虽然在严格调用上下文中不允许装箱,但总是允许 lambda 结果表达式的装箱 - 即,结果表达式出现在赋值上下文中,而不考虑封闭 lambda 表达式的上下文。但是,如果显式类型化的 lambda 表达式是重载方法的参数,则通过最具体检查(15.12.2.5)优先选择避免装箱或拆箱 lambda 结果的方法签名。

如果 lambda 的 body 是语句表达式(即,允许单独作为语句的表达式),则它兼容产生 void 的函数类型;简单地丢弃任何结果。因此,例如,以下两个都是合法的:

avatar

一般来说,() -> expr 形式的 lambda,其中 expr 是语句表达式,解释为 () -> { return expr; } 或 () -> { expr; },这取决于目标类型。

15.27.4. lambda 表达式的运行时计算

运行时,lambda 表达式的计算类似于类实例创建表达式的计算,在正常完成范围内产生一个对象引用。lambda 表达式的计算不同于 lambda body 的调用。

分配并初始化一个新的具有下面属性的类的实例,或引用一个现存的具有下面属性的类的实例。如果要创建一个新的实例,但没有足够的空间来分配对象,则 lambda 表达式的计算通过抛出 OutOfMemoryError 来突然完成。

lambda 表达式的值是具有以下属性的类的实例的引用:

    * 类实现目标函数式接口类型,以及,如果目标类型是交集类型,交集中提及的每个其他接口类型。

    * 如果 lambda 表达式具有类型 U,则对于 U 的每个非 static 成员方法 m:

      如果 U 的函数类型具有 m 前面的子签名,则类声明一个重写 m 的方法。此方法的 body 具有计算 lambda body,如果它是一个表达式,或执行 lambda body,如果它是一个块,的效果;如果期望结果,则将它从方法返回。

      如果被重写的方法的类型的擦除在签名上与 U 的函数类型的擦除不同,则在计算或执行 lambda body 之前,则方法的 body 检查,每个参数值是 U 的函数类型中相应的参数类型的擦除的子接口的实例;如果不是,则抛出 ClassCastException。

    * 尽管它可以重写 Object 类的方法,但该类不会重写上面提及的目标函数式接口类型或其他接口类型的其他方法。

这些规则旨在为 Java 编程语言的实现提供灵活性,即:

    * 不需要在每次计算时分配新的对象。

    * 由不同 lambda 表达式产生的对象不需要属于不同的类(例如,如果 bodies 相同)。

    * 由计算产生的每个对象不需要属于同一个类(例如,捕获的局部变量可能是内联的)。

    * 如果“现存的实例”是可用的,则不需要在前一个 lambda 计算时创建(例如,可能在封闭类的初始化过程中分配)。

如果目标函数式接口类型是 java.io.Serializable 的子类型,则结果对象将自动是可序列化类的实例。使派生自 lambda 表达式的对象可序列化,可能会产生额外的运行时开销和安全问题,所以“默认”不要求 lambda 派生的对象是可序列化的。

15.28. 常量表达式

ConstantExpression:
    Expression

常量表达式是一个表示未突然完成的基元类型或 String 值的表达式,且仅使用以下组合的:

    * 基元类型字面量和 String 类型字面量(3.10.1,3.10.2,3.10.3,3.10.4,3.10.5)

    * 到基元类型的强制转换和到 String 类型的强制转换(15.16)

    * 一元操作符 +、-、~ 和 !(但不是 ++ 或 --)(15.15.3,15.15.4,15.15.5,15.15.6)

    * 乘法操作符 *、/ 和 %(15.17)

    * 加法操作符 + 和 -(15.18)

    * 移位操作符 <<、>> 和 >>>(15.19)

    * 关系操作符 <、<=、> 和 >=(但不是 instanceof)(15.20)

    * 相等操作符 == 和 !=(15.21)

    * 按位和逻辑操作符 &、^ 和 |(15.22)

    * 条件与操作符 && 和条件或操作符 ||(15.23,15.24)

    * 三元条件操作符 ? :(15.25)

    * 括号化表达式,其包含的表达式是常量表达式。

    * 引用常变量(4.12.4)的简单名称(6.5.6.1)。

    * 引用常变量(4.12.4)的 TypeName . Identifier 形式的限定名称(6.5.6.2)。

String 类型的常量表达式总是“被拘束的”,以便共享唯一的实例,使用方法 String.intern。

常量表达式总是被视为 FP-strict(15.4),即使它出现在非常量表达式不被视为 FP-strict 的上下文中。

常量表达式在 switch 语句(14.11)中用作 case 标签,并且对赋值转换(5.2)和类或接口的初始化具有特殊意义。他们还可以控制 while、do 或 for 语句正常完成(14.21)和使用数值操作数的条件操作符 ? : 的类型。

avatar