Java 编程语言是一种静态类型语言,这意味着每一个变量和每一个表达式在编译时都具有一个已知的类型。
Java 编程语言也是一种强类型语言,因为类型限制变量(4.12)可以持有的或者表达式可以产生的值,限制在那些值上支持的操作,并确定操作的含义。强静态类型有助于在编译时检测错误。
Java 编程语言中的类型分为两类:原始类型和引用类型。原始类型(4.2)是布尔类型和数值类型。数值类型是整形 byte、short、int、long 和 char,以及浮点类型 float 和 double。引用类型(4.3)是 class 类型、interface 类型和 array 类型。也有一种特殊的 null 类型。对象(4.3.1)是动态创建的 class 类型的实例或动态创建的 array。引用类型的值引用对象。所有对象,包括 arrays,支持 class Object(4.3.2)的方法。字符串字面量由 String 对象(4.3.3)表示。
Java 编程语言中有两种类型:原始类型(4.2)和引用类型(4.3)。相应地,有两种数据值,其可以存储在变量中,作为参数传递,通过方法返回,并可以操作:原始类型(4.2)和引用类型(4.3)。
Type:
PrimitiveType
ReferenceType
也有一种特殊的 null 类型,表达式 null(3.10.7,15.8.1)的类型,其没有名字。
由于 null 类型没有名字,不可能声明一个 null 类型的变量,或者强制转换为 null 类型。
null 引用仅可能是 null 类型表达式的值。
null 引用总是可以赋值或强制转换为任何引用类型(5.2,5.3,5.5)。
实际上,程序员可以忽略 null 类型,并仅认为 null 仅仅是一种可以是任何引用类型的特殊字面量。
原始类型由 Java 编程语言预定义,并由保留关键字命名(3.9):
PrimitiveType:
{Annotation} NumericType
{Annotation} boolean
NumericType:
IntegralType
FloatingPointType
IntegralType:
(one of)
byte short int long char
FloatingPointType:
(one of)
float double
原始类型值不与其它原始类型值共享状态。
数值类型是整形和浮点类型。
整数类型是 byte、short、int 和 long,其值分别为8位、16位、32位和64位有符号的二进制补码整数,以及 char,其值为表示 UTF-16代码单元(3.1)的16位无符号整数。
浮点类型是 float,其值包括 32位 IEEE 754浮点数字,以及 double,其值包括64位 IEEE 754浮点数字。
boolean 类型只有两个值:true 和 false。
整形值是以下范围内的整数:
对于 byte,从-128到127,包含
对于 short,从-32768到32767,包含
对于 int,从-2147483648到2147483647,包含
对于 long,从-9223372036854775808到9223372036854775807,包含
对于 char,从 '\u0000' 到 '\uffff' 包含,即,从0到65535
Java 编程语言提供了一些在整形值上执行的运算符:
比较运算符,其产生一个 boolean 类型的值:
数字比较运算符 <、<=、> 和 >=(15.20.1)
数字等价性运算符 == 和 !=(15.21.1)
数值运算符,其产生一个 int 或 long 类型的值:
一元加和减运算符 + 和 -(15.15.3,15.15.4)
乘法算子 *、/ 和 %(15.17)
加法算子 + 和 -(15.18)
增量运算符 ++,包括前缀(15.15.1)和后缀(15.14.2)
减量运算符 --,包括前缀(15.15.2)和后缀(15.14.3)
有符号和无符号位移运算符 <<、>> 和 >>>(15.19)
按位补运算符 ~(15.15.5)
整数位运算符 &、^ 和 |(15.22.1)
条件运算符 ? :(15.25)
强制转换运算符(15.16),其可以将一个整数值转换为任何指定的数字类型值
字符串串联运算符 +(15.18.1),当给定一个 String 操作数和一个整形操作数时,其会将整形操作数转换为一个表示其十进制形式值的 String,然后产生
一个新创建的 String,其是两个字符串的串联结果
其它有用的构造器、方法和常量预定义在类 Byte、Short、Integer、Long 和 Character 中。
如果一个除了位移运算符以外的整数运算符具有至少一个 long 类型的操作数,则使用64位精度执行操作,并且数字运算符的结果是 long 类型的。如果其它操作数不是 long,则首先通过数字提升(5.6)将其加宽(5.15)为 long 类型。
否则,使用 32 位精度执行操作,并且数字运算符的结果是 int 类型的。如果操作数都不是 int,则首先通过数字提升将其加宽为 int 类型。
任何整形的任何值可以强制转换为任何数字类型或从任何数字类型转换来。整形和布尔类型之间无法强制转换。
有关将整数表达式转换为布尔值的成语,请参见4.2.5。
整数运算符不以任何方式指示上溢和下溢。
整数运算符可以由于以下原因而抛出异常(11(Exceptions)):
任何整数运算符可以抛出一个 NullPointerException,如果需要 null 引用的拆箱转换(5.1.8)的话。
整数除运算符 /(15.17.2)和整数取余运算符 %(15.17.3)可以抛出一个ArithmeticException,如果右侧操作数是零。
增量和减量运算符 ++(15.14.2,15.15.1)和 --(15.14.3,15.15.2)可以抛出一个 OutOfMemoryError,如果需要装箱转换(5.1.7),并且没有足够可用的内存来执行此转换。
浮点类型是 float 和 double,它们在概念上与单精度32位和双精度64位格式的 IEEE 754的值和 IEEE Standard for Binary Floating-Point Arithmetic, ANSI/IEEE Standard 754-1985 (IEEE, New York)中指定的操作相关联。
IEEE 754标准不仅包括有符号和大小组成的正负数,而且还包含正负零、正负无穷和特殊的非数字值(以下简称 NaN)。NaN 值用于表示某些非法操作,如零除以零,的结果。float 和 double 的 NaN 常量预定义为 Float.NaN 和 Double.NaN。
每个 Java 编程语言的实现需要支持两个标准的浮点值集合,叫做 float 值集合和 double 值集合。此外,Java 编程语言的实现可以支持两个扩展指数浮点值集或其中一个,叫做浮点扩展指数值集和双扩展指数值集。在某些情况下,这些扩展指数值集可以代替标准值集来表示 float 和 double(5.1.13,15.4)类型的表达式值。
任何浮点值集的有穷非零值都可以用形式 s ⋅ m ⋅ (2 的 (e - N + 1)) 次方来表示,其中 s 是 +1 或者 -1,m 是小于 2 的 N 次方的正整数,e 是一个介于 Emin = -(2 的 (K-1) 次方 -2) 和 Emax = 2 的 (K-1) 次方 -1 之间的整数,包括,并且 N 和 K 是依赖于值集的参数。有些值可以用这种形式以多种方式表示;例如,假设值集中的值 v 可以以这种形式用特定的值 s、m 和 e 表示,然后如果发生了 m 是偶数,而 e 小于 2 的 (K-1) 次方,则可以使用相同的 m 和增加 1 后的 e 来为相同的值 v 产生第二种表示。如果 m ≥ 2 的 (N-1) 次方,这种形式的表示称为标准化;否则,该表示被称为非标准化。如果值集中的值不能以这种 m ≥ 2 的 (N-1) 次方的形式来表示,则该值被称为非标准化值,因为它不具有标准化的表示。
表 4.2.3-A 中总结了两个必需的参数 N 和 K(以及在衍生参数 Emin 和 Emax 上)上的约束和两个可选的浮点值集。
实现支持其中一个或者两个扩展指数值集,然后对于每个支持的扩展指数值集来说,这有一个特定于实现的常量 K,其值受到表 4.2.3-A 的约束;此值 K 反过来指示 Emin 和 Emax 的值。
四个中的每一个值集不仅包包括上面描述的有穷非零值,而且还包括 NaN 值和另四个值,其分别是正零、负零、正无穷和负无穷。
请注意,表 4.2.3-A 中设计的约束,因此浮点值集的每个元素也必须是浮点扩展指数值集、双精度浮点值集和双精度扩展指数值集的一个元素。同样,双精度浮点值集的每个元素也必须是双精度扩展指数值集的一个元素。每个扩展指数值集比相应的标准值集具有更大的指数值范围,但不具有更高的精度。
浮点值集的元素是可以用 IEEE 754 标准中定义的单浮点格式来精确表示的值。双精度浮点值集的元素是可以用 IEEE 754 标准中定义的双浮点格式来精确表示的值。但是,注意,此处定义的浮点扩展指数和双精度浮点扩展指数值集的值都不各自对应可以用 IEEE 754 单精度扩展和双精度扩展格式表示的值。
浮点、浮点扩展指数、双精度浮点和双精度浮点扩展指数值集不是类型。对于 Java 编程语言的实现来说,使用浮点值集的元素来表示 float 类型的值总是正确的;但是,在某些代码区域中允许实现使用浮点扩展指数值集来代替。类似地,对于实现来说,使用双精度浮点值集的元素来表示 double 类型的值总是正确的;但是,在某些代码区域中允许实现使用双精度浮点扩展指数值集来代替。
除 NaN 之外,浮点值是有序的;从最小到最大的排列,它们是负无穷、负有穷非零值、正负零、正有穷非零值和正无穷。
IEEE 754允许每个单精度和双精度浮点格式具有不同的 NaN 值。当新的 NaN 生成时,每个硬件体系结构都返回 NaN 的特定位模式,程序员也可以创建具有不同位模式的 NaNs 来编码,例如,追溯诊断信息。
在大多数情况下,Java SE 平台处理给定类型的 NaN 值,就像折叠成一个标准值一样,因此这个规范通常引用的任意 NaN,就像一个规范值。
However, version 1.3 of the Java SE platform introduced methods enabling the programmer to distinguish between NaN values: the Float.floatToRawIntBits and Double.doubleToRawLongBits methods. The interested reader is referred to the specifications for the Float and Double classes for more information.
正零和负零比较相等;因此,表达式 0.0==-0.0 的结果是 true,而 0.0>-0.0 的结果是 false。但是其它操作可以区分正负零;例如,1.0/0.0 具有值正无穷,而 1.0/-0.0 是负无穷。
NaN 是无序的,所以:
如果操作数中的一个或者两个是 NaN(15.20.1),则数字比较运算符 <、<=、> 和 >= 返回 false。
如果任一操作数是 NaN,则相等运算符 == 返回 false。
特别地,如果 x 或者 y 是 NaN,则 (x<y) == !(x>=y) 将是 false。
如果任一操作数是 NaN(15.21.1),则不相等运算符 != 返回 true。
特别地,当且仅当 x 是 NaN 时, x!=x 是 true。
#### 4.2.4. 浮点运算符
Java 编程语言提供了一些在浮点值上执行的运算符:
比较运算符,其会产生 boolean 类型值。
数字比较运算符 <、<=、> 和 >=(15.20.1)
数字等价性运算符 == 和 !=(15.21.1)
数值运算符,其会产生 float 或 double 类型的值。
一元加和减运算符 + 和 -(15.15.3,15.15.4)
乘法算子 *、/ 和 %(15.17)
加法算子 + 和 -(15.18.2)
自增运算符 ++,包括前缀(15.15.1)和后缀(15.14.2)
自减运算符 --,包括前缀(15.15.2)和后缀(15.14.3)
条件运算符 ? :(15.25)
强制转换运算符(15.16),其会将浮点值转换为任意指定数字类型的值
字符串串联运算符 +(15.18.1),当给定一个 String 操作数和一个浮点操作数时,其会将浮点操作数转换为一个表示该浮点数十进制形式(不丢失信息)值的 String,然后通过串联两个字符串来生成一个新创建的 String。
其它有用的构造器、方法和常量预定义在类 Float、Double 和 Math 中。
如果二元运算符的操作数至少有一个是浮点类型的,则该操作是一个浮点操作,即使其它操作数是整形的。
如果数值运算符的操作数至少有一个是 double 类型的,则该操作将会使用 64 位浮点运算执行,并且该数值运算符的结果是 double 类型的值。如果其它操作数不是 double,首先通过数字提升(5.6)将它们加宽(5.1.5)为 double 类型。
否则,此操作将使用 32 位浮点运算执行,并且数值运算符的结果是 float 类型的。(如果其它操作数不是 float,则首先通过数字提升将它们加宽为 float 类型。)
任何浮点类型值可以强制转换为或转换自任何数字类型。无法在浮点类型和布尔类型之间进行强制转换。
有关将浮点表达式转换为布尔值的成语,请参见 4.2.5。
浮点数字上的运算符行为与 IEEE 754(取余运算符(15.17.3)除外)指定的表现一样。特别地,Java 编程语言需要支持 IEEE 754 非规范化数字和渐进下溢,这使得证明特定数值算法的可取性变得更容易。如果计算结果是非规范化数字,浮点操作不会“刷新到零”。
Java 编程语言要求,浮点运算的行为如同每个浮点运算符将其浮点结果舍入到结果精度一样。不精确的结果必须舍入到可表示的最接近无穷精度结果的值;如果两个最近的可表示的值相等,则选择具有最小有效位零的一个。这是 IEEE 754 标准的默认舍入模式,称为舍入到最近。
当将浮点值舍入到整数(5.1.3)时,Java 编程语言使用向零舍入,这种情况下,其执行起来就像数字被截断,丢弃尾数位一样。向零舍入选择最接近的且尾数不大于无穷精度结果的格式的值作为其结果。
上溢的浮点操作产生有符号无穷。
下溢的浮点操作产生非规范化值或有符号零。
没有数学确定结果的浮点操作产生 NaN。
所有具有 NaN 的数值操作产生 NaN 作为结果。
浮点运算符可以由于以下原因而抛出异常(11(Exceptions)):
如果需要拆箱转换 null 引用,任何浮点运算符可以抛出 NullPointerException。
如果需要装箱转换(5.1.7),并且没有足够的可用内存来执行该转换,则自增和自减运算符 ++(15.14.2,15.15.1)和 --(15.14.3,15.15.2)可以抛出 OutOfMemoryError。
boolean 类型表示具有两个可能值的逻辑量,由字面量 true 和 false(3.10.3)表示。
boolean 运算符是:
关系运算符 == 和 !=(15.21.2)
逻辑运算符 !(15.15.6)
逻辑运算符 &、^ 和 |(15.22.2)
条件与和条件或运算符 &&(15.23)和 ||(15.24)
条件运算符 ? :(15.25)
字符串串联运算符 +(15.18.1),当给定一个 String 操作数和一个 boolean 操作数时,其会将 boolean 操作数转换为一个 String("true"或"false"),然后产生一个新创建的两个字符串的串联体的 String
Boolean 表达式决定几种语句中的控制流:
if 语句(14.9)
while 语句(14.12)
do 语句(14.13)
for 语句(14.14)
boolean 表达式还决定条件运算符 ? :(15.25)中的哪个子表达式被计算。
只有 boolean 和 Boolean 表达式可以用在控制流语句中以及作为条件运算符 ? : 的第一个操作数。
根据任何非零值为 true 的 C 语言约定,通过表达式 x!=0,可以将整形或浮点表达式 x 转换为一个 boolean 值。
根据除了 null 以外的任何引用都是 true 的 C 语言约定,通过表达式 obj!=null,可以将一个对象引用 obj 转换为一个 boolean值。
通过字符串约定(5.4),可以将一个 boolean 值转换为一个 String。
boolean 值可以强制转换为 boolean、Boolean 或 Object(5.5)类型。不允许在 boolean 类型上进行其它强制转换。
有几种引用类型:类类型(8.1)、接口类型(9.1)、类型变量(4.4)和数组类型(10.1)。
ReferenceType:
ClassOrInterfaceType
TypeVariable
ArrayType
ClassOrInterfaceType:
ClassType
InterfaceType
ClassType:
{Annotation} Identifier [TypeArguments]
ClassOrInterfaceType . {Annotation} Identifier [TypeArguments]
InterfaceType:
ClassType
TypeVariable:
{Annotation} Identifier
ArrayType:
PrimitiveType Dims
ClassOrInterfaceType Dims
TypeVariable Dims
Dims:
{Annotation} [ ] {{Annotation} [ ]}
类或接口类型由一个标识符或一个逗号分隔的标识符序列组成,其中每个标识符可以选择后跟类型参数(4.5.1)。如果类型参数出现在类或接口类型中的任何位置,则它是参数化类型(4.5)。
类或接口类型中的每个标识符被归类为包名或类型名(6.5.1)。归类为类型名的标识符可以被注解修饰。如果类或接口类型具有形式 T.id(可选地后跟类型参数),则 id 必须是一个可访问成员类型 T(6.6,8.5,9.5)的简单名称,否则将出现一个编译时错误。类或接口类型表示该成员类型。
对象是类实例或数组。
引用值(通常只是引用)是这些对象的指针,以及一个特殊的 null 引用,其不引用任何对象。
类实例通过类实例创建表达式(15.9)显式地创建。
数组通过数组创建表达式(15.10.1)显式地创建。
当字符串串联运算符 +(15.18.1)在非常量表达式中使用时,一个新的类实例被隐式地创建,产生一个新的 String(4.3.3)类型的对象。
当一个数组初始化器表达式(10.6)被执行时,一个新的数组对象被隐式地创建;当一个类或接口被初始化、一个类的新的实例被创建(15.9)或一个局部变量声明语句被执行(14.4)时,会出现这种情况。
类型 Boolean、Byte、Short、Character、Integer、Long、Float 和 Double 类型的新对象可以通过装箱转换(5.1.7)被隐式地创建。
在对象引用上的运算符有:
字段访问,使用限定名(6.6)或字段访问表达式(15.11)
方法调用(15.12)
强制转换运算符(5.5,15.16)
字符串串联运算符 +(15.18.1),当给定一个 String 操作数和一个引用时,其将会通过调用引用对象的 toString 方法(如果该引用或 toString 的结果是一个 null 引用,则使用 "null")把引用转换为一个 String,然后将产生一个新创建的两个字符串的串联体的 String。
instanceof 运算符(15.20.2)
引用等价性运算符 == 和 !=(15.21.3)
条件运算符 ? :(15.25)。
可能有许多到同一对象的引用。大多数对象具有状态,存储在作为类实例的对象的字段或数组对象的组件的变量中。如果两个变量包含到同一对象的引用,可以使用一个变量的到该对象的引用来修改该对象的状态,然后修改后的状态可以通过另一个变量中的引用来观察到。
每个对象与一个管程(17.1)相关联,其是由同步方法(8.4.3)和同步语句(14.19)来使用,以提供多线程(17(Threads and Locks))对状态的并发访问控制。
类 Object 是所有其它类的超类。
所有类和数组类型都继承(8.4.8)类 Object 的方法,总结如下:
方法 clone 用于制作对象的一个副本。
方法 equals 定义了对象相等性的表示法,其是基于值而不是引用比较。
方法 finalize 仅在对象销毁前运行(12.6)。
方法 getClass 返回表示该对象的类的 Class 对象。
每个引用类型都存在一个 Class 对象。例如,它可以用来发现类的完全限定名、其成员、其直接父类以及其实现的接口。
getClass 的方法调用表达式的类型是 Class<? extends |T|>,其中 T 是搜索 getClass 的类或接口,而 |T| 表示对 T 的擦除(4.6)。
声明为同步(8.4.3.6)的类方法在与该类的 Class 对象相关联的管程上进行同步。
方法 hashCode 在哈希表如 java.util.HashMap 中非常有用,与方法 equals 一起使用。
方法 wait、notify 和 notifyAll 在使用线程(17.2)的并发编程中使用。
方法 toString 返回该对象的一个 String 表示。
类 String 的实例表示 Unicode 码点序列。
String 对象具有一个常量(无法改变)值。
String 字面量(3.10.5)是到类 String 实例的引用。
当结果不是常量表达式(15.28)时,字符串拼接运算符(15.18.1)隐式地创建一个新的 String 对象。
如果两个引用类型具有相同的二进制名(13.1)并且,类型参数,如果有,相同,则它们是相同的编译时类型,递归地应用此定义。
运行时,多个具有相同二进制名的引用类型可以由不同的类加载器同时加载。这些类型表示同一类型声明,也可能不表示。即使两个这样的类型表示相同的类型声明,它们也被认为是不同的。
两种引用类型是相同的运行时类型,如果:
它们同时是类或接口类型,由同一类加载器定义,并且具有相同的二进制名(13.1),在这种情况下,它们有时被称为相同的运行时类或相同的运行时接口。
它们同时是数组类型,并且它们的组件类型是同一运行时类型(10(Arrays))。
类型变量是一个在类、接口、方法和构造器体中用作类型的非限定标识符。
类型变量由泛型类、接口、方法或构造器(8.1.2,9.1.2,8.4.4,8.8.4)的类型参数的声明引入。
TypeParameter:
{TypeParameterModifer} Identifier [TypeBound]
TypeParameterModifer:
Annotation
TypeBound:
extends TypeVariable
extends ClassOrInterfaceType {AdditionalBound}
AdditionalBound:
& InterfaceType
声明为类型参数的类型变量的范围在6.3中指定。
每个声明为类型参数的类型变量都有一个边界,如果没有为类型变量声明边界,则假定是 Object。如果声明了一个边界,则该边界由以下两者中的一个组成:
单个类型变量 T,或
类或接口类型 T,可能后跟接口类型 I1 & ... & In。
如果 I1 ... In 中的任何一个类型是类类型或类型变量,则这是一个编译时错误。
边界的所有组成类型的擦除必须两两不同,否则将出现一个编译时错误。
如果两个接口类型是同一泛型接口的不同参数化类型,则类型变量不能同时是两个接口类型的子类型,否则将出现一个编译时错误。
由类型变量边界中的第一个类型决定该类型变量的擦除,并且类类型或类型变量只能出现在第一个位置时,边界中类型的顺序才有意义。
具有边界 T & I1 & ... & In 的类型变量 X 的成员是出现在声明类型变量的点处的交叉类型(4.9) T & I1 & ... & In 的成员。
The type variable T has the same members as the intersection type C & I, which in turn has the same members as the empty class CT, defined in the same
scope with equivalent supertypes. The members of an interface are always public, and therefore always inherited (unless overridden). Hence mI is a
member of CT and of T. Among the members of C, all but mCPrivate are inherited by CT, and are therefore members of both CT and T.
If C had been declared in a different package than T, then the call to mCPackage would give rise to a compile-time error, as that member would not be
accessible at the point where T is declared.
类或接口的泛型(8.1.2,9.1.2)声明定义了一个参数化类型集合。
参数化类型是形式为 C<T1,...,Tn> 的类或接口类型,其中 C 是泛型类型的名称,<T1,...,Tn> 是一个类型参数列表,这些类型参数表示该泛型类型的一个特定的参数化。
泛型类型有类型参数 F1,...,Fn,这些类型参数具有相应的边界 B1,...,Bn。参数化类型的每个类型参数 Ti 涉及所有类型,这些类型是在相应边界中列举出的所有类型的子类型。即,对于 Bi 中的每个边界类型 S,Ti 是 S[F1:=T1,...,Fn:=Tn](4.10)的子类型。
如果满足以下条件的话,则参数化类型 C<T1,...,Tn> 是格式良好的:
C 是泛型类型的名称。
类型参数的数量与 C 的泛型声明中的类型参数的数量一致。
当受捕获转化(5.1.10)产生类型 C<X1,...,Xn> 时,对于 Bi 中的每个边界类型 S 来说,每个类型参数 Xi 都是 S[F1:=X1,...,Fn:=Xn] 的子类型。
如果一个参数化类型不是格式良好的,则这是一个编译时错误。
在此规范中,无论何时我们说一个类或接口类型,我们也将泛型版本包括在内,除非显式排除。
如果满足以下任一情况,则两个参数化类型可证明是不同的:
它们是不同泛型类型声明的参数化。
它们的类型参数的任何一个可证明是不同的。
A parameterized type may be an parameterization of a generic class or interface which is nested. For example, if a non-generic class C has a generic member class D, then C.D is a parameterized type. And if a generic class C has a non-generic member class D, then the member type C.D is a parameterized type, even though the class D is not generic.
类型参数可以是引用类型或通配符。通配符在只需要有关类型参数的部分了解的情况下很有用。
TypeArguments:
< TypeArgumentList >
TypeArgumentList:
TypeArgument {, TypeArgument}
TypeArgument:
ReferenceType
Wildcard
Wildcard:
{Annotation} ? [WildcardBounds]
WildcardBounds:
extends ReferenceType
super ReferenceType
通配符可以给定显式的边界,就像常规的类型变量声明一样。上边界由以下语法表示,其中 B 是边界:
? extends B
与方法签名中声明的普通类型变量不同,使用通配符时不需要类型推断。因此,允许在通配符上声明下边界,使用以下语法,其中 B 是下边界:
? super B
通配符 ? extends Object 等价于无界通配符 ?。
如果满足以下条件之一,两个类型参数可证明是不同的:
两个参数都不是类型变量或通配符,并且两个参数不是同一类型。
一个类型参数是类型变量或通配符,带有 S 的上边界(来自捕获转换(5.1.10),如有必要);另一个类型参数 T 不是类型变量或通配符;既不满足 |S| <: |T| 也不满足 |T| <: |S|(4.8,4.10)。
每个类型参数是类型变量或通配符,带有 S 和 T 的上边界(来自捕获转换,如有必要);既不满足 |S| <: |T| 也不满足 |T| <: |S|。
类型参数 T1 被称为包含另一个类型参数 T2,写作 T2 <= T1,如果在以下规则的自反和传递闭合下(其中表示子类型化(4.10)),可证明由 T2 表示的类型集合是由 T1 表示的类型集合的子集:
如果 T <: S,? extends T <= ? exntends S
? exntends T <= ?
如果 S <: T,? super T <= ? super S
? super T <= ?
? super T <= ? extends Object
T <= T
T <= ? extends T
T <= ? super T
The relationship of wildcards to established type theory is an interesting one, which we briefly allude to here. Wildcards are a restricted form of existential types. Given a generic type declaration G, G is roughly analogous to Some X <: B. G. 通配符与已建立的类型理论的关系是一个有趣的问题,我们在此简要地提到了这一点。通配符是存在类型的受限形式。给定一个泛型类型声明 G,G 大致类似于某些 X <: B. G。 Historically, wildcards are a direct descendant of the work by Atsushi Igarashi and Mirko Viroli. Readers interested in a more comprehensive discussion should refer to On Variance-Based Subtyping for Parametric Types by Atsushi Igarashi and Mirko Viroli, in the Proceedings of the 16th European Conference on Object Oriented Programming (ECOOP 2002). This work itself builds upon earlier work by Kresten Thorup and Mads Torgersen (Unifying Genericity, ECOOP 99), as well as a long tradition of work on declaration based variance that goes back to Pierre America's work on POOL (OOPSLA 89).
Wildcards differ in certain details from the constructs described in the aforementioned paper, in particular in the use of capture conversion (§5.1.10) rather than the close operation described by Igarashi and Viroli. For a formal account of wildcards, see Wild FJ by Mads Torgersen, Erik Ernst and Christian Plesner Hansen, in the 12th workshop on Foundations of Object Oriented Programming (FOOL 2005).
让 C 成为一个具有类型参数 A1,...,An 的泛型类或接口声明,让 C<T1,...,Tn> 成为一个 C 的参数化,其中,对于 1 ≤ i ≤ n,Ti 是一个类型(而不是通配符)。然后:
让 m 成为 C 中的一个成员或构造器声明,其类型像声明的一样是 T(8.2,8.8.6)。
C<T1,...,Tn> 中 m 的类型是 T[A1:=T1,...,An:=Tn]。
让 m 成为 D 中的一个成员或构造器声明,其中 D 是一个由 C 继承的类或由 C 实现的接口。让 D<U1,...,Uk> 成为 C<T1,...,Tn> 的父型,其对应 D。
C<T1,...,Tn> 中 m 的类型是 D<U1,...,Uk> 中 m 的类型。
如果 C 的参数化中的类型参数的任何一个是通配符,则:
C<T1,...,Tn> 中字段、方法和构造器的类型是 C<T1,...,Tn>(5.1.10)的捕获转换中的字段、方法和构造器的类型。
让 D 成为 C 中的一个(可能泛型)类或接口声明。然后 C<T1,...,Tn> 中 D 的类型是 D,如果 D 是泛型,则其中所有类型参数都是无界通配符。
这没有任何结果,因为不执行捕获转换就访问一个参数化类型的成员是不可能的,并且不可能在类实例创建表达式(15.9)中的关键字 new 后面使用通配符。
上一段落唯一的例外是当嵌套的参数化类型用作 instanceof 运算符(15.20.2)中的表达式时,其中不应用捕获转换。
在泛型类型声明中声明的 static 成员必须使用对应泛型类型(6.1,6.5.2,6.5.6.2)的非泛型类型来引用,否则产生一个编译时错误。
换言之,使用参数化类型来引用在泛型类型声明中声明的 static 成员是非法的。
类型擦除是一个从类型(可能包括参数化类型和类型变量)到类型(从不是参数化类型或类型变量)的映射。我们把 |T| 写作类型 T 的擦除。擦除映射定义如下:
参数化类型 G<T1,...,Tn>(4.5)的擦除是 |G|。
嵌套类型的 T.C 擦除是 |T|.C。
数组类型 T[] 的擦除是 |T|[]。
类型变量(4.4)的擦除是其最左侧边界的擦除。
每个其它类型的擦除是该类型本身。
类型擦除也将构造器或方法的签名(8.4.2)映射成没有参数化类型或类型变量的签名。构造器或方法签名 s 的擦除是一个由与 s 的名称相同的名称和 s 中给定的所有正规参数类型的擦除组成签名。
如果方法或构造器的签名擦除过,则方法(8.4.5)的返回类型和泛型方法或构造器(8.4.4,8.8.4)的类型参数也要进行擦除。
泛型方法的签名的擦除不具有类型参数。
由于一些类型信息会在编译期间擦除掉,所以在运行时并非所有类型都可用。运行时完全可用的类型被称为具体化类型。
当且仅当满足以下任意一项时,一个类型是具体化的:
它表示一个非泛型类或接口声明。
它是一个参数化类型,其中所有类型参数都是无界通配符(4.5.1)。
它是一个原始类型(raw type)(4.8)。
它是一个原始类型(primitive type)(4.2)。
它是一个数组类型(10.1),该数组类型的元素类型是具体化的。
它是一个嵌套类型,其中,对于由"."分隔的每个类型 T,T 本身是具体化的。
例如,如果一个泛型类 X 具有一个泛型成员类 Y,则类型 X.Y 是具体化的,因为 X 是具体化的,并且 Y 是具体化的。类型 X<?>.Y 不是具体化的,因为不是 Y 具体化的。
交集类型不是具体化的。
The decision not to make all generic types reifiable is one of the most crucial, and controversial design decisions involving the type system of the Java programming language.
不使所有泛型类型具体化的决定是涉及 Java 编程语言类型系统的最关键、最具争议的设计决策之一。
Ultimately, the most important motivation for this decision is compatibility with existing code. In a naive sense, the addition of new constructs such as generics has no implications for pre-existing code. The Java programming language, per se, is compatible with earlier versions as long as every program written in the previous versions retains its meaning in the new version. However, this notion, which may be termed language compatibility, is of purely theoretical interest. Real programs (even trivial ones, such as "Hello World") are composed of several compilation units, some of which are provided by the Java SE platform (such as elements of java.lang or java.util). In practice, then, the minimum requirement is platform compatibility - that any program written for the prior version of the Java SE platform continues to function unchanged in the new version.
One way to provide platform compatibility is to leave existing platform functionality unchanged, only adding new functionality. For example, rather than modify the existing Collections hierarchy in java.util, one might introduce a new library utilizing generics. The disadvantages of such a scheme is that it is extremely difficult for pre-existing clients of the Collection library to migrate to the new library. Collections are used to exchange data between independently developed modules; if a vendor decides to switch to the new, generic, library, that vendor must also distribute two versions of their code, to be compatible with their clients. Libraries that are dependent on other vendors code cannot be modified to use generics until the supplier's library is updated. If two modules are mutually dependent, the changes must be made simultaneously.
Clearly, platform compatibility, as outlined above, does not provide a realistic path for adoption of a pervasive new feature such as generics. Therefore, the design of the generic type system seeks to support migration compatibility. Migration compatibiliy allows the evolution of existing code to take advantage of generics without imposing dependencies between independently developed software modules.
The price of migration compatibility is that a full and sound reification of the generic type system is not possible, at least while the migration is taking place.
为了便于与非泛型遗留代码进行交互,可以将参数化类型(4.5)的擦除(4.6)或元素类型是参数化类型的数组类型(10.1)的擦除用作类型。此种类型称为原始类型(raw type)。
更确切地说,原始类型被定义为以下之一:
通过在不附带类型参数列表的情况下采用泛型类型声明的名称而形成的引用类型。
元素类型是原始类型的数组类型。
原始类型 R 的不是继承自 R 的父类或父接口的非 static 的成员类型。
非泛型类或接口类型不是原始类型(raw type)。
Inner 的成员的类型依赖于 Outer 的类型参数。如果 Outer 是原始的,Inner 也必须被视为原始的,因为没有用于 T 的合法绑定。
此规则仅适用于不是继承来的类型成员。依赖于类型变量的继承来的类型成员将作为原始类型继承,这是由于,原始类型的父类型将被擦除,这个规则,本节后面将对其进行说明。
上述规则的另一个含义是,原始类型的泛型内部类本身只能用作原始类型:
不能将 Inner 作为部分原始类型(一个“稀有”类型)来访问:
由于 Outer 本身是原始的,因此包括 Inner 所有其内部类都是这样,因此不能向 Inner 传递任何类型参数。
原始类型的父类(各自地,父接口)是泛型类型任意参数化的父类(父接口)的擦除。
不是继承自其父类或父接口的原始类型 C 的构造器(8.8)、实例方法(8.4,9.4)或非 static 字段(8.3)的类型是原始类型,该原始类型对应在与 C 相对应的泛型声明中的其类型的擦除。
原始类型 C 的 static 方法或 static 字段的类型与在对应于 C 的泛型声明中的其类型相同。
向不是继承自其父类或父接口的原始类型的非 static 类型成员传递类型参数,是一个编译时错误。
尝试将参数化类型的类型成员用作原始类型,是一个编译时错误。
这意味着对“稀有”类型的禁止扩展到了限定类型被参数化的情况,但我们尝试将内部类用作原始类型:
这与上面讨论的情况恰恰相反。对于这种不完整的类型,没有正当的理由。在遗留代码中,没使用任何类型参数。在非遗留代码中,我们应该正确地使用泛型类型并传递所有必需的类型参数。
类的父类型可以是原始类型。类的成员访问被视为标准访问,而父类型的成员访问被视为原始类型。在类的构造器中,调用 super 被视为在原始类型上的方法调用。
原始类型的使用仅被允许作为兼容遗留代码的让步。强烈不建议,在 Java 编程语言中引入泛型后编写的代码中使用原始类型。可能在 Java 编程语言将来的版本中不再允许使用原始类型。
为了确保始终标记潜在的类型判定规则的违反行为,某些对原始类型的成员的访问将导致编译时未检查警告。当访问原始类型的成员或构造器时,编译时未检查警告规则如下:
在字段赋值时:如果字段访问表达式(15.12.1)中的 Primary 的类型是原始类型,则如果擦除改变了字段的类型,将产生一个编译时未检查警告。
在方法或构造器调用时:如果搜索(15.12.1)的类或接口的类型是原始类型,则如果擦除改变了方法或构造器的形式参数类型中的任何一个,将产生一个编译时未检查警告。
对于方法调用,当形式参数类型不会在擦除后改变(即使返回类型 和/或 throws 子句改变了),对于从字段读取,或者对于原始类型的类实例创建,不会产生编译时未检查警告。
Note that the unchecked warnings above are distinct from the unchecked warnings possible from unchecked conversion (§5.1.9), casts (§5.5.2), method declarations (§8.4.1, §8.4.8.3, §8.4.8.4, §9.4.1.2), and variable arity method invocations (§15.12.4.2).
The warnings here cover the case where a legacy consumer uses a generified library. For example, the library declares a generic class Foo that has a field f of type Vector, but the consumer assigns a vector of integers to e.f where e has the raw type Foo. The legacy consumer receives a warning because it may have caused heap pollution (§4.12.2) for generified consumers of the generified library.
(Note that the legacy consumer can assign a Vector from the library to its own Vector variable without receiving a warning. That is, the subtyping rules (§4.10.2) of the Java programming language make it possible for a variable of a raw type to be assigned a value of any of the type's parameterized instances.)
The warnings from unchecked conversion cover the dual case, where a generified consumer uses a legacy library. For example, a method of the library has the raw return type Vector, but the consumer assigns the result of the method invocation to a variable of type Vector. This is unsafe, since the raw vector might have had a different element type than String, but is still permitted using unchecked conversion in order to enable interfacing with legacy code. The warning from unchecked conversion indicates that the generified consumer may experience problems from heap pollution at other points in the program.
原始类型与通配符密切相关。两者都是基于现存的类型。原始类型可以看作其类型规则故意是无界的通配符形式,以适应与遗留代码的交互。历史上,原始类型在通配符之前;
交集类型取 T1 & ... & Tn (n > 0) 形式,其中 Ti (1 ≤ i ≤ n) 是类型。
交集类型可以从类型参数边界(4.4)和强制转换表达式(15.16)中派生;它们也出现在捕获转换(5.1.10)和最小上边界计算(4.10.4)的处理过程中。
交集类型的值是那些是类型 Ti for 1 ≤ i ≤ n 的所有值的对象。
每个交集类型 T1 & ... & Tn 引出一个概念类或接口,目的是识别交集类型的成员,如下所示:
对于每个 Ti (1 ≤ i ≤ n),让 Ci 是最具体的类或数组类型,如 Ti <: Ci。然后一定有某个 Ck,如 Ck <: Ci,对于任何 i (1 ≤ i ≤ n),否则产生一个编译时错误。
对于 1 ≤ j ≤ n,如果 Tj 是一个类型变量,则让 Tj' 是一个其成员与 Tj 的 public 成员相同的接口;否则,如果 Tj 是一个接口,则让 Tj' 是 Tj。
如果 Ck 是对象,则会产生一个概念接口;否则,产生一个具有直接父类 Ck 的概念类。这个类或接口具有直接父接口 T1', ..., Tn',并声明在交集类型出现的包中。
交集类型的成员是其诱导的类或接口的成员。
在交集类型和类型变量的边界之间进行区分是值得的。每个类型变量边界都会诱导一个交集类型。此交集类型通常是微不足道的,由单个类型组成。边界的形式受到限制(只有第一个元素可以是类或类型变量,并且只有一个类型变量可以出现在边界中),以排除某些不方便的情况的存在。但是,捕获转换可能导致创建类型变量,其边界更一般,如数组类型)。
子类型和父类型关系是类型上的二元关系。
一个类型的父类型通过对直接父类型关系的自反和传递闭包获得,写作 S >1 T,其由本节后面给出的规则定义。我们写 S :> T 来表示 S 和 T 之间持有的父类型关系。
如果 S :> T 且 S ≠ T,则 S 是 T 的适当的父类型,写作 S > T。
类型 T 的子类型是所有类型 U,这样 T 是 U 和 null 类型的一个父类型。我们写 T <: S 来表示类型 T 和 S 之间持有的子类型关系。
如果 T <: S 且 S ≠ T,则 T 是 S 的一个适当的子类型,写作 T < S。
如果 S >1 T,则 T 是 S 的一个直接子类型,写作 T <1 S。
子类型化没有扩展到参数化类型: T <: S 不意味着 C <: C。
以下规则定义了原始类型中的直接父类型关系:
double >1 float
float >1 long
long >1 int
int >1 char
int >1 short
short >1 byte
给定一个非泛型类型声明 C,类型 C 的直接父类型是以下所有:
C 的直接父类型(8.1.4)。
C 的直接父接口(8.1.5)。
如果 C 是一个没有直接父接口(9.1.3)的接口类型,则为类型 Object。
给定一个泛型类型声明 C<F1,...,Fn> (n > 0),原始类型 C(4.8)的直接父类型是以下所有:
原始类型 C 的直接父类型。
原始类型 C 的直接父接口。
如果 C<F1,...,Fn> 是一个没有直接父接口(9.1.2)的泛型接口类型,则为类型 Object。
给定一个泛型类型声明 C<F1,...,Fn> (n > 0),泛型类型 C<F1,...,Fn> 的直接父接口是以下所有:
C<F1,...,Fn> 的直接父类型。
C<F1,...,Fn> 的直接父接口。
如果 C<F1,...,Fn> 是一个没有直接父接口的泛型接口类型,则为类型 Object。
原始类型 C。
给定一个泛型类型声明 C<F1,...,Fn> (n > 0),参数化类型 C<F1,...,Fn> 的直接父类型,其中 Ti (1 ≤ i ≤ n) 是一个类型,是如下所有:
D<U1 θ,...,Uk θ>,其中 D<U1,...,Uk> 是一个泛型类型,其是泛型类型 C<T1,...,Tn> 的一个直接父类型,且 θ 是替代 [F1:=T1,...,Fn:=Tn]。
C<S1,...,Sn>,其中 Si 包含 Ti (1 ≤ i ≤ n)(4.5.1)。
如果是 C<F1,...,Fn> 一个没有直接父接口的泛型接口类型,则为 Object。
原始类型 C。
给定一个泛型类型声明 C<F1,...,Fn> (n > 0),参数化类型 C<R1,...,Rn> 的直接父类型,其中至少 Ri (1 ≤ i ≤ n) 中的一个是一个通配符类型参数,是应用到 C<R1,...,Rn>(5.1.10)的捕获转换的结果的参数化类型 C<X1,...,Xn> 的直接父类型。
交集类型 T1 & ... & Tn 的直接父类型是 Ti (1 ≤ i ≤ n)。
类型变量的直接父类型是其边界中列举出的类型。
类型变量是其下边界的一个直接父类型。
null 类型的直接父类型是除 null 类型自己以外的所有引用类型。
以下规则定义了数组类型中的直接父类型:
如果 S 和 T 两者都是引用类型,则当且仅当 S >1 T 时, S[] >1 T[]。
Object >1 Object[]
Cloneable >1 Object[]
java.io.Serializable >1 Object[]
如果 P 是原始类型,则:
Object >1 P[]
Cloneable >1 P[]
java.io.Serializable >1 P[]
一组引用类型的最小上边界,或“润滑剂”,是一种共享的父类型,它比任何其它共享的父类型更具体(也就是所,没有其它共享父类型是该最小上边界的子类型)。这个类型,lub(U1, ..., Uk),确定如下:
如果 k = 1,则润滑剂是此类型本身: lub(U) = U。
否则:
对于每个 Ui (1 ≤ i ≤ k):
让 ST(Ui) 是 Ui 的父类型集合。
让 EST(Ui),Ui 的擦除后的父类型的集合,是:
EST(Ui) = { |W| | W in ST(Ui) },其中 |W| 是 W 的擦除。
计算擦除后的父类型的集合的原因是为了处理类型集合包含泛型类型的几个不同参数化的情况。
例如,给定的 List 和 List,仅仅将集合 ST(List) = { List, Collection, Object } 和 ST(List) = { List, Collection, Object } 相交,将产生一个集合 { Object },我们将会失去上边界可以安全地假定为一个 List 的事实的追踪。
相比之下,将 EST(List) = { List, Collection, Object } 与 EST(List) = { List, Collection, Object } 相交会产生 { List, Collection, Object },其最终将使我们产生 List<?>。
让 EC,U1 ... Uk 擦除后的候选集合,成为所有集合 EST(Ui) (1 ≤ i ≤ k) 的交集。
让 MEC,U1 ... Uk 最小擦除后的候选集合,成为:
MEC = { V | V in EC, and for all W ≠ V in EC, it is not the case that W <: V }
因为我们正在寻求推断更精确的类型,所以我们希望筛选出任何是其它候选者的父类型的候选者。这就是 EMC 计算完成。在我们的运行示例中,我们有 EC = { List, Collection, Object },因此 MEC = { List }。下一步是恢复 MEC 中的擦除后的类型的类型参数。
对于作为泛型类型的 MEC 的任何元素 G:
让“相关的”G 的参数化, Relevant(G),是:
Relevant(G) = { V | 1 ≤ i ≤ k: V in ST(Ui) and V = G<...> }
在我们的运行示例中,仅有的 MEC 的泛型元素是 List,且 Relevant(List) = { List, List }。我们现在寻求为包含(4.5.1)String 和 Object 两者的 List 寻找一个类型参数。
这通过以下定义的最少包含参数化(lcp)操作的方式来完成。第一行在一个集合上定义 lcp(),如 Relevant(List),作为在此集合的元素的列表上的操作。下一行定义在此列表上的操作,作为在列表元素上的两两还原。第三行是在参数化类型对上的 lcp() 的定义,其反过来依赖于最小包含类型参数(lcta)的概念。lcta() 是为所有可能的情况定义的。
让 G 的“候选者”参数化,成为包含 G 的所有相关的参数化的泛型类型 G 的最具体的参数化:
Candidate(G) = lcp(Relevant(G))
其中 lcp(),最小包含调用,是:
lcp(S) = lcp(e1, ..., en) where ei (1 ≤ i ≤ n) in S
lcp(e1, ..., en) = lcp(lcp(e1, e2), e3, ..., en)
lcp(G<X1, ..., Xn>, G<Y1, ..., Yn>) = G<lcta(X1, Y1), ..., lcta(Xn, Yn)>
lcp(G<X1, ..., Xn>) = G<lcta(X1), ..., lcta(Xn)>
而且其中 lcta(),最小包含类型参数,是:(假定 U 和 V 是类型)
lcta(U, V) = U if U = V, otherwise ? extends lub(U, V)
lcta(U, ? extends V) = ? extends lub(U, V)
lcta(U, ? super V) = ? super glb(U, V)
lcta(? extends U, ? extends V) = ? extends lub(U, V)
lcta(? extends U, ? super V) = U if U = V, otherwise ?
lcta(? super U, ? super V) = ? super glb(U, V)
lcta(U) = ? if U's upper bound is Object, otherwise ? extends lub(U,Object)
并且其中 glb() 定义在5.1.10。
让 lub(U1 ... Uk) 是:
Best(W1) & ... & Best(Wr)
其中 Wi (1 ≤ i ≤ r) 是 MEC 的元素,U1 ... Uk 的最小擦除后的候选者集合;
并且其中,如果这些元素中的任一是泛型的,我们使用候选者参数化(以便恢复类型参数);
如果 X 是泛型的,则 Best(X) = Candidate(X);否则,X。
严格来说,这个 lub() 函数仅接近于一个最小上边界。正式地,可能存在一些其它类型 T,如所有 U1 ... Uk 是是 T 的子类型,T 是 lub(U1, ..., Uk) 的一个子类型。但是,Java 编程语言编译器必须实现如上所述的 lub()。
lub() 函数有可能产生一个无穷类型。这是允许的,Java 编程语言编译器必须识别这种情况,并使用循环数据结构适当地表示它们。
无穷类型的可能性来自 lub() 的递归调用。熟悉递归类型的读者应注意,无穷类型与递归类型不一样。
类型在大多数声明和一些表达式中使用。特别地,有 16 个使用类型的类型上下文:
声明中:
1. 类声明的 extends 或 implements 子句中的类型(8.1.4,8.1.5,8.5,9.5)
2. 接口声明的 extends 子句中的类型(9.1.3,8.5,9.5)
3. 方法的返回类型(包括注解类型的元素的类型)(8.4.5,9.4,9.6.1)
4. 方法或构造器的 throws 子句中的类型(8.4.6,8.8.5,9.4)
5. 泛型类、接口、方法或构造器的类型参数声明的 extends 子句中的类型(8.1.2,9.1.2,8.4.4,8.8.4)
6. 类或接口的字段声明中的类型(8.3,9.3,8.9.1)
7. 方法、构造器或 lambda 表达式的形参声明中的类型(8.4.1,8.8.1,9.4,15.27.1)
8. 方法的接收参数的类型(8.4.1)
9. 局部变量声明中的类型(14.4,14.14.1,14.14.2,14.20.3)
10. 异常参数声明中的类型(14.20)
表达式中:
11. 用于显式构造器调用语句或类实例创建表达式或方法调用表达式的显式类型参数列表中的类型(8.8.7.1,15.9,15.12)
12. 在不限定的类实例创建表达式中,作为要实例化(15.9)的类类型,或者要实例化(15.9.5)的匿名类的直接父类或直接父接口
13. 数组创建表达式中的元素类型(15.10.1)
14. 转换表达式的转换运算符中的类型(15.16)
15. instanceof 关系运算符后面的类型(15.20.2)
16. 在方法引用表达式(15.13)中,作为要搜索成员方法的引用类型,或者要构造的类类型或数组类型。
此外,类型也用作:
以上任何上下文中的数组类型的元素类型;
以上任何上下文中的参数化类型的非通配符类型参数或通配符类型参数的边界。
最后,在 Java 编程语言中有三个表示类型使用的术语:
无界通配符(4.5.1)
在可变的任意参数(8.4.1)类型中的 ...,以指示数组类型
在构造器声明(8.8)中的简单的类型名,以指示构造对象的类
类型上下文中类型的含义由下面给出:
4.2,对于基本类型
4.4,对于类型参数
4.5,对于参数化的类和接口类型,或者在参数化类型中作为类型参数或在参数化类型中作为通配符类型参数的边界出现
4.8,对于原始的类和接口类型
4.9,对于类型参数边界中的交集类型
6.5,对于泛型不重要(6.1)的上下文中的类和接口类型
10.1,对于数组类型
一些类型上下文限制如何参数化引用类型:
以下类型上下文要求,如果一个类型是参数化引用类型,则其不具有通配符类型参数:
类声明的 extends 或 implements 子句中(8.1.4,8.1.5)
接口声明的 extends 子句中(9.1.3)
在未限定的类实例创建表达式中,作为要实例化的类类型(15.9),或者作为要实例化的匿名类的直接父类或直接父接口(15.9.5)
在方法引用表达式中(15.13),作为要搜索成员方法的引用类型,或者作为要构造的类类型或数组类型。
此外,要用在显式构造器调用语句或类实例创建表达式或方法调用表达式或方法引用表达式中的显式类型参数列表中允许非通配符类型参数(8.8.7.1,15.9,15.12,15.13)。
以下类型上下文要求,如果类型是参数化引用类型,其只有无界通配符类型参数(即,它是可具体化的类型):
作为数组创建表达式中的元素类型(15.10.1)
作为跟在 instanceof 关系运算符后的类型(15.20.2)
以下类型上下文完全不允许参数化引用类型,因为它们涉及异常,并且异常类型是非泛型的(6.1):
作为可以由方法或构造器抛出的异常类型(8.4.6,8.8.5,9.4)
在异常参数声明中(14.20)
在任何使用类型的类型上下文中,它可能注解表示原始类型的关键字或表示简单的引用类型名的标识符。它也可能通过在数组类型中所需的嵌套层次的 [ 的左侧写一个注解来注解数组类型。这些位置的注解称为类型注解,并在9.7.4中指定。下面是一些示例:
@Foo int[] f; 注解原始类型 int
int @Foo [] f; 注解数组类型 int[]
int @Foo [][] f; 注解数组类型 int[][]
int[] @Foo [] f; 注解数组类型 int[],其是数组类型 int[][] 的组件类型
在声明中出现的五个类型上下文作为一些声明上下文占据了相同不变的句法(9.6.4.1):
方法的返回类型(包括注解类型的元素类型)
类或接口的字段声明中的类型(包括枚举常量)
方法、构造器或 lambda 表达式的形参声明中的类型
局部变量声明中的类型
异常参数声明中的类型
实际上,程序中相同的句法位置既可以是类型上下文,也可以是声明上下文,因为声明的修饰符先于声明的实体的类型。9.7.4 解释了这种位置的注解是如何被视为出现在类型上下文或声明上下文或两者中的。
一个变量是一个存储位置,具有一个相关的类型,有时称为编译时类型,它是一个基元类型(4.2)或一个引用类型(4.3)。
一个变量的值由一个赋值(15.26)或一个前缀或后缀 ++(自增)或 --(自减)运算符(15.14.2,15.14.3,15.15.1,15.15.2)来改变。
变量值与其类型的兼容性由 Java 编程语言的设计来保证,只要程序不给出编译时未检查警告(4.12.2)风险。默认值(4.12.5)是兼容的,检查所有变量赋值的赋值兼容性(5.2),通常在编译时,但是,在涉及数组的情况下,进行运行时检查(10.5)。
基元类型变量总是持有该精确原始类型的原始值。
类类型 T 的变量持有一个 null 引用,或一个到类 T 或任何是 T 的子类的类的实例的引用。
接口类型的变量可以持有一个 null 引用,或一个到任何实现此接口的类的实例的引用。
注意,不保证变量始终引用其声明类型的子类型,但仅指向此声明类型的子类或子接口。这是由于下面讨论的堆污染的可能性。
如果 T 是一个基元类型,则一个类型“T 数组”的变量可以持有一个 null 引用,或一个到任何“T 数组”类型的数组的引用。
如果 T 是一个引用类型,则一个类型“T 数组”的变量可以持有一个 null 引用,或一个到任何“S 数组”类型的数组的引用,因此类型 S 是类型 T 的子类或子接口。
类型 Object[] 的变量可以持有一个到任何引用类型数组的引用。
类型 Object 的变量可以持有一个 null 引用,或一个到任何对象的引用,而不管它是一个类或一个数组的一个实例。
参数化类型的变量可能引用一个不是该参数化类型的对象。这种情况被称为堆污染。
如果程序执行一些涉及给出编译时未检查警告(4.8,5.1.9,5.5.2,8.4.1,8.4.8.3,8.4.8.4,9.4.1.2,15.12.4.2)风险的原始类型的操作,或如果程序通过一个父类型是原始或非泛型的数组变量来为一个不可具体化的元素类型的数组变量其别名,则才可能发生堆污染。
变量总是引用一个表示参数化类型的类的实例的对象。
以上示例中 ls 的值总是一个提供 List 表示的类的实例。
将原始类型的表达式赋值为参数化类型的变量时,应仅在合并未使用参数化类型的遗留代码时使用,而不是使用更现代的代码。
如果没有需要发出编译时未检查警告的操作发生,并且不存在具有不可具体化的元素类型的数组变量的不安全别名,则堆污染将不会发生。注意,这并不意味着当实际出现编译时未检查警告时才会发生堆污染。可以运行一个程序,其中一些二进制是由编译期为 Java 编程语言的早期版本产生的,或来自显式地抑制未检查警告的源。这种充其量是不健康的。
相反,尽管执行可能(可能会)给出编译时未检查警告风险的代码,也可能不会发生堆污染。事实上,良好的编程实践要求程序员使自己满足,尽管有任何未检查警告,代码是正确的,且堆污染不会发生。
有八种变量:
1. 类变量是在类声明中(8.3.1.1)用关键字 static,或在接口声明中(9.3)带或不带关键字 static 声明的字段。
当准备(12.3.2)类或接口并初始化为默认值(4.12.5)时,其类变量被创建。当类或接口被卸载(12.7)时,其类变量立马停止存在。
2. 实例变量是在类声明中不使用关键字 static(8.3.1.1)声明的字段。
如果类 T 有一个实例变量字段 a,然后创建一个新的实例变量 a,并初始化为默认值(4.12.5),作为每个新创建的类 T 或任何 T 的子类(8.1.4)的类的对象的一部分。当字段的对象不再被引用,对象的任何必需的终结(12.6)完成后,此实例变量立马停止存在。
3. 数组组件是无名变量,不管什么时候创建一个数组对象时(10(Arrays),15.10.2),它们也被创建并初始化为默认值(4.12.5)。
4. 方法参数(8.4.1)命名传递给方法的参数值。
对于在方法声明中声明的每个参数,每一次调用方法时(15.12),一个新的参数变量被创建。新的变量初始化为相应的来自方法调用的参数值。当方法体执行完成时,方法参数立即停止存在。
5. 构造器参数(8.8.1)命名传递给构造器的参数值。
对于在构造器声明中声明的每个参数,每一次一个类实例创建表达式(15.9)或显式的构造器调用(8.8.7)调用该构造器时,一个新的参数变量被创建。该新变量初始化为相应的来自创建表达式或构造器调用的参数值。当构造器体执行完成时,构造器参数立即停止存在。
6. lambda 参数(15.27.1)命名传递给 lambda 表达式体(15.27.2)的参数值。
对于声明在 lambda 表达式中的每个参数,每一次由 lambda 体实现的方法被调用时(15.12),一个新的参数变量被创建。新的变量初始化为相应的来自该方法调用的参数值。当 lambda 表达式体执行完成时,lambda 参数立即停止存在。
7. 每次异常被 try 语句(14.20)的 catch 子句捕获时,异常参数被创建。
新的变量初始化为与该异常(11.3,14.18)关联的真正的对象。当与 catch 子句关联的块执行完成时,异常参数立即停止存在。
8. 由局部变量声明语句(14.4)声明的局部变量。
无论何时控制流进入一个块(14.2)或 for 语句(14.14),对于直接包含在该块或 for 语句中的局部变量声明语句中声明的每个局部变量,新的变量被创建。
局部变量声明语句可能包含初始化变量的表达式。但是,带有初始化表达式的局部变量不被初始化,直到声明该局部变量的局部变量声明语句被执行。(明确的赋值规则(16(明确的赋值))防止局部变量的值在其被初始化或以其他方式赋值之前被使用)。当块或 for 语句执行完成时,局部变量立即停止存在。
如果不是特殊情况,在执行局部变量声明语句时,总是可以认为局部变量被创建。例外的情况包括 switch 语句(14.11),其中控制可能进入一个块,但绕过局部变量声明语句的执行。但是,由于由明确的赋值规则(16(明确的赋值))强加的限制,因此,由此类绕过局部变量声明语句声明的局部变量不能在其被赋值语句(15.26)明确地赋值之前使用。
变量可以声明为 final。final 变量仅可以赋值一次。如果 final 变量被分配,则这是一个编译时错误,除非它在此赋值之前(16(明确的赋值))明确未分配。
一旦 final 变量被分配,它总是包含相同的值。如果 final 变量持有到一个对象的引用,则该对象的状态可以被在该对象上的操作改变,但该变量总是引用相同的对象。这也适用于数组,因为数组时对象;如果 final 变量持有到一个数组的引用,则该数组的组件可以被在该数组上的操作改变,但该变量总是引用相同的数组。
空 final 是声明缺少初始化器的 fianl 变量。
常变量是用常量表达式(15.28)初始化的基元类型或 String 类型的 final 变量。变量是否是常变量可能对类的初始化(12.4.1)、二进制兼容性(13.1,13.4.9)和明确的赋值(16(明确的赋值))有影响。
三种变量被隐式地声明为 final:接口字段(9.3)、是try-with-resources 语句(14.20.3)的资源的局部变量,以及 multi-catch 子句(14.20)的异常参数。uni-catch 子句的异常参数从不隐式声明为 final,但实际上可以是 final。
某些未声明为 fianl 的变量反而事实上被视为 final:
如果以下所有为 true,则声明符具有初始化器(14.4.2)的局部变量事实上是 final:
它未被声明为 final。
它从不在赋值表达式中(15.26)作为左手边出现。(注意,包含初始化器的局部变量声明符不是赋值表达式。)
它从不作为前缀或后缀自增或自减运算符(15.14,15.15)的操作数出现。
声明符缺少初始化器的局部变量事实上是 final,如果以下所有是 true:
它未被声明为 final。
无论何时它在赋值表达式中作为左手边出现,并且在赋值之前,它是明确未赋值且未明确赋值;即,在赋值表达式(16(明确赋值))的右手边之后,它是明确未被分配且未明确分配。
它从不作为前缀或后缀自增或自减运算符的操作数出现。
为了方便确定其事实上是否是 final,方法、构造器、lambda 或异常参数(8.4.1,8.8.1,9.4,15.27.1,14.20)被视为声明符具有初始化器的局部变量。
如果变量事实上是 final,向其声明添加 final 修饰符不会引入任何编译时错误。反之,在合法程序中声明为 final 的局部变量或参数将变成事实上 final,如果移除 final 修饰符。
程序中每个变量在其值被使用之前都必须有一个值:
每个类变量、实例变量或数组组件在创建时(15.9,15.10.2)用默认值初始化:
对于 byte 类型,默认值是零,即值 (byte) 0。
对于 short 类型,默认值是零,即值 (short) 0。
对于 int 类型,默认值是零,即 0。
对于 long 类型,默认值是零,即 0L。
对于 float 类型,默认值是正零,即 0.0f。
对于 double 类型,默认值是正零,即 0.0d。
对于 char 类型,默认值是 null 字符,即 '\u0000'。
对于 boolean 类型,默认值是 false。
对于所有引用类型(4.3),默认值是 null。
每个方法参数(8.4.1)初始化为相应的由方法调用者(15.12)提供的参数值。
每个构造器参数(8.8.1)初始化为由类实例创建表达式(15.9)或显式构造器调用(8.8.7)提供的相应的参数值。
异常参数(14.20)初始化为抛出的表示异常(11.3,14.18)的对象。
局部变量(14.4,14.14)在使用之前必须被显式地给定一个值,通过初始化(14.4)或赋值(15.26),以可以使用明确赋值规则(16(Definite Assignment))验证的方式。
在 Java 编程语言中,每个变量和每个表达式具有可以在编译时就确定的类型。类型可以基元类型或引用类型。引用类型包括类类型和接口类型。引用类型通过类型声明引入,其包括类声明(8.1)和接口声明(9.1)。我们通常使用术语类型来表示类或接口。
在 Java 虚拟机中,每个对象属于某些特定的类:在产生对象(15.9)的创建表达式中提及的类,或使用其 Class 对象以调用反射来产生对象的类,或通过字符串串联运算符 +(15.18.1)来隐式地创建对象的 String 类。这个类被称为这个对象的类。对象被称为其类和其类的所有父类的实例。
每个数组也具有类。方法 getClass,当数组对象调用时,将返回表示该数组(10.8)的类的类对象(Class 类)。
变量的编译时类型总是被声明,且表达式的编译时类型可以在编译时推导出。编译时类型限制了变量在运行时可以持有的或表达式在运行时可以产生的可能的值。如果运行时值是一个不是 null 的引用,则它引用一个具有一个类的对象或数组,且该类必须与编译时类型兼容。
尽管变量或表达式可以具有一个为接口类型的编译时类型,没有接口的实例。类型为接口类型的变量或表达式可以引用任何对象,其类实现了(8.1.5)该接口。
有时变量或表达式被称为具有“运行时类型”。这是指在运行时由该变量或表达式的值引用的对象的类,假定该值不是 null。
由于两个原因,编译时类型和运行时类型之间的对应是不完全的:
1. 运行时,类和接口由 Java 虚拟机使用类加载器来加载。每个类加载器定义了它自己的类和接口集合。因此,很有可能,两个类加载器加载同一类或接口定义,但在运行时产生不同的类或接口。反之,正确编译的代码可能在链接时失败,如果加载它的类加载器不一致。
See the paper Dynamic Class Loading in the Java Virtual Machine, by Sheng Liang and Gilad Bracha, in Proceedings of OOPSLA '98, published as ACM SIGPLAN Notices, Volume 33, Number 10, October 1998, pages 36-44, and The Java Virtual Machine Specification, Java SE 8 Edition for more details.
2. 类型变量(4.4)和类型参数(4.5.1)在运行时是不可具体化的。因此,相同的类或接口在运行时表示多个来自编译时的参数化类型(4.5)。特别地,所有编译时给定泛型类型(8.1.2,9.1.2)的参数化共享单个运行时表示形式。
在某些条件下,很有可能,参数化类型的变量引用不是该参数化类型的对象。这种情况被称为对污染(4.12.2)。该变量总是引用一个对象,其是表示该参数化类型的类的实例。