Java 编程语言的开发工具应该在源代码可用时,必须支持自动重新编译。特定的实现还可以在版本化数据库中存储类型的源和二进制,并实现使用数据库的完整性机制的 ClassLoader,以防止链接错误,通过向客户端提供二进制兼容的版本。
广泛分发的包和类的开发者面临一组不同的问题。在互联网上,这是我们最喜欢的广泛分布的系统的例子,自动重新编译那些预先存在的直接或间接依赖要更改的类型的二进制文件通常是不现实或不可能的。相反,此规范定义了一组更改,允许开发者对包或类或接口类型进行更改,同时保留(不破坏)与预先存在的二进制文件的兼容性。
在 SOM(Forman, Conner, Danforth, and Raper, Proceedings of OOPSLA '95)的 Release-to-Release 二进制兼容性框架中,Java 编程语言二进制在作者识别的所有相关转换下都是二进制兼容的(对于添加实例变量有一些警告)。使用他们的方案,这列出了 Java 编程语言支持的一些重要的二进制兼容性更改:
* 重新实现现存的方法、构造器和初始化器,以提升性能。
* 改变方法或构造器在以前抛出异常的输入上的返回值,这些异常通常不应发生,或因进入无限循环或导致死锁而失败。
* 向现存的类或接口添加新的字段、方法或构造器。
* 删除类的 private 字段、方法或构造器。
* 当更新整个包时,删除包中类和接口的包访问字段、方法或构造器。
* 重新排序现有的类型声明中的字段、方法或构造器。
* 在类层次结构中向上移动方法。
* 重新排序类或接口的直接父接口列表。
* 在类型层次结构中插入新的类或接口类型。
本章指定了由所有实现保证的二进制兼容性的最小标准。当混合不知道是来自兼容的源的类和接口的二进制,但其源已按照此处描述的兼容方式进行了修改时,Java 编程语言可以保证兼容性。请注意,我们正在讨论的是发行版应用程序之间的兼容性。发行版 Java SE 平台的兼容性讨论已经超出了本章的范围。
我们鼓励开发者系统提供一些工具,提醒开发者注意更改无法重新编译的预先存在的二进制文件的影响。
此章首先指出了 Java 编程语言的任何二进制格式必须具有的某些属性(13.1)。接下来它定义了二进制兼容性,解释了什么是二进制兼容性和什么不是(13.2)。最后,它列举了一大组可能的包(13.3)、类(13.4)和接口(13.5)的更改,指出了这些更改中的哪些会被保证保留二进制兼容性,哪些不会被保证。
有时,引用形式:(JVMS §x.y)用于表示来自 Java SE 8 版的 Java 虚拟机规范的概念。
程序必须被编译成由 Java SE 8 版的 Java 虚拟机规范指定的 class 文件格式,或由 Java 编程语言编写的类加载器映射成该格式的表示。
生成的 class 文件必须具有一些特定的属性。这些属性中有许多专门用于支持保留二进制兼容性的源代码转换。所需的属性有:
-
类或接口必须以它的的二进制名称命名,其必须满足一下约束:
顶层类型(7.6)的二进制名称是它的规范名称(6.7).
成员类型(8.5,9.5)的二进制名称由它的直接封闭类型的二进制名称,后跟 $,后跟此成员的简单名称,组成。
局部类(14.3)的二进制名称由它的直接封闭类型的二进制名称,后跟 $,后跟一个非空数字序列,后跟此局部类的简单名称,组成。
匿名类(15.9.5)的二进制名称由它的直接封闭类型的二进制名称,后跟 $,后跟一个非空数字序列,组成。
泛型类或接口(8.1.2,9.1.2)声明的类型变量的二进制名称是它的直接封闭类型的二进制名称,后跟 $,后跟类型变量的简单名称。
泛型方法(8.4.4)声明的类型变量的二进制名称是声明该方法的类型的二进制名称,后跟
$,后跟方法描述符(JVMS §4.3.3),后跟 $ ,后跟类型变量的简单名称。泛型构造器(8.8.4)声明的类型变量的二进制名称是声明该构造器的类型的二进制名称,后跟
$,后跟构造器描述符(JVMS §4.3.3),后跟 $ ,后跟类型变量的简单名称。 -
另一个类或接口类型的引用必须是符号性的,使用该类型的二进制名称。
-
常变量(4.12.4)字段的引用必须在编译期被解析为由该常变量的初始化器表示的值 V。
如果这样一个字段是 static,则该字段的引用不应出现在二进制文件的代码中,包括声明该字段的类或接口。这样一个字段必须总是看起来是已经初始化的(12.4.2);该字段的默认初始值(假如与 V 不同)必须不能被观察到。
如果这样一个字段是非 static,则该字段的引用不应出现在二进制文件的代码中,除非是包含该字段的类。(它将是一个类而不是接口,因为接口仅含有 static 字段。)实力创建期间(12.5),此类应具有将此字段的值设置为 V 的代码。
-
给定一个合法的在类 C 中表示字段访问的表达式,其引用一个名为 f 不是常变量的字段,且是声明在一个(可能不同的)类或接口 D 中,我们按如下所示定义该字段引用的限定类型:
如果表达式引用了一个简单名称,则 f 是当前类或接口 C 的一个成员,然后让 T 是 C;否则,让 T 是 f 是其成员的最内部的词法封闭类型声明。无论哪种情况,T 都是引用的限定类型。
如果该引用是 TypeName.f 形式,其中 TypeName 表示一个类或接口,则 TypeName 表示的类或接口是该引用的限定类型。
如果表达式是 ExpressionName.f 或 Primary.f 形式,则:
如果 ExpressionName 或 Primary 的编译时类型是交集类型 V1 & ... & Vn(4.9),则该引用的限定类型是 V1。 否则,ExpressionName 或 Primary 的编译时类型是该引用的限定类型。
如果表达式是 super.f 形式,则 C 的父类是该引用的限定类型。
如果表达式是 TypeName.super.f 形式,则由 TypeName 表示的类的父类是该引用的限定类型。
对 f 的引用必须编译为引用的限定类型的擦除(4.6)的符号引用,再加上字段的简单名称 f。引用还必须包含对字段的声明类型的擦除的符号引用,这样验证器可以检查该类型是否如预期的那样。
-
给定类或接口 C 中的一个方法调用表达式或一个方法引用表达式,引用一个在类或接口 D 中声明(或隐式声明的(9.2))的名为 m 的方法,我们按如下定义方法调用的限定类型:
如果 D 是 Object,则表达式的限定类型是 Object。
否则:
如果方法引用一个简单名称,则如果 m 是当前类或接口 C 的一个成员,让 T 是 C;否则,让 T 是 m 是其成员的最内部的词法封闭类型声明。无论哪种情况,T 是该方法调用的限定类型。 如果表达式是 TypeName.m 或 ReferenceType::m 形式,则由 TypeName 或 ReferenceType 表示的类型是该方法调用的限定类型。 如果表达式是 ExpressionName.m 或 Primary.m 或 ExpressionName::m 或 Primary::m 形式,则: 如果 ExpressionName 或 Primary 的编译时类型是交集类型 V1 & ... & Vn(4.9),则该方法调用的限定类型是 V1。 否则,ExpressionName 或 Primay 的编译时类型是该方法调用的限定类型。 如果表达式是 super.m 或 super::m 形式,则 C 的父类是该方法调用的限定类型。 如果表达式是 TypeName.super.m 或 TypeName.super::m 形式,则如果 TypeName 表示类 X,X 的父类是该方法调用的限定类型;如果 TypeName 表示接口 X,X 是该方法调用的限定类型。
对方法的引用必须在编译期解析为该调用的限定类型的擦除(4.6)的符号引用,再加上方法签名(8.4.2)的擦除。方法签名必须包括所有以下由 15.12.3 确定的:
方法的简单名称
方法参数的数量
每个参数的类型的符号引用
对方法的引用还必须包括表示的方法的返回类型的擦除的符号引用,或表示的方法被声明为 void 且不返回值的指示。
-
给定一个类实例创建表达式(15.9),或一个显式的构造器调用语句(8.8.7.1),或一个在类或接口 C 中引用(可能不同)类或接口 D 中声明的构造器 m 的 ClassType::new(15.13)形式的方法引用表达式,我们按如下定义该构造器调用的限定类型:
如果表达式是 new D(...) 或 ExpressionName.new D(...) 或 Primary.new D(...) 或 D::new 形式,则该调用的限定类型是 D。
如果表达式是 new D(...) {...} 或 ExpressionName.new D(...) {...} 或 Primary.new D(...) {...} 形式,则表达式的限定类型是该表达式的编译时类型。
如果表达式是 super(...) 或 ExpressionName.super(...) 或 Primary.super(...) 形式,则该表达式的限定类型是 C 的直接父类。
如果表达式是 this(...) 形式,则该表达式的限定类型是 C。
对构造器的引用必须在编译时解析为该调用的限定类型的擦除(4.6)的符号引用,再加上构造器的签名(8.8.2)。构造器的签名必须包括:
构造器参数的数量
每个形式参数的类型的符号引用
类或接口的二进制表示还必须包含以下所有:
-
如果它是一个类且不是 Object,则此类的直接父类的擦除的符号引用。
-
如果有,对每个直接父接口的擦除的符号引用。
-
类或接口中声明的每个字段的说明,给定为字段的简单名称和字段的类型的擦除的符号引用。
-
如果它是一个类,则每个构造器擦除后的签名,如上所述。
-
对于类或接口中声明的每个方法(对于接口,排除它的隐式声明的方法(9.2)),它的擦除后的签名和返回类型,如上所述。
-
实现类或接口所需要的代码:
对于接口,字段初始化器和每个 default 方法实现的代码。 对于类,字段初始化器、实例和 static 初始化器,以及每个方法或构造器实现的代码。
-
每个类型必须包含足够的信息以恢复其规范名称(6.7).
-
每个成员类型必须具有足够的信息以恢复其源代码级别访问修饰符。
-
每个嵌套类和嵌套接口必须具有对其直接封闭类的符号引用(8.1.3)。
-
每个类必须包含其所有成员类型(8.5),和在其方法、构造器、static 初始化器、实例初始化器和字段初始化器中出现的所有局部类和匿名类的符号引用。
每个接口必须包含其所有成员类型(9.5),和在其 default 方法和字段初始化器中出现的局部类和匿名类的符号引用。
-
由 Java 编译器发出的构造必须被标记为合成的(synthetic),如果它不对应在源代码中显式或隐式声明的构造,除非发出的构造是类初始化方法(JVMS §2.9)。
-
由 Java 编译器发出的构造必须被标记为授权的(mandated),如果它对应在源代码(8.8.1,8.8.9,8.9.3,15.9.5.1)中隐式声明的形式参数。
以下形式参数被隐式声明在源代码中:
非 private 内部成员类(8.8.1,8.8.9)的构造器的第一个形式参数。
父类是内部或局部(不是在 static 上下文中)(15.9.5.1)的匿名类的匿名构造器的第一个形式参数。
valueOf 方法的形式参数 name,其被隐式声明在枚举类型(8.9.3)中.
对于引用,以下构造被隐式声明在源代码中,但未被标记为授权的(mandated),因为只有形式参数可以在 class 文件(JVMS §4.7.22)中被这样标记:
类和枚举类型(8.8.9,8.9.2)的默认构造器
匿名构造器(15.9.5.1)
枚举类型(8.9.3)的 values 和 valueOf 方法
枚举类型(8.9.3)的特定 public 字段
接口(9.2)的特定 public 方法
容器注解(9.7.5)
以下部分讨论了类和接口类型声明的更改,而不打破与现存二进制的兼容性。在以上给出的转换需求下,Java 虚拟机和它的 class 文件格式支持这些更改。任何其它合法的二进制格式,例如可被在以上需求下的类加载器映射回 class 文件的压缩或加密表示,也必须支持这些更改。
类型的更改与预先存在的二进制是二进制兼容的(等价地,不打破与其的二进制兼容性),如果预先存在的先前链接没有错误的二进制仍然正确链接而没有错误。
二进制的编译依赖于其他类和接口的可访问成员和构造器。为了保留二进制兼容性,类或接口应该将它的可访问成员和构造器,它们的存在和行为,视为与他的用户的契约。
Java 编程语言旨在防止契约的添加和偶然的名称冲突破坏二进制兼容性。具体地说,重载特定方法名称的更多方法的添加不会打破与预先存在的二进制的兼容性。编译时(15.12.2),预先存在的二进制用于方法查找的方法签名是使用重载解析算法选择的。
如果 Java 编程语言被设计成,在运行时选择被执行的特定方法,则只能在运行时检测到这样的歧义。这样的规则将意味着,添加额外的重载方法以便在调用点上使歧义成为可能,可能会打破与未知数量的预先存在的二进制的兼容性。有关更多讨论,请参见 13.4.23。
二进制兼容性与源代码兼容性是不同的。特别是,13.4.6 中的示例显示,一组兼容的二进制可以从不会一起编译的源中生成。此示例是典型的:添加一个新的声明,在源代码的未更改的部分中更改了名称的含义,而预先存在的源代码的未更改部分的二进制保留了该名称的完全限定的前一个含义。生成一组一致的源代码需要提供与前一含义相对应的限定名称或字段访问表达式。
一个新的顶层类或接口可以添加到包中,而不打破与预先存在的二进制的兼容性,前提是新类型不重用以前给不相关类型的名称。
如果一个新类型重用了以前给一个不相关类型的名称,则可能产生冲突,因为两者类型的二进制不能被同一类加载器加载。
非 public 和不是父类或父接口的顶层类和接口类型的 public 类型更改只影响在其中声明他们的包中的类型。这种类型可能被删除或更改,即使此处该描述了不兼容,前提是该包的受影响的二进制被一起更新。
这一节描述了类和其成员和构造器的更改对预先存在的二进制的影响。
如果一个未被声明为 abstract 的类被更改为 abstract,则预先存在的试图创建该类实例的二进制将会在链接时抛出 InstantiationError,或(如果使用了反射方法)在运行时抛出 InstantiationException,因此,不建议对已广泛分发的类进行这样的更改。
将被声明为 abstract 的类更改为不再被声明为 abstract,不会打破与预先存在的二进制的兼容性。
如果将未被声明为 final 的类更改为 final,则如果预先存在的此类的子类的二进制被加载,将会抛出 VerifyError,因为 final 类不能有子类;不建议对已广泛分发的类进行这样的更改。
将被声明为 final 的类更改为不再被声明为 final,不会打破与预先存在的二进制的兼容性。
将非 public 类更改为 public,不会打破与预先存在的二进制de兼容性。
如果一个 public 类被更改为非 public,则如果预先存在的二进制被需要的链接了,但不再具有对该类类型的访问,会抛出 IllegalArgumentError;对于广泛分发的类,建议不要进行这种更改。
#### 13.4.4. 父类和父接口
加载时会抛出 ClassCircularityError,如果一个类是其本身的父类。对于广泛分发的类,不建议进行可能导致这种循环的类层次结构的更改,当新编译的二进制与预先存在的二进制一起加载时。
更改类类型的直接父类或直接父接口集合不会打破与预先存在的二进制的兼容性,前提是类类型的父类或父接口的总集不丢失任何成员。
如果直接父类或直接父接口的集合的更改导致任何类或接口不再是父类或父接口,则如果预先存在的二进制与被修改的类的二进制一起加载时,可能会导致链接错误。对于广泛分发的类,不建议进行这种更改。
添加或移除类的类型参数本身不会对二进制兼容性产生任何影响。
如果这样的类型参数用在字段或方法的类型中,则可能具有更改上述类型的正常含义。
重命名类的类型参数对于预先存在的二进制没有任何影响。
更改类的类型参数的第一个边界可能会更改在其自己的类型中使用该类型参数的任何成员的擦除(4.6),这可能会影响二进制兼容性。此类边界的更改类似于方法或构造器(13.4.13)的类型参数的第一个边界的更改。
更改任何其他边界对二进制兼容性没有影响。
通过添加具有相同的名称和访问性(对于字段),或相同名称和访问性和签名和返回类型(对于方法)的实例(分别为 static)作为父类或子类的实例(分别为 static)成员,不会导致与预先存在的二进制的不兼容。即使一组被链接的类遇到编译时错误,也不会发生错误。
删除非 private 的类成员或构造器可能会导致链接错误,如果成员或构造器已被预先存在的二进制使用。
super 关键字可用于在父类中声明的方法,越过在当前类中声明的任何方法。编译时,表达式 super.Identifier 被解析成父类 S 中的方法 m。如果方法 m 是实例方法,则运行时被调用的方法是具有与 m 相同签名的方法,其是包含涉及 super 的表达式的类的直接父类的成员。
更改成员或构造器声明的访问以允许更小范围的访问,可能会打破与预先存在的二进制的兼容性,当解析这些二进制时导致链接错误被抛出。如果将访问修饰符从包访问更改为 private 访问、从 protected 访问到包或 private 访问,或从 public 访问到 protected、包或 private 访问,则允许更小范围的访问。因此,对于广泛分发的类,不建议将其成员或构造器更改为允许更小的访问权限。
可能令人惊讶的是,二进制格式被定义成这样,当子类(已经)定义了一个具有更小访问权限的方法时,将成员或构造器改为更易于访问不会导致链接错误。
允许父类的 protected 方法被更改为 public,而不破坏预先存在的子类的二进制,这有助于使二进制变得不那么脆弱。另一种选择,在这种更改会导致链接错误的情况下,会产生额外的二进制兼容性问题。
广泛分发的程序不应该向他们的客户端暴露任何字段。除了上面讨论的二进制兼容性问题以外,这通常是良好的软件工程实践。向类中添加字段会打破与预先存在的未重新编译的二进制的兼容性。
假设一个对具有限定类型 T 的字段 f 的引用。进一步假设,f 实际上是 T 的父类,S,的实例(分别对应 static)字段,f 的类型是 X。
如果与 f 具有相同名称的类型 X 的新字段被添加到 S 的子类中,其是 T 的父类或是 T 本身,则可能会发生链接错误。除了以上介绍的以外,仅当以下之一为 true 时,会发生这样的链接错误:
* 新字段比旧的具有更小的可访问性。
* 新字段是一个 static(分别对应实例字段)字段。
特别是,由于字段访问以前引用了父类的具有不兼容类型的字段,在类不会被重新编译的情况下,不会发生链接错误。以前编译的具有这种引用的类将继续引用在父类中声明的字段。
从类中删除字段将打破与任何预先存在的引用该字段的二进制的兼容性,当这种来自预先存在的二进制的引用被链接时,将抛出 NoSuchFieldError。只有 private 字段可以从广泛分发的类中删除。
为了保证二进制兼容性,添加或删除字段 f,其类型涉及类型变量(4.4)或参数化类型(4.5),等价于具有相同名称的其类型是 f 的类型的擦除(4.6)的字段的添加(分别对应删除)。
如果一个字段由非 final 更改为 final,则这回打破与预先存在的试图向该字段赋新值的二进制的兼容性。
删除关键字 final 或将值更改为字段被初始化的值,不会打破与现存的二进制的兼容性。
如果一个字段是常变量(4.12.4),同时是 static,则删除关键字 final 或更改它的值不会通过导致他们无法运行来打破与预先存在的二进制的兼容性,但他们将无法看到该字段使用的新值,除非重新编译他们。这是决定支持条件编译(14.21)所带来的副作用。(可以假设,如果使用发生在常量表达式中,不会看到新值,但在其他情况下会看到。未必总是这样;预先存在的二进制总是看不到新值。)
要求内联 static 常变量值的另一个原因是 switch 语句。他们是唯一一种依赖常量表达式的语句,即 switch 语句的每个 case 标签必须是一个其值不同于每个其他 case 标签的常量表达式。case 标签通常引用 static 常变量,因此可能不会立即发现所有标签都有不同的值。如果证实编译时没有重复的标签,则将这些值内联到 class 文件中可以确保运行时也没有重复的标签 - 这是一个非常可取的属性。
在广泛分发的代码中避免“易变的常量”问题的最好方式是使用仅将 static 常变量用于真正不可能发生更改的值。除了对于真正的数学常量,我们建议在源代码中尽量少使用 static 常变量。
一般来说,我们建议只将常量表达式分配给接口字段。
我们提醒,而不是建议,如果接口基元类型的字段可能更改,它的值可以惯用地表达为:
确保该值不是常量。其他基元类型也存在类似的习语。
另一个要注意的是,static 常变量决不能显示为其类型(4.12.5)的默认初始值。这意味着在类初始化过程中,所有这些字段首先被初始化。
如果非 private 字段未被声明为 static,然后被更改为 static,反之亦然,则如果预先存在的期望其他种类的字段的二进制使用该字段,链接错误,特别是 IncompatibleClassChangeError,会发生。不建议在已广泛分发的代码中进行这种更改。
添加或删除字段的 transient 修饰符不会打破与预先存在的二进制的兼容性。
向类中添加方法或构造器声明不会打破与预先存在的二进制的兼容性,即使在无法重新编译类型的情况下,因为调用以前引用了具有不兼容类型的父类的方法或构造器。具有此类引用的以前编译的类将继续引用在父类中声明的方法或构造器。
假设一个具有限定类型 T 的方法 m 的引用。进一步假设,m 实际上是在 T 的父类,S,中声明的实例(分贝对应 static)方法。
如果类型 X 的具有相同与 m 相同的签名和返回类型的新方法被添加到 S 的子类中,其是 T 的父类或是 T 本身,则可能会发生链接错误。除上述情况外,仅当一下任一项为 true 时,才会发生此类链接错误:
* 新方法的可访问性低于旧的。
* 新方法是 static(分别对应实例方法)方法。
从类中删除方法或构造器可能会打破与任何预先存在的引用该方法或构造器的二进制的兼容性,当链接此类来自预先存在的二进制的引用时,可能会抛出 NoSuchMethodError。仅当父类中没有具有相匹配的签名和返回类型的方法时,才会发生此类错误。
如果非内部类的源代码不包含任何显式声明的构造器,则隐式地声明一个默认的无参构造器(8.8.9)。向这种类的源代码中添加一个或多个构造器声明将阻止此默认构造器被隐式地声明,有效地删除一个构造器,除非其中一个新构造器也没有参数,从而替换默认的构造器。默认的无参构造器与声明其的类具有相同的访问修饰符,因此,如果想要保留与预先存在的二进制的兼容性,任何代替都应具有尽可能相同或更多的访问权限。
添加或删除方法或构造器的类型参数本身不会对二进制兼容性产生任何影响。
如果在方法或构造器的类型中使用这样的类型参数,则可能具有更改上述类型的正常含义。
重命名方法或构造器的类型参数对于预先存在的二进制没有任何影响。
更改方法或构造器的类型参数的第一个边界可能会改变任何在其自己类型中使用该类型参数的成员的擦除(4.6),这还可能影响二进制兼容性。特别是:
* 如果类型参数用作字段类型,效果就像是该字段被删除,一个具有相同名称的其类型是新的类型变量的擦除的字段被添加。
* 如果类型参数用作方法的任何形式参数的类型,但不用作返回类型,效果就像是该方法被删除,用一个新的除上述形式参数的类型之外完全相同的方法代替,其现在将类型参数的新擦除作为他们的类型。
* 如果类型参数用作方法的返回类型,但不用作方法的任何形式参数的类型,效果就像是方法被删除,用一个新的除返回类型之外完全相同的方法代替,其现在是类型参数的新擦除。
* 如果类型参数同时用作方法的返回类型和方法的一个或多个形式参数的类型,效果就像是方法被删除,用一个新的除返回类型,其现在是类型参数的新擦除,和上述形式参数的类型,其现在将类型参数的新擦除作为他们的类型,之外完全相同的方法代替。
更改其他任何边界对二进制兼容性都没有影响。
更改方法或构造器的形式参数的名称对预先存在的二进制没有任何影响。
更改方法名称,或方法或构造器的形式参数的类型,或向方法或构造器的声明添加或删除参数,将创建具有新签名的方法或构造器,并具有删除带有旧签名的方法或构造器和添加带有新签名的方法或构造器的效果(13.4.12)。
将方法最后一个形式参数的类型从 T[] 更改为类型 T 可变参数(即,T...),不会对预先存在的二进制产生影响,反之亦然。
为了保持二进制兼容性,添加或删除签名涉及类型变量(4.4)或参数化类型(4.5)的方法或构造器 m 等价于另外的相等的签名是 m 的签名的擦除(4.6)的方法的添加(分别对应删除)。
更改方法的结果类型,或用 void 替换结果类型,或用结果类型替换 void,具有删除旧方法并添加具有新结果类型或新 void 结果(参见 13.4.12)的新方法的组合效果。
为了保持二进制兼容性,添加或删除其返回类型涉及类型变量(4.4)或参数化类型(4.5)的方法或构造器 m 等价于另外相等的其返回类型是 m 的返回类型的擦除(4.5)方法的添加(分别对应删除)。
将 abstract 方法更改为非 abstract,不会打破与预先存在的二进制的兼容性。
将非 abstract 方法更改为 abstract 将打破与预先存在的以前调用该方法的二进制的兼容性,导致 AbstractMethodError。
将 final 方法更改为非 final,不会打破与预先存在的二进制的兼容性。
将非 final 实例方法更改为 final,可能会打破与预先存在的依赖重写该方法的功能的二进制的兼容性。
将非 final 的类(static)方法更改为 final,不会打破与现存的二进制的兼容性,因为该方法无法被重写。
添加或删除方法的 native 修饰符,不会打破与预先存在的二进制的兼容性。
在预先存在的 native 方法上对未重新编译的类型的更改的影响超出了本规范的范围,应提供实现说明。鼓励实现,但不是必需的,以限制此类影响的方式实现 native 方法。
如果非 private 方法是 static(即,类方法),并被更改为非 static(即,实例方法),反之亦然,则可能打破与预先存在的二进制的兼容性,导致链接时错误,即 IncompatibleClassChangeError,如果预先存在的二进制使用了这些方法。不建议在已广泛分发的代码中进行此类更改。
添加或删除方法的 synchronized 修饰符,不会打破与预先存在的二进制的兼容性。
更改方法或构造器的 throws 子句,不会打破与预先存在的二进制的兼容性;仅在编译时检查这些子句。
更改方法或构造器的 body,不会打破与预先存在的二进制的兼容性。
方法上的关键字 final 并不意味着,该方法可以被安全地内联;它仅意味着,方法无法被重写。链接时仍然可以提供该方法的新版本。此外,原始的程序的结构必须为了反射而保留。
因此,我们提醒,Java 编译器无法在编译时以内联方式展开方法。通常,我们建议,实现使用后期绑定(运行时)的代码生成和优化。
添加新的重载现存方法或构造器的方法或构造器,不会打破与预先存在的二进制的兼容性。每个调用使用的签名在编译这些现存的二进制时确定;因此,新添加的方法或构造器也不会被使用,即使他们的签名既适用又比最初选择的签名更具体。
虽然在下次编译类或接口时,添加新重载的方法或构造器可能会导致编译时错误,因为没有最具体(15.12.2.5)的方法或构造器,当执行程序时,不会发生此类错误,因为执行时不进行重载解析。
如果向子类添加了一个实例方法,它重写了父类中的方法,则在预先存在的二进制中的方法调用将发现子类的方法,这些二进制不受影响。
如果向类中添加了一个类方法,则不会发现此方法,除非引用的限定类型是子类类型。
添加、删除或更改类的 static 初始化器(8.7),不会影响预先存在的二进制。
添加或重排序枚举中的常量不会打破与预先存在的二进制的兼容性。
如果预先存在的二进制试图访问不再存在的枚举常量,客户端在运行时抛出 NoSuchFieldError。因此,对于广泛分发的枚举,不建议进行此类更改。
在所有其他方面,枚举的二进制兼容性规则与类相同。
本节描述了接口和其成员声明的更改对预先存在的二进制的影响。
将非 public 接口更改为 public,不会打破与预先存在的二进制的兼容性。
如果将 public 接口更改为非 public,则如果预先存在的二进制链接需要但不在具有对该接口类型的访问,抛出 IllegalAccessError,因此对于广泛分发的接口,不建议进行此类更改。
接口层次结构的更改将会以与类层次结构更改相同的方式导致错误,如 13.4.4 所述。特别是,导致类的任何前面的父接口不再是父接口的更改,可以打破与预先存在的二进制的兼容性,导致 VerifyError。
向接口添加 abstract 方法,不会打破与预先存在的二进制的兼容性。
添加到 C 的父接口的字段可能会隐藏从 C 的父类中继承来的字段。如果原始引用是对实例字段,则会产生 IncompatibleClassChangeError。如果原始引用是赋值,则会产生 IllegalAccessError。
从接口中删除成员可能导致预先存在的二进制中出现链接错误。
更改接口类型参数的效果与类的类型参数的那些相似更改相同。
更改接口中字段声明的注意事项与类中那些 static final 字段的相同,如 13.4.8 和13.4.9 所述。
更改接口中 abstract 方法声明de注意事项包含类中 abstract 方法的那些,如 13.4.14、13.4.15、13.4.19、13.4.21 和 13.4.23 所述。
添加 default 方法,或将方法从 abstract 更改为 default,不会打破与预先存在的二进制的兼容性,但如果预先存在的二进制试图调用该方法,可能会导致 IncompatibleClassChangeError。如果限定类型,T,是两个接口,I 和 J,的子类型,其中 I 和 J 都声明了具有相同签名和结果的方法,同时 I 和 J 互相不是子接口,会发生此错误。
换言之,添加 default 方法是二进制兼容的更改,因为它不会再链接时引入错误,即使它在编译时或调用时引入了错误。实际上,引入 default 方法时发生意外冲突的风险与向非 final 类中添加新方法的相似。在发生冲突时,向类中添加方法不太可能触发 LinkageError,但对子方法的意外重写可能导致无法预测的方法行为。这两种更改都可能在编译时导致错误。
注解类型的行为与任何其他接口完全一样。添加或删除注解类型中的元素类似于添加或删除方法。对于注解类型的其他更改,例如使注解类型可重复(9.6.3),这些都有重要的考虑因素,但他们对由 Java 虚拟进执行的二进制的链接没有影响。相反,此类更改会影响操作注解的反射 APIs 的行为。当对基础注解类型进行各种更改时,这些 APIs 的文档指定了他们的行为。