JAVA语言规范 JAVA SE 8 - 类型、值和变量
- 类型和值的种类
- 简单类型和值
- 整数类型和值
- 整数操作
- 浮点数类型、格式和值
- 浮点数操作
- boolean类型和布尔值
- 引用类型和值
- 对象
- Object 类
- String 类
- 当引用类型相同时
- 类型变量
- 参数化类型
- 参数化类型的类型引元
- 参数化类型的成员和构造器
- 类型擦除
- 可具化类型
- 原生类型
- 交集类型
- 子类型化
- 简单类型之间的子类型化
- 类与接口类型之间的子类型化
- 数组类型之间的子类型化
- 最低上边界
- 使用类型之处
- 变量
- 简单类型的变量
- 引用类型的变量
- 变量的种类
- final 变量
- 变量的初始值
- 类型、类和接口
Java编程语言是一种静态类型语言
,这意味着每个变量和每个表达式都具有在编译时就明确的类型。
Java编程语言还是一种强类型语言
,因为类型限制了变量可以持有的值和表达式可以产生的值,限定了在这些值上可以进行的操作,并且确定了这些操作的含义。强静态类型机制有助于在编译时探测错误
。
Java编程语言的类型可以分成两类:简单类型
和引用类型
。简单类型包括 boolean
类型和数字
类型,其中数字类型包括整数类型byte、short、int、long和char
, 以及浮点数类型float和double
。引用类型包括类类型
、接口类型
和数组类型
, 另外还有一个特殊的空类型
。对象
是动态创建的类类型的实例或者是动态创建的数组,而引用类型的值是对对象的引用
。所有对象,包括数组在内,都支持Object类的方法。字符串字面常量是用String对象表示的。
类型和值的种类
在Java编程语言中有两种类型:简单类型和引用类型。相应地, 有两种数据值
,它们可以被存储在变量中
,被当作引元传递
、作为返回值
,或者在其上执行操作
,它们就是简单值
和引用值
。
还有一种特殊的空类型
,即表达式null的类型,它没有 名字。
因为空类型没有名字,所以不能声明一个变量是空类型的
,也不能将变量类型转换为空类型
。
空引用
只能是空类型表达式的值。
空引用总是可以被赋值给或被类型转换为任何引用类型。
在实践中,程序员可以忽略空类型,只需认为空仅仅是一个特殊的字面常量,它可以具有任何引用类型。
简单类型和值
简单类型是Java编程语言预定义
,并且以其保留关键字来命名的
简单类型值 互相
之间并不共享状态
。
数字类型
包括整数
类型和浮点数
类型。
整数
类型包括byte
、short
、int
和long
,它们的值分别是8位、16位、32位和64位
有符号二进制补码表示
的整数;char
也是一种整数类型,它的值是16位无符号整数
,表示 UTF-16码元
。
浮点数
类型包括float
和double
,前者的值包括32位 IEEE 754浮点数
,而后者的值包括64位 IEEE 754浮点数
。
boolean
类型只有两个值:true
和false
。
整数类型和值
整数类型的取值范围分别如下:
对于byte
类型,是从-128到127的闭区间
。
对于short
类型,是从-32 768到32 767的闭区间
。
对于int
类型,是从-2 147 483 648到2 147 483647的闭区间
。
对于 long
类型,是从-9 223 372 036 854 775 808 到 9 223 372 036 854 775 807 的闭区间
。
对于char
类型,是从 ‘u0000
’ 到 ‘uffff
’ 的闭区间
,即从0 到 65 535
。
整数操作
Java编程语言提供了大量可作用于整数值的操作符
,包括:
1.比较操作符
,它们会产生boolean类型的值:
数字`比较`操作符 <、<=、> 和 >=
数字`判等`操作符==和!=
2.数字操作符
,它们会产生int或long类型的值:
一元加减操作符 + 和 -
乘除操作符 *、/ 和 %
加减操作符 + 和 -
递增操作符 ++,包括前缀式和后缀式
递减操作符 --,包括前缀式和后缀式
有符号和无符号移位操作符 << 、>> 和 >>>
位运算补码操作符 ~
整数位操作符 & 、^ 和 !
3.条件操作符
? :
。
4.类型转换操作符
,可以将整数值转换为任何指定的数字类型。
5.字符串连接操作符
+ ,当给定一个String类型操作数和一个整数操作数时,该操作符会将整数操作数转换为以十进制形式表示这个数值的String, 然后将两个字符串连接起来产生一个新的字符串。
在Byte
、Short
、Integer
、Long
和Character
类中还预定义了其他有用的构造器、方法和常量。
对于除移位操作符之外
的整数操作符,如果
其至少有一个操作数是long类型的
,那么该操作就会按照64位精度执行
,并且该数值操作符
的结果
也是long类型
。如果另一个操作数不是long类型的
,那么它首先会通过数字上下文被拓宽到 long类型
。
否则,该操作就会按照32位精度执行
,并且该数值操作符的结果
也是int类型
的。如果两个操作数其中之一不是int类型的,那么它首先会通过数字提升被拓宽到int类型
。
整数类型的任何值都可以通过类型转换
与其他任何数字类型进行双向转换
,但是在整数类型和boolean类型之间是无法转换的
。
整数操作符不会以任何方式提示上溢或下溢
。
整数操作符
会因为 下列原因
而 抛出异常
:
1.如果需要对空引用进行拆箱转换
,那么任何整数操作符都会抛出 NullPointerException
。
2.如果右操作数是0
,那么整数除法操作符 / 和整数取余操作符 % 都会抛出 ArithmeticException
。
3.如果需要进行装箱转换
,但是没有足够的内存可用来执行该转换
,那么递增和递减操作符 ++ 与 -- 就会抛出 OutOfMemoryError
。
整数操作代码示例:
class Test {
public static void main(String[] args){
int i = 1000000;
System.out.printin(i * i);
long l = i;
System.out.printin(l * l);
System.out.printin(20296 / (l - i));
}
}
这个程序会产生下面的输出:
-727379968
1000000000000
并且在执行到除以 l-i
时会产生ArithmeticException
异常,因为等于0。程序中第一 个乘法是按照32位精度执行的,而第二个乘法是一个long乘法。产生-727379968
这样的数值输出的原因是因为这个乘法的数学结果应该是1 000 000 000 000
,它对于int类型来说太大了,以至于产生了溢出,而-727379968
正是正确结果的低32表示的十进制数值
。
浮点数类型、格式和值
浮点数类型包括float和double,它们在概念上分别对应单精度32位和双精度64位格式的IEEE 754数值以及由IEEE二进制浮点数算术运算标准,即ANSI/IEEE 754标准754- 1985 (IEEE,纽约)指定的操作。
IEEE 754标准
不仅包含由一个符号位
和一个量值
构成的正负数字
,而且还包含正负0、 正负无穷
,以及特殊的“NaN”值
(表示非数字Not a Number)。NaN值用来表示某些无效操作的结果,例如0除0。float
和 double
的 NaN常量
被预定义为Float.NaN
和Double.NaN
。
Java编程语言的每个实现都需要支持浮点值的两个标准集
,即单精度浮点值集
(float value set)和双精度浮点值集
(double value set)。另外,任何Java编程语言的实现可以同时支持两个扩展指数浮点值集
,或者只支持其中之一
,它们是单精度扩展指数值集
(float- extended-exponent value set)和双精度扩展指数值集
(double-extended-exponent value set)。这些扩展指数值集可以在某些情况下替代标准值集
,用来表示类型为float和double的表达式的值。
任何浮点值集中的有限非0值全部都可以表示成
s
.
m
.
2
e
−
N
+
1
s.m.2^{e - N +1}
s.m.2e−N+1 的形式,其中s是 +1 或-1, m是小于
2
N
2^{N}
2N的正整数,e是位于从
E
m
i
n
=
−
(
2
K
−
1
−
1
)
E{_{min}}= -(2^{K-1} - 1)
Emin=−(2K−1−1) 到
E
m
a
x
=
2
K
−
1
−
1
E{_{max}}= 2^{K-1} - 1
Emax=2K−1−1 的闭区间中的整 数,其中N和K是依赖于所用的值集的参数。按照这种形式,某些值可能会有多种表示方式。例如,假设值集中某个值v可以用s、m和e的恰当取值来按照这种形式表示,那么当m是偶数,e小于
2
K
−
1
2^{K-1}
2K−1 时会怎样呢?我们可以将m取值减半,然后给e增加1,这样就会产生v的第二种表示。在符合这种形式的表示中,如果m>=
2
K
−
1
2^{K-1}
2K−1,那么这种表示就称为规格化
的表示,否则,这种表示就称为非规格化
的表示。如果值集中的某个值不能按照m>=
2
K
−
1
2^{K-1}
2K−1的 方式来表示,那么这个值就称为非规格化值
,因为它没有任何规格化的表示。
对于两个必须支持的值集,以及两个扩展的浮点值集来说,在参数N和K上的约束(以及在推导出的参数 E m i n E{_{min}} Emin 到 E m a x E{_{max}} Emax上的约束)如下表所示。
表4-1 浮点值集参数
参数 | 单精度 | 单精度扩展指数 | 双精度 | 双精度扩展指数 |
---|---|---|---|---|
N | 24 | 24 | 53 | 53 |
K | 8 | >=11 | 11 | >=15 |
E m i n E{_{min}} Emin | +127 | >=+1023 | +1023 | >=-16383 |
E m a x E{_{max}} Emax | -126 | <=-1022 | -1022 | <=-16382 |
如果某个Java实现支持一个或同时支持两个扩展指数值集,那么对于所支持的每一个 扩展指数值集,都有一个依赖于该实现的特定的常数K,它的值由 上表 限定,而这个值也就决定了 E m i n E{_{min}} Emin 和 E m a x E{_{max}} Emax 的值。
这四个值集每一个都不仅包含上述有限非0值,并且还包含NaN值和四个分别表示正 0、负0、正无穷和负无穷的值。
需要注意的是,表4-1中的约束条件的设计方式,是为了使得单精度浮点值集的每个元素也必然都是单精度扩展指数值集、双精度浮点值集和双精度扩展指数值集的元素
。类似地,双精度浮点值集的每个元素也必然都是双精度扩展指数值集的元素
。每个扩展指数值集的指数取值范围都比对应的标准值集要大,但是其精度并不会提高
。
单精度浮点值集的元素就是可以用IEEE 754标准中定义的单精度浮点数格式表示的值, 而双精度浮点值集就是可以用IEEE 754标准中定义的双精度浮点数格式表示的值。但是要注意,这里定义的单精度浮点扩展指数值集和双精度浮点扩展指数值集并没有分别对应于可以用IEEE 754单精度扩展和双精度扩展格式表示的值
。
单精度浮点、单精度扩展指数浮点、双精度浮点和双精度扩展指数浮点值集都不是类型
。对于Java编程语言的实现来说,使用单精度浮点值集的元素来表示类型的值总是 正确的。但是,在某些特定的代码部分使用单精度扩展指数浮点集的元素来替换也是允许的。类似地,对于Java编程语言的实现来说,使用双精度浮点值集的元素来表示double类型的值总是正确的。但是,在某些特定的代码部分使用双精度扩展指数浮点集的元素来替换也是允许的。
除了 NaN, 浮点数都是有序的
,按照从小到大排列
,分别是负无穷、负有穷非0值、 正0和负0、正有穷非0值,以及正无穷
。
IEEE 754允许为NaN定义多个不同的值
,每一种都有单精度和双精度两种浮点格式。 尽管在产生新的NaN时,每一种硬件架构都会返回其特有的NaN位模式
,但是程序员还是 可以创建具有不同位模式的NaN
,用于对诸如回归诊断信息
等内容进行编码。
基本上,JavaSE平台会将给定类型的NaN值当作单一的规范值处理,因此本规范通常会引用任意一个NaN值当作是对这个规范值的引用。
但是,Java SE的1.3版本中定义了可以让程序员对NaN值进行区分的方法,即: Float .floatToRawIntBits
和 Double. doubleToRawLongBits
方法。感兴趣的读者可以参考 Float类和Double类的规范以了解更多信息。
正0和负0相比较结果
是相等
,因此表达式0.0==-0.0
的结果是true,而0.0>-0.0
的 结果是false。但是其他操作可以区分正0和负0
,例如,1.0/0.0的值是正无穷
,但是 1.0/-0.0的结果是负无穷
。
NaN是无序的
,因此:
1.如果操作数中有一个或两个同时是NaN,则数字比较操作符 <、<=、> 和>=返回 false。
2.如果操作数中有一个是NaN,则判等操作符 == 返回false。
特别地,如果x或y是NaN,则(x<y) ==
!(x>=y)为false。
3.如果操作数中有一个是NaN,则判不等操作符 != 返回true 。
特别地,当且仅当x是NaN,则x != x 为true。
浮点数操作
Java编程语言提供了大量可作用于浮点值的操作符,包括:
1.比较操作符
,它们会产生boolean类型的值:
数字比较操作符 <、<=、> 和 >=
数字判等操作符 == 和 !=
2.数字操作符
,它们会产生float或double类型的值:
一元正负操作符+和-
乘除操作符*、/和%
加减操作符+和-
递增操作符++,包括前缀式和后缀式
递减操作符--,包括前缀式和后缀式
3.条件操作符
?:。
4.类型转换操作符
,可以将浮点值转换为任何指定的数字类型
。
5.字符串连接操作符
+,当给定一个String类型操作数和一个浮点操作数时,该操作符会将浮点操作数转换为以十进制形式(不丢失任何信息)表示这个数值的String,然后将两个字符串连接起来产生一个新的字符串。
在Float, Double和Math类中还预定义了其他的一些构造器、方法和常量。
如果二元操作符的操作数至少有1个是浮点类型的,那么该操作就是浮点操作,即使另一个操作数是整数也是如此。
如果数值操作符的操作数至少有一个是double类型的,那么该操作就会按照64位浮点算术运算执行,并且该数值操作符的结果也是double类型的。如果另一个操作数不是 double类型的,那么它首先会通过数字提升被拓宽
到double类型。
否则,该操作就会按照32位浮点算术运算执行,并且该数值操作符的结果也是float类型的。如果另一个操作数不是float类型的,那么它首先会通过数字提升被拓宽
到float类型。
浮点类型的任何值都可以通过类型转换
与其他任何数字类型进行双向转换,但是在浮点类型和boolean类型之间是无法转换的
。
作用于浮点数的操作符将按照IEEE 754规定的方式执行(只有取余操作符例外)。特别地,Java编程语言要求支持IEEE 754的非规格化浮点数
和渐进下溢
,这 可以使得对特定的数值算术运算所具有属性的验证变得更加容易。如果浮点操作的运算结果是非规格化数,它不会进行“置0 (flush to zero)
"操作。
Java编程语言要求浮点算术运算
的行为必须遵循这样的规则
,即每个浮点操作符都会将其浮点运算结果舍入到所需的精度
,这个不精确的结果必须是无限精确的结果经过舍入得到的最接近于它的可表示值
。如果经舍入后,有两个最接近的可表示值与无限精确的结果的距离是相等的,那么就会选择最低有效位为0的那个可表示值
。这就是IEEE 754标准的缺省舍入模式
,称为“最近舍入
”。
Java编程语言在将浮点值向整数转换
时,会使用向0舍入的模式
,在这种情况下,会将数字截尾,即丢弃尾部的位
。向0舍入会对结果进行选择,其结果的格式所表示的值在量级上最接近并且不大于无限精确的结果。
上溢的浮点操作会产生一个有符号的无穷。
下溢的浮点操作会产生一个非规格化的值或有符号的0。
其结果没有数学定义的浮点操作会产生一个NaN。
操作数中有NaN的所有数值操作都会产生一个NaN作为结果。
浮点操作符
会因为 下列原因
而 抛出异常
:
1.如果需要对空引用进行拆箱转换
,那么任何浮点操作符都会抛出 NullPointerException
。
2.如果需要进行装箱转换
,但是没有足够的内存
可用来执行该转换,那么递增和递减 操作符++ 与--
就会抛出 OutOfMemoryError
。
浮点操作范例
class Test {
public static void main(String[] args) {
// An example of overflow:
double d = 1e308;
System.out.print("overflow produces infinity:");
System.out.printin(d + "*10==" + d*10);
// An example of gradual underflow:
d = 1e-305 * Math.PI;
System.out.print("gradual underflow: " + d + "n
");
for (int i = 0; i < 4; i++)
System.out.print(" "+ (d /= 100000));
System.out.printin();
// An example of NaN:
System.out.print("0.0/0.0 is Not-a-Number:");
d = 0.0/0.0;
System.out.printin(d);
//An example of inexact results and rounding:
System.out.print("inexact results with float:");
for (int i = 0; i < 100; i++) {
float z = 1.0f / i;
if (z * i != 1.0f)
System.out.print(" " + i);
}
System.out.printin();
// Another example of inexact results and rounding:
System.out.print("inexact results with double:");
for (int i = 0; i < 100; i++) {
double z = 1.0 / i;
if (z * i != 1.0)
System.out.print(" " + i);
}
System.out.printin();
// An example of cast to integer rounding:
System.out.print("cast to int rounds toward 0:");
d = 12345.6;
System.out.printin((int)d + " " + (int)(-d));
}
}
这个程序会产生下面的输出:
overflow produces infinity: 1.0e+308*10==Infinity
gradual underflow: 3.141592653589793E-305
3.1415926535898E-310 3.141592653E-315 3.142E-320 0.0
0.0/0.0 is Not-a-Number: NaN
inexact results with float: 0 41 47 55 61 82 83 94 97
inexact results with double: 0 49 98
cast to int rounds toward 0: 12345 -12345
这个示例说明,和其他操作相比,渐进下溢会产生精度的渐进丢失
。
当i为0时,其结果涉及被0除,因此z会变成正无穷,而z*0会变成并不等于1.0的NaN。
boolean类型和布尔值
boolean类型表ZK―种逻辑量,它有两种可能的取值,由字面常量true和false表示。
布尔操作符包括:
1.判等操作符
== 和 !=。
2.逻辑取反操作符
!。
3.逻辑操作符
&、^ 和 !。
4.条件与
和 条件或操作符
&& 和 || 。
5.条件操作符
?:。
6.字符串连接操作符
+ ,当给定一个String类型操作数和一个boolean 操作数时,该操作符会将boolean操作数转换为String ( true或false),然后将两个字符串连接起来产生一个新的字符串。
布尔表达式在下列几种语句中可用来确定控制流:
if语句
while语句
do语句
for语句
boolean表达式还可以确定条件操作符 ?:中哪一个子表达式会用来计算。
只有boolean或Boolean表达式才能用于控制流语句和用作条件操作符 ?:的第一个操作数。
整数或浮点表达式x可以通过x!=0这样的表达式转换为boolean值,这遵循了C语言的习惯,即非0的值表示true。
对象引用obj可以通过obj !=null这样的表达式转换为boolean值,这遵循了C语言的习惯,即任何除null之外的引用都是true。
boolean值可以通过字符串转换转换为String值
。
将boolean值类型转换为boolean、Boolean和Object类型是允许的
,而对boolean类型做其他任何类型转换都是不允许的
。
引用类型和值
Java有四种引用类型:类类型、接口类型、类型变量和数组类型。
下面的样例代码:
class Point { int[] metrics; }
interface Move{ void move(int deltax, int deltay); }
声明了一个类类型Point, 一个接口类型Move,并且用一个数组类型int[](整型数组)声 明 了 Point 类的 metrics 域。
类或接口类型由一个标识符或由圆点分隔的标识符序列构成,其中每个标识符后面都可选地包含类型引元
。如果类型引元在类或接口类型的任何地方出现,那么该类 型就是一个参数化类型
。
在类或接口类型中的每个标识符都会被当作包名或类型名,其中被当作类型名的标识符可以被注解。如果某个类或接口类型的形式为T.id (后面可选地跟着类型引元),那么id必须是T的可访问成员类型的简单名,否则就会产生编译时错误,而该类或接口类型表示的就是该成员类型。
对象
对象是类的实例或数组
。
引用值
(经常直接简称“引用
”)是指向这些对象的指针
,并且有一个特殊的空引用
, 它不指向任何对象
。
类实例
是显式地通过类实例创建表达式
创建的。
数组
是显式地通过数组创建表达式
创建的。
如果字符串连接操作符+
用于非常量
表达式中,那么就 会隐式地创建一个新的类实例
,从而产生一个string类型的新对象
。
如果对数组初始化器表达式求值
,那么就会隐式地创建一个新的数组对象
。这会发生在下列情况中:初始化类或接口,创建类的新实例, 以及执行局部变量声明语句。
Boolean、Byte、Short、Character、Integer、Long、Float 和 Double 类型的新对象可以通过装箱转换
被隐式地创建
。
对象创建
class Point {
int x, y;
Point() { System.out.printin("default"); }
Point(int x, int y) {this.x = x; this.y = y; }
/* A Point instance is explicitly created at
class initialization time: */
static Point origin = new Point(0, 0);
/* A String can be implicitly created by a *
operator: */
public String toString() { return "(" + x + "," + y +")" }
}
class Test{
public static void main(String[] args) {
/* A Point is explicitly created
using newlnstance: */
Point p = null;
try {
p = (Point)Class.forName("Point"). newlnstance();
} catch(Exception e){
System.out.printin(e);
}
/* An array is implicitly created
by an array constructor: */
Point a[] = { new Point(0,0), new Point(1,1) };
/* Strings are implicitly created
by + operators: */
System.out.prihtln("p: " + p);
System.out.printin("a: { " + a[0] + ", " + a[l] + " }" );
/* An array is explicitly created
by an array creation expression: */
String sa[] = new String[2];
sa[0] ="he"; sa[l] ="llo";
System.out.printIn(sa[0] + sa [1]);
}
}
这个程序会产生下面的输出:
default
p:(0,0)
a: { (0,0), (1,1) }
hello
作用于对象引用的操作符有:
1.使用限定名
或域访问表达式
的域访问
。
2.方法调用
。
3.类型转换操作符
。
4.字符串连接操作符
+,当给定一个String类型操作数和一个引用时, 该操作符会调用被引用对象的tostring方法,将引用转换为String (如果该引用或 toString的结果是空引用,则使用“null”),然后将两个字符串连接起来产生一个新的字符串。
5.instanceof 操作符
。
6.引用判等操作符
== 和 !=。
7.条件操作符
?:。
多个引用可以指向同一个对象
。绝大多数对象都具有状态
,这些状态存储在对象的域中
,而这些对象是类的实例;或者存储在变量中
,而这些变量是数组对象的成员。如果两个变量包含指向同一个对象的引用,那么当通过其中一个变量的对象引用修改该对象的状态时,通过另一个变量中的引用可以观察到变化后的状态
。
简单标识和引用标识
class Value { int val; }
class Test {
public static void main(String[] args) {
int i1 = 3;
int 12 = i1;
i2 = 4;
System.out.print("i1==" + i1);
System.out.println(" but i2==" + i2);
Value v1 = new Value();
v1.val = 5;
Value v2 = v1;
v2.val = 6;
System.out.print("vl.val==" + v1.val);
System.out.println(" and v2.val==" + v2.val);
}
}
这个程序会产生下面的输出:
i1==3 but i2==4
v1.val==6 and v2.val==6
因为v1.val和v2.val引用的是由唯一的new表达式创建的Value对象里的同一个实例变量,而i1和i2是不同的变量。
每个对象都与一个监视器
关联,它会被synchronized方法
和 synchronized语句
用来对多线程并发访问对象状态
进行控制。
Object 类
Object类是所有其他类的超类。
所有类和数组类型都继承了Object类的方法,具体包括:
1.Clone方法
,用来创建对象的副本。
2.equals方法
,定义了对象判等的标准,该标准应该是基于值比较而不是引用比较的。
3.finalize方法
,在对象销毁前运行。
4.getClass方法
,返回表示对象所属类的Class对象。
对于每一个引用类型都存在Class对象。它有很多用处,例如用来发现某个类的完全限定名、它的成员、它的直接超类,以及它实现的所有接口。
getClass的方法调用表达式的类型是Class<? extends |T|>, 其中T是为getClass搜索到的类或接口。
被声明为synchronized的类方法
会在与该类的Class对象相关联的监视器上同步
。
5.hashcode方法
,它与equals方法一起,在诸如java.uti l.Hashmap这样的散列表中非常有用。
6.wait、notify和notifyAll方法
,在使用线程的并发编程中会用到它们。
7.toString方法
,返回对象的String表示。
String 类
String类的实例表示Unicode码位序列。
每个String对象都有常量值(不可修改)。
字符串字面常量是对String类的实例的引用。
如果字符串连接操作符运算的结果不是编译时的常量表达式
,那么该操作符会隐式地创建新的String对象
。
当引用类型相同时
如果两个引用类型具有相同的二进制名字
,并且如果它们有类型引元
的话, 通过递归地运用本定义
,也可以认为它们是相同
的,那么这两个引用类型就是相同的编译时类型
。
如果两个引用类型是相同的,有时我们会称其为同一个类
或同一个接口
。
在运行时
,若干个具有相同的二进制名字的引用类型可以被不同的类加载器同时加载
。 这些类型可以表示也可以不表示相同的类型声明
,而即便这样的两个类型表示相同的类型声明,它们也会被认为是有区别的
。
如果满足下列条件
,则两个引用类型
就是同一个运行时类型
:
1.它们都是类或接口类型
,由相同的类加载器定义
,并且有相同的二进制名字
,此时它们也被称为是同一个运行时类或同一个运行时接口。
2.它们都是数组类型
,并且其成员的类型也都是同一个运行时类型
。
类型变量
类型变量是在类、接口、方法和构造器中用作类型的非限定标识符。
类型变量可以声明
为泛化类声明
、泛化接口声明
、泛化方法声明
和泛化构造器声明
中的类型参数。
类型变量声明为类型参数时,其作用域在第6.3节中说明。
每个声明为类型参数
的类型变量
都有一个边界
。如果对某个类型变量没有声明任何边界,则默认其边界为Object。如果声明了边界,则它由下列两种情况构成:
1.单个类型变量T。
2.类或接口类型T,后面可能还有接口类型
I
1
α
−
I
1
n
I{_1} α- I{_1}n
I1α−I1n
如果
I
1
−
I
n
I{_1} - I{_n}
I1−In类型中任何一个是类类型或类型变量,那么就会产生一个编译时错误
。
对于边界来说,其所有构成类型的擦除
必须都是互不相同的,否则就会产 生一个编译时错误
。
如果两个接口类型是同一个泛化接口的不同参数化版本,那么类型变量就不能同时是这两个接口类型的子类型,否则就会产生一个编译时错误。
类型在边界中的顺序
只在两种情况下才显得重要,第一种是类型变量的擦除取决于边界中的第一个类型
;第二种是类类型或类型变量只能出现在第一个类型的位置
。
边界为 I 1 α − I 1 n I{_1} α- I{_1}n I1α−I1n 的类型变量X的成员,是由位于类型变量声明处的交集类型 I 1 α − I 1 n I{_1} α- I{_1}n I1α−I1n 的成员构成的。
类型变量的成员
package TypeVarMembers;
class C{
public void mCPublic() {}
protected void mCProtected() {}
void mCPackage() {}
private void mCPrivate() {}
}
interface I {
void mI();
}
class CT extends C implements I {
public void mI() {}
}
class Test {
<T extends C & I> void test(T t) {
t.mI(); // OK
t.mCPublic(); // OK
t.mCProtected(); // OK
t.mCPackage(); // OK
t.mCPrivate(); // Compile-time error
}
}
类型变量T有交集类型C & I,
也就是与用等价的超类在相同的作用域中声明的空类CT具有相同的成员。接口的成员总是public的,因此总是可以被继承(除非被覆盖),所以mH 是CT和T的成员。在C的成员中,除了 mCPrivate之外的成员都被CT继承了,因此也就是 CT和T的成员。
如果C与T在不同的包中,那么对mCPackage的调用将会产生一个编译时错误,因为这个成员在T被声明的地方是不可访问的。
参数化类型
泛化类
或泛化接口的声明
定义了一个参数化类型集
。
参数化类型是形式
为
C
<
T
1
,
…
,
T
n
>
C<T_{1} , … ,T_{n} >
C<T1,…,Tn>的类或接口,其中C是泛型名
,而
<
T
1
,
…
,
T
n
>
<T_{1} , … ,T_{n} >
<T1,…,Tn>是表示该泛型的特定参数化形式的类型引元列表
。
泛型带有类型参数
F
1
,
…
,
F
n
F_{1} , … ,F_{n}
F1,…,Fn,并且这些类型参数还带有相应的边界
B
1
,
…
,
B
n
B_{1} , … ,B_{n}
B1,…,Bn 。参数化类型的每个类型引元
T
1
T_{1}
T1 都限定在相应的边界中列出的所有类型的任意子类型的范围内
。也就是说,对于
B
i
B_{i}
Bi中的每一个边界类型S,
T
1
T_{1}
T1是
S
[
F
1
:
=
T
1
,
…
,
F
n
:
=
T
n
]
S[F_{1}:=T_{1},… ,F_{n}:=T_{n}]
S[F1:=T1,…,Fn:=Tn] 的子类型。
如果满足下列条件,那么参数化类型
C
<
T
1
,
…
,
T
n
>
C<T_{1} , … ,T_{n} >
C<T1,…,Tn> 就是良构的:
1.C是泛型名。
2.类型引元的数量与在C的泛化声明中的类型参数的数量相同。
3.当经过捕获转换
并产生类型
C
<
X
1
,
…
,
X
n
>
C<X_{1} , … ,X_{n} >
C<X1,…,Xn> 后,对于每一个在
B
i
B_{i}
Bi中的边界类型S,每一个类型引元
X
i
X_{i}
Xi, 都是
S
[
F
1
:
=
X
1
,
…
,
F
n
:
=
X
n
]
S[F_{1}:=X_{1},… ,F_{n}:=X_{n}]
S[F1:=X1,…,Fn:=Xn] 的子类型。
如果一个参数化类型不是良构的,那么就会产生一个编译时错误
。
在本规范中,无论何时提到类或接口类型,都包括泛化版本,除非明确将其排除。
如果两个参数化类型
满足下列两个条件
之一,就被认为是可证不同的
:
1.它们是对不同的泛型定义的参数化版本。
2.它们的任何类型引元都是可证不同的。
下面是良构的参数化类型的示例:
Seq<String>
Seq<Seq<String>>
Seq<String>.Zipper<Integer>
Pair<String,Integer>
下面是泛型的不正确参数化的示例:
Seq<int>不合法,因为简单类型不能当作类型引元。
Pair<String>不合法,因为类型引元数量不够。
Pair<String, String, String>不合法,因为类型引兀数量过多。
参数化类型
可以是对嵌套的泛化类或接口的参数化版本
。例如,如果非泛化类C有一个泛化成员类D< T>,那么C.D< Object>
就是一个参数化类型。并且如果泛化类C< T>有一个非泛化成员类D,那么成员类型C< String>.D
就是一个参数化类型,尽管类D不是泛化类。
参数化类型的类型引元
类型引元
可以是引用类型
或通配符
。如果只要求提供类型参数的部分信息,那么通配符就很有用了。
就像常规类型变量声明一样,可以指定通配符的明确边界
。上界
是通过下面的语法指定的,其中B是边界
:
? extends B
与在方法签名中声明的普通类型变量不同,当使用通配符时,不需要任何类型推断
。因此,通过下面的语法来声明通配符的下界
是允许的,其中B是下界
:
? super B
通配符 ? Extends Object
等价于
无界通配符?
。
如果两个类型引元
满足下列条件之一
,就被认为是可证不同
的:
1.引元既不是类型变量,也不是通配符,并且两个引元不是相同的类型
。
2.其中一个类型引元是类型变量或通配符,且具有上界(来自捕获转换,如果这种转换是必需的)S
; 而另一个类型引元T
不是类型变量或通配符,并且|S|<:|T|
和 |T|<:|S|
都不成立。
3.两个类型引元都是类型变量或通配符
,且具有上界
(来自捕获转换,如果这种转换是必需的)S和T, 并且|S|<:|T|
和 |T|<:|S|
都不成立。
在下列规则(其中 <: 表示子类型
)的 自反
和传递闭包
的范围内,如果
T
2
T_{2}
T2所表示的类型集可证是
T
1
T_{1}
T1所表示的类型集的子集,那么我们就认为类型引元
T
1
T_{1}
T1包含另一个类型引元
T
2
T_{2}
T2,记为
T
2
T_{2}
T2 <=
T
1
T_{1}
T1:
? extends T<=? extends S 如果 T<: S
? extends T<=?
? super T<=? super S 如果 S<:T
? super T<=?
? super T<=? extends Object
T<=T
T<=? extends T
T<=? super T
通配符
与现有建立起来的类型理论
之间的关系
非常有趣,这里简要描述
一下:通配符是存在类型(existential type)的严格形式
,对于泛型声明G<T extends B>, G<?>
大体上相当于 Some X<:B. G<X>
。
历史上,通配符是对Atsushi Igarashi
和Mirko Viroli
的研究工作的直接继承,如果读者对更全面的关于此话题的讨论感兴趣,可以参阅收录在第16届欧洲面向对象编程大会 (European Conference on Object Oriented Programming, ECOOP 2002
)论文集中的 Atsushi Igarashi
和 Mirko Viroli
撰写的《On Variance-Based Subtyping for Parametric Types
》一文。这项研究工作本身也是构建于更早的研究工作之上的,包括Kresten Thorup
和Mads Torgersen
的研究工作(Unifying Genericity, ECOOP 1999
)和长久以来一直进行的对基于声明的变量的研究工作,后者可以追溯到Pierre America
对POOL
的研究工作(OOPSLA 1989
)。
通配符与前面提到的论文中描述的结构在某些细节上有所区别,特别是使用了捕获转换
,而不是Igarashi
和Viroli
描述的close操作
。对于通配符的形式化描述,可 以参阅 Mads Torgersen、Erik Ernst 和 Christian Plesner Hansen
在第 12 届面向对象语言基础 的研讨会(FOOL 2005
)上发表的《Wild FJ
》一文。
无边界的通配符
import java.util.Collection;
import java.util.ArrayList;
class Test {
static void printCollection(Collection<?> c){
// a wildcard collection
for(Object o : c) {
System.out.printin(o);
}
}
public static void main(String[] args){
Collection<String> cs = new ArrayList<String>();
cs.add("hello");
cs.add("world");
printCollection(cs);
}
}
注意 如果使用Collection<Object>
作为输入参数c的类型,那么这个方法的用处就会显得十分有限,因为它将只能通过具有Collection<Object>
类型的引元表达式来调用, 而这种情况又非常罕见。相反,如果使用无边界的通配符,将使得任意类型的Collection集合都可以用作参数。
下面的示例中,数组的元素类型就是通过通配符参数化的:
public Method getMethod(Class<?>[] parameterTypes) { ... }
有边界的通配符
boolean addAll(Collection<? extends E> c)
这个方法声明在接口 Collection<E>
的内部,并且被设计为特输入引元中的所有元素添加到被调用该方法的集合对象中。一种很自然会想到的方法是使用Collection<E>
作为c的类型,但其实并不需要如此严苛。
另一种方法是将该方法本身声明为泛化
的:
<T> boolean addAll(Collection<T> c)
这个版本足够灵活,但是要注意,类型参数在签名中只用了一次
。这反映了这样一个事实,即类型参数并未用来表示在引元类型与返回类型或抛出异常类型之间的任何相互依赖关系
。当并不存在这种相互依赖性时,泛化方法就并非好的选择,而使用通配符将是首选
。
Reference(T referent, ReferenceQueue<? super T> queue)
其中referent可以被插入到任何队列中,只要该队列的元素类型是referent的类型T 的超类型即可,而T就是通配符的下界
。
参数化类型的成员和构造器
假设C是具有类型参数
A
1
,
…
,
A
n
A_{1} , … ,A_{n}
A1,…,An的泛化类或接口声明,而
C
<
T
1
,
…
,
T
n
>
C<T_{1} , … ,T_{n} >
C<T1,…,Tn>是对C的参数化版 本,其中1<=i<=n,
T
1
T_{1}
T1是类型(而不是通配符),那么:
1.如果m是C中的成员或构造器声明,其声明的类型是T, 那么,m在
C
<
T
1
,
…
,
T
n
>
C<T_{1} , … ,T_{n} >
C<T1,…,Tn>中的类型就是
T
[
A
1
:
=
T
1
,
…
,
A
n
:
=
T
n
]
T[A_{1}:=T_{1},… ,A_{n}:=T_{n}]
T[A1:=T1,…,An:=Tn] 。
2.如果m是D中的成员或构造器声明,其中D是C扩展的类或实现的接口,并且如果
D
<
U
1
,
…
,
U
n
>
D<U_{1} , … ,U_{n} >
D<U1,…,Un>是
C
<
T
1
,
…
,
T
n
>
C<T_{1} , … ,T_{n} >
C<T1,…,Tn> 对应于D的超类型,那么,m在
C
<
T
1
,
…
,
T
n
>
C<T_{1} , … ,T_{n} >
C<T1,…,Tn>中的类型就是 m在
D
<
U
1
,
…
,
U
n
>
D<U_{1} , … ,U_{n} >
D<U1,…,Un>中的类型。
如果C的参数化版本中任意一个或多个类型引元是通配符,那么:
1.在
C
<
T
1
,
…
,
T
n
>
C<T_{1} , … ,T_{n} >
C<T1,…,Tn>中的域、方法和构造器的类型就是在
C
<
T
1
,
…
,
T
n
>
C<T_{1} , … ,T_{n} >
C<T1,…,Tn>的捕获转换中的域、方法和构造器的类型。
2.如果D是C中的类或接口(可能是泛化的)声明,那么D在
C
<
T
1
,
…
,
T
n
>
C<T_{1} , … ,T_{n} >
C<T1,…,Tn>中的类型就是 D,其中,如果D是泛化的,那么所有类型引元都是无界通配符。
这一点并不重要,因为不可能在不执行捕获转换的情况下访问参数化类型的成员,并且也不可能在类实例创建表达式中在new关键字之后使用通配符类型。
前面这段话的唯一例外是将嵌套的参数化类型用作instanceof操作符中的表达式,这时捕获转换是不可用的。
在泛型声明中声明的static成员必须使用对应于该泛型的非泛型来引用,否则就会产生一个编译时错误
。
换句话说,使用参数化类型来引用在泛型声明中声明的static成员是非法的
。
类型擦除
类型擦除是一种映射
,即将类型
(可能包含参数化类型和类型变量)映射为
(不再是参数化类型或类型变量的)类型
。我们用|T|来表示类型T的擦除
。擦除映射的定义
如下:
1.参数化类型
G
<
T
1
,
…
,
T
n
>
G<T_{1} , … ,T_{n} >
G<T1,…,Tn>的擦除是|G|
2.嵌套类型T.C的擦除是|T|.C
3.数组类型T[]的擦除是|T| []
4.类型变量的擦除是其最左边界的擦除
5.每种其他类型的擦除都是该类型自身
类型擦除也会将构造器或方法的签名映射为没有任何参数化类型或类型变量的签名。构造器或方法签名s的擦除还是一个签名:它由与s相同的名字以及所有在s中给定的形参类型的擦除构成。
如果一个构造器或方法的签名被擦除,那么该方法的返回类型,或该泛化方法或构造器的类型参数也都要被擦除。
在泛化方法签名的擦除中,是没有任何类型参数的。
可具化类型
因为有些类型信息在编译时会被擦除,所以并非所有类型都是在运行时可用的。在运行时完全可用的类型被称为“可具化类型(reifiable type)
”。
一个类型当且仅当满足
下列条件之一
时,才是可具化类型
:
1.它引用的是非泛化的类或接口类型的声明。
2.它是参数化类型,其中所有类型引元都是无界的通配符。
3.它是原生类型。
4.它是简单类型。
5.它是数组类型,且其元素类型是可具化的。
6.它是嵌套类型,且其中用分隔开的每一个类型T都是可具化的。
例如,如果泛化类X<T>
有一个泛化成员类Y<U>
,那么类型X<T>.Y<?>
就是可具化的, 因为X<?>
是可具化的,同时Y<?>
也是可具化的。类型X<?>.Y<Object>
不是可具化的,因 为Y<Object>
不是可具化的。
交集类型不是可具化的。
不使所有泛型都是可具化
的这个设计决定,是Java编程语言的类型系统中最重要也是最具争议性的设计决定之一。
最终导致做出这个设计决定的最重要的动因就是与现有代码的兼容性。从最朴素的意义 上讲,添加像泛型这样的新结构对之前已有的代码是没有意义的。对于Java编程语言来说, 用之前版本编写的所有程序在新版本中都维持之前的意义,那么在本质上它就是与之前版本兼容的。但是,这个概念,即按照术语可称为"语言兼容性
”,只具有纯理论上的意义。真正的程序(即使是很简单的程序,例如“Hello World”)由若干个编译单元构成,其中有些是 Java SE平台提供的(例如java. lang和java.util中的元素)。因此,实际上兼容性的最低要求就是平台兼容性
,即用Java SE之前版本编写的所有程序在新版本中要保证其功能不变。
提供平台兼容性的一种方式是保持现有平台的功能不变,只添加新的功能
。例如,不去修改java.util中现有集合类型的层次结构,而只是利用泛型引入新的类库。
这种模式的缺点
是集合类库之前已有的客户端程序向新类库进行迁移会变得异常困难
。 集合用来在独立开发的模块之间交换数据,如果其中一个模块的提供者决定转换到新的泛化类库上,那么该提供者必须发布两个版本的代码,以便兼容与这个模块交换数据的客户端程序。依赖于其他提供者代码的类库不能修改为使用泛型,除非其提供者的类库也做出了更新。如果两个模块互相依赖,那么就必须同时进行这种修改。
很明显,平台依赖性,正如上面概述的那样,并不能提供现实可行的途径,去接受诸如泛型这类需要普及的新特性。因此,泛型系统的设计转而寻求支持迁移兼容性
。迁移兼容性允许现有代码演化为可以使用泛型,但是不会在彼此独立开发的软件模块之间强加任何依赖性。
迁移兼容性的代价是不能创建一个完全可具化的泛型系统,至少是在迁移正在发生时是不可能的
。
原生类型
为了促进与非泛化遗留代码之间的交互,可以将参数化类型的擦除或元素类型为参数化类型的数组类型的擦除当作类型使用。这种类型被称为原生类型
。
更准确地,下列类型都被定义
为原生类型:
1.引用类型,且其采用了某种泛型声明的名字,但是没有伴随任何类型引元列表。
2.数组类型,且其元素类型为原生类型。
3.具有原生类型R的非静态成员类型,且其并非从R的超类型或超接口继承而来。
非泛化的类或接口类型不是原生类型。
要了解为什么具有原生类型的非静态类型成员被认为是原生的,可以考虑下面的示例:
class Outer<T>{
T t;
class Inner {
T setOuterT(T tl) {
t = tl;
return t;
}
}
}
Inner的成员的类型依赖于Outer的类型参数。如果Outer是原生的,则Inner必须也当作原生看待,因为T没有任何有效的绑定。
这条规则只作用于不是继承而来的类型成员
,而继承而来的依赖于类型变量的类型成员,作为原生类型的超类型将被擦除这条规则的应用结果,将作为原生类型被继承白本节稍 后会对此进行阐述。
上面这些规则的另一层含义是,具有原生类型的泛化内部类自身可以只用作原生类型
:
class Outer<T>(
class Inner<S> {
S s;
}
}
在上面的示例中,不能将inner当作部分原生的类型(“稀有"类型)进行访问:
Outer.Inner<Double> x = null; // illegal
Double d = x.s;
因为Outer自身是原生的,因此其所有内部类,包括Inner在内,也都是原生的,所以不能将任何类型引元传递给Inner。
原生类型的超类(或超接口)是泛型的任意参数化版本的超类(或超接口)的擦除。
构造器的类型、实例方法的类型,以及并非从超类或超接口继承而来的具有原生类型C的非静态域的类型,都是原生类型,这些原生类型对应于有关C的泛化声明中其类型的擦除。
具有原生类型C的静态方法或静态域的类型与有关C的泛化声明中的类型相同。
如果将类型引元传递给并非从超类或超接口继承而来的具有原生类型的非静态类型成员,那么就会产生一个编译时错误。
如果试图将参数化类型的类型成员当作原生类型使用,那么就会产生一个编译时错误。
这意味着对“稀有”类型的禁令扩展到了新的情况,即限定类型是参数化的,但是却试图将内部类用作原生类型
:
Outer<Integer>.Inner x = null; // illegal
这是之前讨论过的情况的对立面。对于这类“夹生”类型,在现实中没有任何实际意义, 因为,在遗留代码中,没有使用类型引元;而在非遗留代码中,应该正确使用泛型类型,并传递所有需要的类型引元。
一个类的超类型可以是原生类型,对这个类的成员访问会当作普通的成员访问进行处理,而对其超类型的成员访问会当作对原生类型的访问进行处理。在这个类的构造器中,对 super的调用会当作在原生类型上的调用进行处理。
使用原生类型
只能是为了兼容遗留代码
而不得已采用的一种折中方法
,在泛型被引入到 Java编程语言中之后,就不再鼓励在代码中使用原生类型了
。Java编程语言的未来版本甚至有可能会禁用原生类型
。
为了确保任何潜在的违反类型规则的行为都能够被标记出来,对原生类型的某些访问会产生编译时的非受检警告。在访问原生类型成员或构造原生类型时,产生编译时非受检警告的规则
如下:
1.在对域赋值时:如果左操作数的类型是原生类型,那么若类型擦除改变了这个域的类型,则产生编译时非受检警告。
2.在调用方法或构造器时:如果要搜索的类或接口的类型是原生类型, 那么若类型擦除改变了该方法或构造器的任何形参类型,则都会产生编译时非受检警告。
3.对于类型擦除不会改变任何形参类型的方法调用(即使其返回类型或throws子句会改变),或者从域中读取值的操作,或者对于原生类型的类实例的创建,都不会产生编译时非受检警告。
注意 上述非受检警告有别于从非受检类型转换、类型转换、 方法声明以及可变元方法调用中产生的非受检警告。
这里的警告涵盖了遗留用户使用泛化类库的情况。例如,某个库声明了泛化类Foo<T extends String>
,其中有一个类型为Vector<T>
的域 f
, 但是这个库的用户将一个整数向量赋值给了 e.f
, 其中e具有原生类型Foo
。遗留用户会接收到警告,因为对于该泛化类库的 泛化用户来说,这可能会导致堆污染
(注意,如果遗留用户将来自类库的Vector<String>
赋值给它自己的Vector
变量,那么是不会接收到任何警告的。这是因为,Java编程语言的子类型规则使得原生类型的变量可以被赋值为类型为任何该类型的参数化实例的值。)
来自非受检转换的警告涵盖了双重情况,即泛化用户使用遗留库的情况。例如,库中的某个方法具有原生返回类型Vector
,但是用户将对该方法调用的结果赋值给类型为 Vector<String>
的变量。这是不安全的,因为该原生向量中的元素类型可能与String不同, 但是通过使用非受检转换,还是允许这样赋值的,这是为了能够与遗留代码进行交互。来自非受检转换的警告表示泛化用户可能会在程序的其他位置遭遇由堆污染而产生的种种问题。
原生类型
class Cell<E> {
E value;
Cell(E v) { value = v; }
E get () {return value; }
void set(E v) { value = v; }
public static void main(String[] args) {
Cell x = new Cell<String> ("abc");
System.out.println(x.value); // OK,has type Object
System.out.println(x.get()); // OK, has type Object
x.set("def"); // unchecked warning
}
}
原生类型和继承
import java.util.*;
class NonGeneric {
Collection<Number> myNumbers() { return null; }
}
abstract class RawMembers<T> extends NonGeneric implements Collection<String> {
static Collection<NonGeneric> eng = new ArrayList<NonGeneric>();
public static void main(String[] args) {
RawMembers rw = null;
Collection<Number> cn = rw.myNumbers(); // OK
Iterator<String> is = rw.iterator(); // Unchecked warning
Collection<NonGeneric> enn = rw.eng; // OK, static member
}
}
在这个程序中(并不表示它能够运行),RawMember<T>
从超接口 Collection<String>
中 继承了下面的方法:
Iterator<String> iterator()
但是,这也就意味着在RawMember
中的iterator ()
的返回类型应该是Iterator
。因此,试图将rw. iterator ()
赋值给Iterator<String>
时,需要进行非受检转换,而这会导致非受检警告的产生。
与此相反,RawMember
从 NonGeneric
类继承了 myNumbers ()
, 其中 NonGeneric 的擦除还是NonGenerico。因此,RawMember
中的myNumber ()
的返回类型没有被擦除,试图将 rw.myNumbers ()
赋值给Collection<Number>
时,不需要进行任何非受检转换,因此也就不会产生任何编译时非受检警告。
类似地,在静态成员eng
中保留了完整的参数化类型,即使是通过原生类型的对象进行访问时也是如此。注意,通过实例来访问静态成员被认为是不良习惯,因而并不鼓励这种方式。
这个示例揭示了这样一个事实:原生类型的某些成员没有被擦除
,即参数化类型的静态成员
,以及从非泛化超类型继承而来的成员
。
原生类型与通配符紧密相关,两者都是基于既存类型的。原生类型
可以被认为是为了能够与遗留代码交互而故意设计出来的类型规则并不完善的通配符
。在历史上,原生类型先于通配符而产生,它们首先被引入到了 GJ中,在Gilad Bracha、Martin Odersky、David Stoutamire 和 Philip Wadler
撰写的发表于 Proceedings of the ACM Conference on Object- Oriented Programming, Systems, Languages and Application
( OOPSLA 98
) , October 1998 上的 论文 Making the future safe fbr the past: Adding Genericity to the Java Programming Language
中对它们进行了描述。
交集类型
交集类型的形式为 T 1 T_{1} T1 & …& T n T_{n} Tn(n > 0),其中 T 1 T_{1} T1( 1 ≤ i ≤ n 1leq i leq n 1≤i≤n) 是类型表达式。
交集类型可以从类型参数边界
和类型转换表达式
派生,它们还可以在捕获转换
和最低上边界计算
的过程中产生。
交集类型的值是这样的对象:对于每一个 T i T_{i} Ti ( 1 ≤ i ≤ n 1leq i leq n 1≤i≤n) 类型来说,这些对象都是它的值。
每一个交集类型
T
1
T_{1}
T1 & …&
T
n
T_{n}
Tn 都可以归纳为一个概念类或接口,用来标识交集类型的成员,就像下面这样:
1.对于每一个类型
T
i
T_{i}
Ti (
1
≤
i
≤
n
1leq i leq n
1≤i≤n) ,假设
C
i
C_{i}
Ci是最具体的类或数组类型,使得
T
i
T_{i}
Ti <:
C
i
C_{i}
Ci, 那么必然有某个
C
k
C_{k}
Ck使得对于任意的 i (
1
≤
i
≤
n
1leq i leq n
1≤i≤n)都有
C
k
C_{k}
Ck <:
C
i
C_{i}
Ci,否则就会产生编译时错误。
2.对于
1
≤
j
≤
n
1leq j leq n
1≤j≤n,如果
T
j
T_{j}
Tj是类型变量,那么就使得
T
j
′
T_{j}^{'}
Tj′是一个接口,其成员与
T
j
T_{j}
Tj的 public 成员相同;否则,如果
T
j
T_{j}
Tj是接口,那么就使得
T
j
′
T_{j}^{'}
Tj′为
T
j
T_{j}
Tj。
3.如果
C
k
C_{k}
Ck是Object, 就归纳为概念接口,否则,就用
C
k
C_{k}
Ck的超类归纳出概念类。这个类或接口的直接超接口是
T
1
′
T_{1}^{'}
T1′ , …,
T
n
′
T_{n}^{'}
Tn′,并且是在交集类型出现的包中声明的。
交集类型的成员就是它归纳出的类或接口的成员。
对于交集类型
和类型变量的边界
之间的差异是很值得深思的。每一个类型变量的边界都会归纳为一个交集类型
,这种交集类型通常都很简单(即由单一类型构成)。边界的形式是受限的
(只有第一个元素可以是类或类型变量,并且在边界中只能出现一个类型变量),以杜绝某些尴尬的情况。但是,捕获转换可能会导致创建出边界更加普通(例如数组类型)的类型变量。
子类型化
子类型和超类型之间的关系是类型上的二元关系。
一个类型的超类型是通过其直接超类型关系之上的自反和传递闭包而获得的,记作 S>
1
_{1}
1T, 这是由本节后续内容中描述的规则所定义的。我们用S:>T来表示S和T之间的超类型关系。
如果S:>T且S
≠
not=
=T,则S是T的真超类型
,记作S>T。
类型T的子类型U是所有能够使得T是U的超类型的类型以及空类型。我们用T<:S来表示T和s之间的子类型关系。
如果T<:S且S
≠
not=
= T,则T是S的真子类型
,记作T<S。
如果S>
1
_{1}
1T, 且T是S的直接子类型,记作T<
1
_{1}
1S。
子类型化并未扩展至参数化类型:T<:S 不表示 C<T><:C<S>
。
简单类型之间的子类型化
下列规则定义了简单类型之间的超类型关系:
double >
1
_{1}
1 float
float >
1
_{1}
1 long
long >
1
_{1}
1 int
int >
1
_{1}
1 char
int >
1
_{1}
1 short
short >
1
_{1}
1 byte
类与接口类型之间的子类型化
给定非泛型声明C
,则类型C
的直接超类型
为如下所有类型:
1.C的直接超类。
2.C的直接超接口。
3.Object类型,如果C是没有任何直接超接口的接口类型。
给定泛型声明
C
<
F
1
,
…
,
F
n
>
C<F_{1},…,F_{n}>
C<F1,…,Fn>(n>0),原生类型C
的直接超类型
为如下所有类型:
1.原生类型C的直接超类。
2.原生类型C的直接超接口。
3.Object类型,如果
C
<
F
1
,
…
,
F
n
>
C<F_{1},…,F_{n}>
C<F1,…,Fn>是没有任何直接超接口的泛化接口类型。
给定泛型声明
C
<
F
1
,
…
,
F
n
>
C<F_{1},…,F_{n}>
C<F1,…,Fn>(n>0),泛型
C
<
F
1
,
…
,
F
n
>
C<F_{1},…,F_{n}>
C<F1,…,Fn>的直接超类型
为如下所有类型:
1.
C
<
F
1
,
…
,
F
n
>
C<F_{1},…,F_{n}>
C<F1,…,Fn>的直接超类。
2.
C
<
F
1
,
…
,
F
n
>
C<F_{1},…,F_{n}>
C<F1,…,Fn>的直接超接口。
3.Object类型,如果
C
<
F
1
,
…
,
F
n
>
C<F_{1},…,F_{n}>
C<F1,…,Fn>是没有任何直接超接口的泛化接口类型。
4.原生类型C。
给定泛型声明
C
<
F
1
,
…
,
F
n
>
C<F_{1},…,F_{n}>
C<F1,…,Fn>(n>0),参数化类型
C
<
F
1
,
…
,
F
n
>
C<F_{1},…,F_{n}>
C<F1,…,Fn>的直接超类型
为如下所有类型,这里每个
T
i
T_{i}
Ti (
1
≤
i
≤
n
1leq i leq n
1≤i≤n)都是一种类型:
1.
D
<
U
1
0
,
…
,
U
k
0
>
D<U_{1}0,…,U_{k}0>
D<U10,…,Uk0>,其中
D
<
U
1
,
…
,
U
k
>
D<U_{1},…,U_{k}>
D<U1,…,Uk>是泛型,并且是泛型
C
<
T
1
,
…
,
T
n
>
C<T_{1},…,T_{n}>
C<T1,…,Tn> 的直接超类型,而 0是替换
[
F
1
:
=
T
1
,
…
,
F
n
:
=
T
n
]
[F_{1}:=T_{1},…,F_{n}:=T_{n}]
[F1:=T1,…,Fn:=Tn]的。
2.
C
<
S
1
,
…
,
S
n
>
C<S_{1},…,S_{n}>
C<S1,…,Sn> ,其中
S
i
S_{i}
Si包含
T
i
T_{i}
Ti (
1
≤
i
≤
n
1leq i leq n
1≤i≤n)。
3.Object类型,如果
C
<
F
1
,
…
,
F
n
>
C<F_{1},…,F_{n}>
C<F1,…,Fn>是没有任何直接超接口的泛化接口类型。
4.原生类型C。
给定泛型声明
C
<
F
1
,
…
,
F
n
>
C<F_{1},…,F_{n}>
C<F1,…,Fn> ( n>0 ),如果参数化类型
C
<
R
1
,
…
,
R
n
>
C<R_{1},…,R_{n}>
C<R1,…,Rn>中至少有一个
R
i
R_{i}
Ri
(
1
≤
i
≤
n
1leq i leq n
1≤i≤n)是通配符类型引元,那么其直接超类型
就是参数化类型
C
<
X
1
,
…
,
X
n
>
C<X_{1},…,X_{n}>
C<X1,…,Xn> 的直接超类型,而
C
<
X
1
,
…
,
X
n
>
C<X_{1},…,X_{n}>
C<X1,…,Xn> 是将捕获转换应用于
C
<
R
1
,
…
,
R
n
>
C<R_{1},…,R_{n}>
C<R1,…,Rn> 后所产生的结果。
交集类型
T
1
T_{1}
T1 & …&
T
n
T_{n}
Tn的直接超类型
是
T
i
T_{i}
Ti (
1
≤
i
≤
n
1leq i leq n
1≤i≤n)。
类型变量
的直接超类型
是在其边界中所列出的类型。
类型变量
是其下边界
的直接超类型
。
空类型
的直接超类型
是除空类型自身之外的所有引用类型。
数组类型之间的子类型化
下列规则定义了数组类型之间的直接超类型关系:
1.如果S和T都是引用类型,则当且仅当S>
1
_{1}
1T时, S [ ] >
1
_{1}
1 T [ ]
2.Object >
1
_{1}
1 Object [ ]
3.Cloneable >
1
_{1}
1Object [ ]
4.java.io.Serializable >
1
_{1}
1Object [ ]
5.如果p是简单类型,则:
- Object > 1 _{1} 1 P [ ]
- Cloneable > 1 _{1} 1 P [ ]
- java.io.Serializable > 1 _{1} 1 P [ ]
最低上边界
一个引用类型集的最低上边界
(缩写lub)就是比其他任何共享超类型更具体的共享超类型
(也就是说,没有任何其他共享超类型是最低上边界的子类
)。这个类型,lub (
U
k
,
…
,
U
k
U_{k},…,U_{k}
Uk,…,Uk) 是按如下规则确定的。
如果k=1, 那么lub就是该类型自身:lub (U) =U。
否则:
- 对每一个
U
i
U_{i}
Ui (
1
≤
i
≤
n
1leq i leq n
1≤i≤n):
设ST(U)是 U i U_{i} Ui的超类型集
设EST( U i U_{i} Ui)是 U i U_{i} Ui的被擦除的超类型集,即:
EST ( U i U_{i} Ui)= { |w| | winST ( U i U_{i} Ui) },其中 |w| 是 w 的擦除。
计算被擦除的超类型集的原因是为了处理这样的情况:类型集中包含了某个泛型的若 干个有区别的参数化版本。
例如,给定List<String>
和List<Object>
, 直接求ST ( List<String>) = { List<Str ing>, Collection<String>, Object }
和ST(List<Object>) = { List<Object>, Collection <Object>, Object }
的交集将会产生{ Object }
,而这将掩盖一个事实,即可以将上边界安全地设定为List
。
相反,求EST(List<String>) = { List, Collection, Object }
和EST(List<Object>)=( List, Collection, Object }
的交集,将会产生{List, Collection, Object}
, 使得我们最终可以找到List<?>
。 - 设EC是 U 1 , … , U k U_{1},…,U_{k} U1,…,Uk被擦除的候选集,即所有EST ( U i U_{i} Ui)( 1 ≤ i ≤ n 1leq i leq n 1≤i≤n )集的交集。
- 设MEC是最小的
U
1
,
…
,
U
k
U_{1},…,U_{k}
U1,…,Uk被擦除的候选集,即MEC = {v | v属于EC,对于任意 w
≠
not=
= v,不存在可w <: v }
因为我们在寻求推断出更准确的类型,所以我们希望过滤掉所有是其他候选类型的超类型的候选类型。这正是计算MEC所要完成的任务。在上面的示例中,EC={List, Collection, Object},
因此MEC={List}。下一步将恢复MEC中被擦除类型的类型引元。 - 对MEC的任何是泛型的元素G:
设G相关的参数化版本为Relevant (G),即:
Relevant (G) = { v | 1 ≤ i ≤ k 1leq i leq k 1≤i≤k : v 属于 ST ( U i U_{i} Ui) 且 V=G<…>}
在上面的示例中,MEC中仅有的泛化元素是List, 并且Inv ( List)= { List<String>, List<Object>}
。现在我们寻求找到同时包含String 和 Object的 List 类型引元。
这是通过下面定义的“最少包含参数化版本(lep)”操作来实现的。第一行在一个集合上,例如Relevant (List),将lcp()定义为在该集合的元素列表上的操作。下一行将在这种列表上的操作定义为在列表元素上两两消减。第三行是在参数化类型对上的 lcp()的定义,这又依赖于"最小包含类型引元(Icta)"这一概念,其中lcta()为所有可能的情况进行了定义。
设G的候选参数化版本为Candidate (G),即泛型G的最具体参数化版本,它包含G的所有相关参数化版本。
Candidate (G)= lcp (Relevant (G))
其中lcp(),即最小包含调用,为:
(1) lcp (s)=lcp ( e i , … , e n e_{i},…,e_{n} ei,…,en)其中 e i e_{i} ei( 1 ≤ i ≤ k 1leq i leq k 1≤i≤k)在 s 中
(2) lcp ( e i , … , e n e_{i},…,e_{n} ei,…,en)= lcp (lcp ( e 1 , e 2 e_{1},e_{2} e1,e2) , e 3 , … , e n e_{3},…,e_{n} e3,…,en)
(3) lcp ( G < X 1 , … , X n > G<X_{1},…,X_{n}> G<X1,…,Xn>, G < Y 1 , … , Y n > G<Y_{1},…,Y_{n}> G<Y1,…,Yn>) = G<lcta ( X 1 X_{1} X1, Y 1 Y_{1} Y1),…,Icta ( X n X_{n} Xn, Y n Y_{n} Yn) >
(4) lcp( G < X 1 , … , X n > G<X_{1},…,X_{n}> G<X1,…,Xn>) = G< lcta ( X i X_{i} Xi),…,Icta ( X n X_{n} Xn) >
其中lcta(),即最小包含类型引元,为:(假设U和V是类型)
(1) Icta(U, V)= U ,如果 U = V,否则为 ?extends lub(U, V)
(2) Icta(U, ? extends V) = ? extends lub (U, V)
(3) Icta(U, ? super V) = ? super glb (U, V)
(4) Icta (? extends U, ? extends V)= ? extends lub(U, V)
(5) Icta (? extends U, ? super V)= U, 如果 U=V,否则为?
(6) Icta (? super U, ? super V)= ? super gib (U, V)
(7) Icta (u) = ? 如果 u 的上边界是 Object, 否则为? extends lub(U, Object)
其中glb()如在第5.1.10节中的定义。 - 设 lub(
U
1
,
…
,
U
k
U_{1},…,U_{k}
U1,…,Uk)为:
Best ( W i W_{i} Wi) &…&Best ( W r W_{r} Wr)
其中 W i W_{i} Wi( 1 ≤ i ≤ r 1leq i leq r 1≤i≤r)都是MEC,即 U 1 , … , U k U_{1},…,U_{k} U1,…,Uk最小擦除候选集的元素;
并且,如果这些元素中只要有泛化的,就使用候选参数化版本(以便恢复类型引元)
如果 x 是泛化的,则 Best (x)= Candidate (x);否则 Best (x) = x。
严格地讲,这个Iub()函数只能得到近似的最小上边界。形式化地讲,可能存在某个其他类型T,使得所有 U 1 , … , U k U_{1},…,U_{k} U1,…,Uk都是T的子类型,并且T是lub( U 1 , … , U k U_{1},…,U_{k} U1,…,Uk)的子类型。但是,Java编程语言的编译器必须按照上面指定的方式实现lub()。
lub()函数可能会产生一个无限类型。这是允许的,并且Java编程语言的编译器必需识别这种情况,并使用循环数据结构来恰当地表示它们。
产生无限类型的可能性来自于对lub()的递归调用。熟悉递归类型的读者应该注意到无限类型
与递归类型
是不同的。
使用类型之处
类型在大多数种类的声明和某些种类的表达式中使用。具体地讲,有16种使用类型的类型上下文:
在声明中:
1)在类声明的extends或implements子句中的类型。
2)在接口声明的extends子句中的类型。
3)方法的返回类型(包括注解类型的元素的类型)。
4)在方法或构造器的throws子句中的类型。
5)在泛化类、接口、方法或构造器的类型参数声明的extends子句中的类型。
6)在类或接口的域声明中的类型(包括枚举常量)。
7)在方法、构造器或lambda表达式的形参声明中的类型。
8)方法的接收参数的类型。
9)在局部变量声明中的类型。
10)在异常参数声明中的类型。
在表达式中:
1)在传递给显式构造器调用语句、类实例创建语句和方法调用表达式的显式类型引元列表中的类型。
2)在非限定类实例创建表达式中,作为被实例化的类类型,或者作为被实例化的匿名类的直接超类或超接口。
3)在数组创建表达式中的元素类型。
4)在类型转换表达式的类型转换操作符中的类型。
5)跟在instanceof关系操作符后面的类型。
6)在方法引用表达式中,作为用来查找成员方法的引用类型,或者作为要构造的类类型或数组类型。
同时,类型还可以用作:
1)在上述任何上下文中的数组类型的元素类型。
2)在上述任何上下文中的参数化类型的非通配符类型引元或有界通配符类型引元。
最后,在Java编程语言中还有三种特殊的情况用来表示对类型的使用:
1)无边界通配符。
2)在可变元参数的类型中的…,表示一个数组类型。
3)在构造器声明中的类型的简单名,表示被构造对象的类。
有些类型上下文对应用类型可以如何被参数化进行了限制:
下面的类型上下文要求:如果某个类型是参数化引用类型,那么它就不能有任何通配符类型引元。
1)在类声明的extends或implements子句中。
2)在接口声明的extends子句中的类型。
3)在非限定类实例创建表达式中,作为被实例化的类类型,或者作为被实例化的匿名类的直接超类或超接口。
4)在方法引用表达式中,作为用来查找成员方法的引用类型,或者作为要构造的类类型或数组类型。
另外,任何通配符类型引元都不允许出现在传递给显式构造器调用语句、类实例创建表达式、方法调用表达式和方法引用表达式的显式类型引元列表中。
下面的类型上下文要求:如果某个类型是参数化引用类型,那么它只能有无界通配 符类型引元(即,它是可具化类型)。
1)在数组创建表达式中作为元素类型。
2)跟在instanceof关系操作符后面的类型。
下面的类型上下文都不允许使用参数化引用类型,因为它们都涉及异常,并且异常的类型是非泛化的:
1)作为在可以被方法或构造器抛出的异常的类型。
2)在异常参数声明中。
在任何使用类型的类型上下文中,都可能会对表示简单类型的关键字或表示引用类型的简单名的标识符进行注解。还可能会对数组类型进行注解,方法是在嵌套在数组类型中的某个级别的 [ 的左边编写注解。在这些位置的注解被称为类型注解,第9.7.4节对其进行了说明。下面是一些示例:
@Foo int [ ] f; 注解简单类型int
int @Foo [] f; 注解数组类型int[]
int @Foo [] [] f; 注解数组类型 int[] []
int[] @FOO [] f; 注解数组类型int[],它是数组类型int[][]的构成类型
出现在声明中的5种类型上下文在语法上的地位与许多声明上下文相同:
1)方法的返回类型(包括注解类型的元素的类型)。
2)在类或接口的域声明中的类型(包括枚举常量)。
3)在方法、构造器或lambda表达式的形参声明中的类型。
4)在局部变量声明中的类型。
5)在异常参数声明中的类型。
在程序中,同一个语法位置既可以是类型上下文,也可以是声明上下文。这是因为声明的修饰符紧贴在所声明实体的类型之前。第9.7.4节解释了在这种位置的注解是如何被理解为出现在类型上下文中还是出现在声明上下文中,亦或是同时出现在两种上下文中。
类型的使用
import java.util.Random;
import java.util.Collection;
import java.util.ArrayList;
class MiscMath<T extends Number> {
int divisor;
MiscMatb(int divisor) { this.divisor = divisor;}
float ratio(long 1) {
try {
1 /= divisor;
} catch (Exception e) {
if (e instanceof ArithmeticException)
1 = Long.MAX_VALUE;
else
1 = 0;
}
return (float);
}
double gausser() {
Random r = new Random();
double[] val = new double[2];
val[0] = r.nextGaussian();
val[1] = r.nextGaussian();
return (val[0] + val[1]) / 2;
}
Collection<Number> fromArray(Number[] na) {
Collection<Number> cn = new ArrayList<Number>();
for (Number n : na)
cn.add(n);
return cn;
}
<S> void loop(S s) {
this.<S>loop(s);
}
}
例4.10中,在下列声明中用到了类型:
1)导入的类型:这里通过从java.util包中导入java.util.Random类型而声明了Random类型。
2)作为类变量或类的实例变量以及接口常量的域,这里MiscMath类的divisor域被声明为int类型。
3)方法参数:这里ratio方法的1参数就被声明为long类型。
4)方法结果:这里ratio方法的结果就被声明为float类型,而 gausser 方法的结果被声明为double类型。
5)构造器参数:这里MiscMath的构造器参数被声明为int类型。
6)局部变量:gausser方法的局部变量 r 和 val 分别被声明为Random 和 double [ ] (double 数组)类型。
7)异常参数:这里catch子句的异常参数e被声明为Exception类型。
8)类型参数:这里MiscMath的类型参数是类型变量T,其声明的边界为Number 类型
9)使用参数化类型的任何声明:这里Number类型在参数化类型Collection<Number>
中被用作类型引元。
在下面种类的表达式中也用到了类型:
1)类实例的创建:这里 gausser 方法的局部变量r被使用Random类型的类实例创建表达式所初始化。
2)泛化类实例的创建:这里Number在new ArrayList<Number>()
中被用作类型引元。
3)数组的创建:这里gausser方法的局部变量val被创建尺寸为2的 double 数组的数组创建表达式所初始化。
4)对泛化方法或构造器的调用:这里loop方法用显式类型引元 s 调用了自己。
5)类型转换:这里ratio方法的return语句在类型转换中使用了 float 类型。
6)instanceof操作符:这里instanceof操作符用来测试e是否与 ArithmeticException 类型赋值兼容。
变量
一个变量就是一个存储位置,并且具有相关联的类型,有时也称其为变量的编译时类型,该类型要么是简单类型,要么是引用类型。
变量的值可以通过赋值或前缀式与后缀式的++ (递增)与–(递减)操作符来改变。
变量的值与其类型的兼容性是由Java编程语言的设计方案所保证的,只要程序没有产生编译时的非受检警告。类型的缺省值都是与其类型兼容的, 而所有赋值操作都会检查其赋值兼容性,这种检查通常是在编译时执行的,但是有一个例外,就是涉及数组的赋值兼容性检查是在运行时执行的。
简单类型的变量
简单类型的变量总是持有简单值,其类型与该简单类型精确匹配。
引用类型的变量
类类型T的变量可以持有空引用,或者是对T类以及T的任意子类的实例的引用。
接口类型的变量可以持有空引用,或者是对实现了该接口的任意类的实例的引用。
注意 变量并不能保证总是对其声明类型的子类型的引用,而只能保证是对其声明类型的子类或子接口的引用。这是因为有可能会出现下面将要讨论的堆污染。
如果T是简单类型
,那么“T的数组”类型的变量就可以持有空引用,或者是对任意“T 的数组”类型的数组的引用。
如果T是引用类型
,那么“T的数组”类型的变量就可以持有空引用,或者是对任意“S 的数组”类型的数组的引用,其中S类型是T类型的子类或子接口。
Object[]类型
的变量可以持有对任意引用类型数组的引用。
Object类型
的变量可以持有空引用,或者是对任意对象的引用,无论引用的对象是类的实例还是数组。
参数化类型的变量有可能会引用不是该参数化类型的对象
,这种情况称为堆污染
。
只有在程序执行某些会产生编译时非受检警告的涉及原生类型的操作时,或者当程序通过超类型是原生类型或非泛化类型的数组变量对不可具化元素类型的数组变量起别名时,才会发生堆污染。
例如,下面的代码:
List l = new ArrayList<Number>();
List<String> ls = l; // Unchecked warning
会产生编译时非受检警告,因为它无论是在编译时(在编译时类型检查规则的限制范围内) 还是运行时,都无法确定变量l是否确实是指向List<String>
对象的引用。
如果执行上述代码,就会产生堆污染,因为变量Is声明成了 List<String>
,但是它指向的对象实际上并非List<String>
。
在运行时,这个问题并不能被识别,因为类型变量没有被具化,因此实例在运行时并未携带任何与创建它们所使用的类型引元相关的信息。
在上面给出的这种简单示例中,看起来好像在编译时识别这种情况并产生错误会显得更加直接。但是,在通常(且典型的)情况下,变量l的值可能是对某个单独编译的方法进行调用的结果,或者它的值有可能会依赖于任意某个控制流。因此,上面的代码十分非典型, 而且是非常不好的习惯。
更近一步讲,Object[]是所有数字类型的超类型这个事实意味着不安全地起别名是完全有可能发生的,而这会导致堆污染。例如,下面的代码可以编译,因为它是静态地类型正确的:
static void m(List<String>... stringLists) {
Object[] array = stringLists;
List<Integer> tmpList = Arrays.asList(42);
array[0] = tmpList; // ①
String s = stringLists[0].get(0); // ②
在①处会发生堆污染,因为在stringLists数组中的元素原本应该指向一个 List<String>
对象,现在指向的却是一个List<Integer>
对象。无论是在通用超类型 (Object[]
)还是不可具化类型(形参声明的类型,List<StHng>[]
)面前,都无法探测到这种污染。因此,在①处不产生任何非受检警告是合理的。但是,在运行时,在②处会产生一个ClassCastException。
对上述方法的任何调用都会产生编译时非受检警告,因为Java编程语言的静态类型系统会认为这种调用是要创建一个元素类型为不可具化的List<String>
的数组。当且仅当方法体对于可变元参数是类型安全的,程序员才能够使用SafeVarargs
注解来关闭调用处的警告。由于像上面这样编写的方法体会导致堆污染,所以使用注解来禁用对调用者的警告是完全不恰当的。
最后,请注意,stringLists数组可以通过除Object []类型之外的类型变量起别名,而在此情况下堆污染仍旧会发生。例如,array变量的类型可以是 java.util.Collection[]
, 即原生元素类型,而上面的方法体可以编译,并且不会产生警告或错误,但是仍旧会导致堆污染。而且,假设Java SE平台定义了 Sequence作为List<T>
的非泛化超类型,那么即便将 Sequence用作array类型,也会产生堆污染。
变量总是会引用到这样的对象上:它是表示参数化类型的某个类的一个实例。
在上述示例中的Is的值就总是某个类的一个实例,这个类提供了 List 的一种表示。
对于用原生类型的表达式向参数化类型的变量进行赋值的做法,应该只有在将未使用参数化类型的遗留代码与使用了参数化类型的更新的代码相结合时,才考虑使用。
如果没有任何操作会触发编译时非受检警告,并且具有不可具化的元素类型的数组变量不会产生任何不安全的别名操作,那么堆污染就不会发生。请注意,这并不表示堆污染只有在确实产生了编译时非受检警告时才会发生。因为运行的程序有可能是二进制程序,它们是用针对较老版本的Java编程语言的编译器产生的,或者其源代码显式地压制了非受检警告的源代码。这种习惯可以说是非常不好的。
相反地,无论执行的代码是否愿意(以及会)产生编译时非受检警告,都有可能不会发生堆污染。实际上,良好的编程习惯要求程序员要确保无论是否存在任何非受检警告,代码都应该是正确的,并且不会发生堆污染。
变量的种类
变量分为8种:
1)类变量
:它是在类声明中用static关键字或在接口声明中无论是否使用static关键字声明的域。
类变量是在类或接口的准备阶段创建的,并且被初始化为缺省值。类变量在类或接口被卸载时即被有效地终止生命周期。
2)实例变量
:它是在类声明中没有使用Static关键字声明的域。
如果类T的域a是实例变量,那么新的实例变量a就会被创建出来,并被初始化为缺省值,作为新创建的T类对象或T的任意子类对象的一部分。当包含实例域的对象不再被引用时,在该对象的所有必需的终结操作执行完成后,该实例变量即被有效地终止生命周期。
3)数组成员
:它们是不具名变量,在新的数组对象被创建时, 它们就被创建并被初始化为缺省值。数组成员在数组不再被引用时,即被有效地终止生命周期。
4)方法参数
:它们对传递给方法的引元值进行命名。
对于在方法声明中声明的每一个参数,在方法每次被调用时都会创建新的参数变量。新的变量被初始化为方法调用中相应的引元值。当方法体执行完成后,方法参数即被有效地终止生命周期。
5)构造器参数
:它们对传递给构造器的引元值进行命名。
对于在构造器声明中声明的每一个参数,在类实例创建表达式或显式的构造器调用中对构造器的每次调用都会创建新的参数变量。新的变量被初始化为创建表达式或构造器调用中相应的引元值。当构造器体执行完成后,构造器参数即被有效地终止生命周期。
6)lambda参数
:它们对传递给lambda表达式体的引元值进行命名。
7)异常参数
:每当异常被try语句的catch子句捕获时,都会创建一个异常参数。
新变量会用与异常相关联的实际对象进行初始化。异常参数在与catch子句相关联的块执行完成后,即被有效地终止生命周期。
8)局部变量
:它们是由局部变量声明语句声明的。
当控制流进入一个块或for语句中时,就会立即为在这个 块或for语句中包含的局部变量声明语句中所声明的每个局部变量创建新变量。
局部变量声明语句可以包含初始化变量的表达式。但是,带有初始化表达式的局部变量只有在声明它的局部变量声明语句被执行时,才会被初始化。(明确赋值规则可以防止局部变量在初始化之前被使用,只有在赋值后局部变量才能使用。)当包含局部变量的块或for语句执行完成后,局部变量即被有效地终止生命周期。
在局部变量声明语句被执行时,通常总是要创建局部变量,但是有一个例外。这个例外涉及switch语句,此时控制流会进入一个块,但是有可能会绕过局部变量声明语句。然而,明确赋值规则强制实施的约束条件使得由这样被绕过的局部变量声明语句所声明的局部变量,在通过赋值表达式被明确赋值之前不能被使用。
不同种类的变量
class Point {
static int numPoints;
int x, y;
int[] w = new int[10];
int setX(int x) {
int oldx = this.x;
this.x = x;
return oldx;
}
}
final 变量
变量可以被声明为final,而final变量只能被赋值一次。如果对final变量赋值,那么除非在赋值之前该变量是明确未赋值的,否则就是一种编译时错误。
一旦final变量被赋值,那么它就始终持有同一个值。如果一个final变量持有的是对对象的引用,那么该对象的状态可以被对象上的操作所修改,但是该变量会始终指向这个对象。这条规则也同样适用于数组,因为数组也是对象。如果一个final变量持有的是指向 数组的引用,那么该数组的元素可以被数组上的操作所修改,但是该变量会始终指向这个数组。
空final是指其声明缺少初始化器的final变量。
常量变量是指用常量表达式初始化的简单类型或String类型的final变量。无论一个变量是否是常量变量,都涉及相关的类初始化、二进制兼容性和明确赋值。
有三种变量被隐式地声明为final:接口的域、带资源的try语句中的资源,以及多重catch子句中的异常参数。单catch子句的异常参数永远都不会被隐式地声明为final,但是它可以被认为效果等同于final。
final变量
将变量声明为final可以起到归档作用,因为它的值不会被修改,同时可以帮助避免编程错误。在下面的程序中,
class Point {
int x, y;
int useCount;
Point(int x, int y) { this.x = x; this.y = y; }
static final Point origin = new Point(0, 0);
Point类声明了 一个final类变量origin, origin变量持有的是对表示坐标原点(0,0 )的 Point类的实例的引用。Point.origin 变量的值永远不能修改,因此它总是引用同一个Point对象,即由它的初始化器创建的对象。但是,在这个Point对象上的操作可以改变它的状态,例如,修改其useCount,甚至可以误导性地修改其x和y的坐标。
某些没有声明为final的变量可以被认为效果等同于final。
对于局部变量,或者是方法、构造器、lambda和异常参数,如果它们没有被声明为 final,但是永远不会作为赋值操作符的左操作数出现,或者永远不会作为前缀或后缀递增或递减操作符的操作数出现,那么它们就会被认为效果等同于final。
另外,声明缺少初始化器的局部变量,如果满足下列所有条件,那么也被认为效果等同于 final:
1.它没有被声明为final。
2.无论何时它作为赋值操作符的左操作数出现,都是明确未赋过值的, 并且在本次赋值操作前未被明确赋值。即它是明确未赋过值的,并且在本次赋值操作的右操作数之后未被明确赋值。
3.它永远不会作为前缀或后缀递增或递减操作符的操作数出现。
如果变量在效果上等同于final,那么对其声明添加final修饰符不会引入任何编译时错误。相反地,在合法的程序中被声明为final的局部变量和参数,如果将final修饰符移除, 那么它们会在效果上等同于final。
变量的初始值
程序中每个变量在使用前都必须拥有值:
1.每个类变量
、实例变量
或数组元素
在被创建时都需要用缺省值进行初始化:
- 对于byte类型,缺省值是0,即(byte)0的值
- 对于short类型,缺省值是0,即(short)0的值
- 对于int类型,缺省值是0, 即 0 的值
- 对于long类型,缺省值是0,即 0l 的值
- 对于float类型,缺省值是正0, 即0.0f
- 对于double类型,缺省值是正0,即0.0d
- 对于char类型,缺省值是空字符,即’u0000’
- 对于boolean类型,缺省值是false
- 对于所有引用类型,缺省值是null
2.每个方法参数
都被初始化为方法的调用者提供的相应的引元值。
3.每个构造器参数
都被初始化为类实例创建表达式或显式构造器调用中提供的相应的引元值。
4.异常参数
被初始化为被抛出的表示异常的对象。
5.局部变量
必须在被使用前显式地通过初始化或赋值操作进行赋值,其赋值方式可以用使用明确赋值规则进行校验。
变量的初始值
class Point {
static int npolnts;
int x, y;
Point root;
}
class Test {
public static void main(String[] args){
System.out.printin("npoints=" + Point.npoints);
Point p = new Point();
System.out.println("p.x=" + p.x + ", p.y=" + p.y);
System.out.println("p.root=" + p.root);
}
}
这个程序会打印出:
npoints=0
p.x=0, p.y=0
p.root=null
上面的输出说明了 npoints是缺省初始化的,这发生在Point类的准备阶段,还说明了 x、y和root也是缺省初始化的,这发生在新的Point被实例化时。第12章对类和接口的加载、链接和初始化的各个方面都进行了完整的描述,另外还有对用于创建新的类实例的类实例化操作的描述。
类型、类和接口
在Java编程语言中,每个变量和表达式都有一个在编译时可以确定的类型。它们的类型可以是简单类型或引用类型。引用类型包括类类型和接口类型。引用类型可以通过类型声明引入,包括类声明和接口声明。我们经常使用类型来引用类或接口。
在Java虚拟机中,每个对象都属于某个特定的类:在产生对象的创建表达式中出现的类,或者其Class对象被用来调用反射方法以产生对象的类,或者用于通过字符串连接操作符+ 隐式创建对象的string类。这个类被称为该对象的类, 而对象则被当作它的类的实例,以及它的类的所有超类的实例。
每个数组也都有一个类。当为数组对象调用getclass方法时,该方法将返回一个表示数组类的(Class类的)类对象。
变量的编译时类型总是声明的类型,而表达式的编译时类型是可以在编译时推断的。编译时类型会对变量在运行时可以持有的值和表达式在运行时可以产生的值进行限定。如果运行时的值是不为null的引用,那么它将指向一个属于某个类的对象或数组,并且这个类必须与编译时类型兼容。
尽管变量或表达式的编译时类型可以是接口类型,但是接口是没有任何实例的。类型是接口类型的变量或表达式可以引用任何实现了该接口的类的对象。
有时变量或表达式被认为含有“运行时类型”,假如这个值不是null的话,该类型即运行时变量或表达式的值所引用的对象所属的类。
编译时类型和运行时类型之间的对应关系并不完全一致是由以下两个原因造成的:
1)在运行时,类和接口是由Java虚拟机用类加载器加载的,而每个类加载器都定义了它自己的类和接口集。因此,两个加载器有可能会加载完全相同的类或接口定义,但是会产生在运行时有区别的类或接口。所以,对于正确编译的代码,如果加载它的类加载器不一 致,那么在链接时可能会失败。
2)类型变量和类型引元在运行时是没有具化的。因此,在运行时相同的类或接口表示的是多个编译时的参数化类型。具体来讲,所有对给定泛化类型声明的编译时调用会共享单一的运行时表示。
在某些条件下,参数化类型的变量可能会引用不是该参数化类型的对象,这种情况就是堆污染。该变量始终将表示该参数化类型的某个类的实例作为对象进行引用。
变量类型与对象的类
interface Colorable {
void setColor(byte r, byte q, byte b);
}
class Point { int x, y; }
class ColoredPoint extends Point implements Colorable {
byte r, g, b;
public void setColor(byte rv, byte gvz byte bv){
r = rv; g = gv; b = bv;
}
}
class Test{
public static void main(String[] args) {
Point p = new Point();
ColoredPoint cp = new ColoredPoint();
p = cp;
Colorable c = cp;
}
}
在这个示例中:
- Test类main方法的局部变量p的类型为Point,并且被初始化赋值为对一个Point类的新实例的引用。
- 类似地,局部变量cp的类型为ColoredPoint,并且被初始化赋值为对一个 ColoredPoint类的新实例的引用。
- 将cp的值赋值给变量p会导致p持有对一个ColoredPoint对象的引用。这是允许的,因为ColoredPoint是Point的子类,因此ColoredPoint类与Point类型是赋值兼容的。ColoredPoint对象包括对Point的所有方法的支持,除了其特有的域r、g、b之外,它还有Point的域,即x和y。
- 局部变量c的类型是接口类型Colorable,因此它可以持有对任何实现了 Colorable
的类的对象的引用。具体来讲,它可以持有对一个ColoredPoint对象的引用。
注意 诸如new Colorable() 这样的表达式是非法的,因为只能创建类的实例,而不能创建接口的实例。但是,表达式new Colorable () { public void setColor…… }
是合法的,因为它声明了 一个实现了 Colorable接口的匿名类。
最后
以上就是可靠果汁最近收集整理的关于JAVA语言规范 JAVA SE 8 - 类型、值和变量类型和值的种类简单类型和值引用类型和值类型变量参数化类型类型擦除可具化类型原生类型交集类型子类型化使用类型之处变量的全部内容,更多相关JAVA语言规范内容请搜索靠谱客的其他文章。
发表评论 取消回复