用Java编程语言编写的每一个表达式要么不产生任何结果,要么有一个可以在编译时推断出来的类型。当表达式出现在大多数的上下文中时,它都必须与该上下文所期望的类型兼容,这个类型被称为目标类型。为方便起见,表达式与其周围上下文的兼容性可以借助下面两种方式实现:
如果这两种策略都无法产生恰当的类型,那么就会产生编译时错误。
确定一个表达式是否是合成表达式的规则,以及在是合成表达式的情况下,其在特定上下文中的类型与兼容性,将依据上下文的种类以及表达式的形式而变化。除了影响表达式的类型之外,目标类型在某些情况下还会影响表达式运行时的行为,以便产生具有恰当类型的值。
类似地,确定目标类型是否允许隐式转换的规则,将依据上下文的种类、表达式的类型以及作为特例的常量表达式的值而变化。从类型S到类型T的转换将允许S类型的表达式在编译时被当作T类型进行处理。在某些情况下,这样做要求在运行时要有相应的动作去检查这种转换的有效性,或者将该表达式运行时的值转译为适合新类型T的形式。
编译时和运行时的类型转换
在Java编程语言中可能会执行的特定转换可以分为以下几种宽泛的种类:
在下列6种转换上下文中,合成表达式可能会受到上下文的影响,或者会发生隐式转换。每种上下文对于确定合成表达式的类型有不同的规则,并且都允许执行上述某些种类的转换,但是同时又会禁止其他种类的转换。这些上下文是:
“转换”还能用来非特指地描述在某个特定转换中的任何转换。例如,我们说某个局部变量的初始化器的表达式将接受“赋值转换”,这意味着系统会根据赋值上下文的规则隐式地为该表达式选择一种具体的转换。
各种转换上下文
class Test {
public static void main(String[] args) {
// Casting conversion (5.4) of a float literal to
// type int. Without the cast operator, this would
// be a compile-time error, because this is a
// narrowing conversion (5.1.3):
int i = (int)12.5f;
// String conversion (5.4) of i1s int value:
System.out.println(" (int) 12.5f==" + i);
// Assignment conversion (5.2) of i's value to type
// float. This is a widening conversion (5.1.2):
float f = i;
// String conversion of f's float value:
System.out.println("after float widening: " + f);
// Numeric promotion (5.6) of i' s value to type
// float. This is a binary numeric promotion.
// After promotion, the operation is float*float:
System.out.print(f);
f = f * i;
// Two string conversions of i and f:
System.out.println("*" + i + "==" + f);
// Invocation conversion (5.3) of f's value
// to type double, needed because the method Math.sin
// accepts only a double argument:
double d = Math.sin(f);
// Two string conversions of f and d:
System.out.println("Math.sin(" + f + ")==" + d);
}
}
这个程序会产生如下输出:
(int)12.5f==12
after float widening: 12.0
12.0*12=144.0
Math.sin(144.0)=-0.49102159389846934
Java编程语言中具体的类型转换可以分成13种。
对任何类型来说,将其转换为同一种类型都是允许的。
这看起来可能根本不值一提,但是它具有两种实际意义。首先,对于表达式来说,总是允许一开始就具有想要的类型。因此,有了上面这条简单的规则,每一个表达式都可以执行简单的标识转换。其次,它意味着在程序中为了显得更清晰,允许包含冗余的强制类型转换操作符。
我们把19种简单类型转换统称为拓宽简单类型转换:
在下列情况下,拓宽简单类型转换不会丢失有关数字型值的整体大小信息,其中数字型值会被精确地保留:
非strictfp表达式中的从float到double类型的拓宽简单类型转换可能会丢失被转换值的整体大小的信息。
从int、long到float类型,或从long到double类型的拓宽简单类型转换可能会导致精度损失,即转换结果可能会丢失被转换值的最低几位。在这种情况下,所产生的浮点值是 使用IEEE 754最近舍入模式对整数值进行恰当舍入而得到的版本。
有符号整数值向整数类型T的拓宽转换将直接对该整数值的二进制补码表示进行符号扩展,以填充更宽的格式。
char类型到整数类型T的拓宽转换将对char值的表示进行0扩展,以填充更宽的格式。
尽管有可能会发生精度损失,但是拓宽简单类型转换永远不会导致运行时异常。
拓宽简单类型转换
class Test {
public static void main(String[] args) {
int big = 1234567890;
float approx = big;
System.out.println(big - (int)approx);
}
}
这个程序会打印:
-46
这表明在将int类型转换为float类型时信息丢失了,因为float类型的值无法精确到9位。
我们把22种简单类型转换统称为窄化简单类型转换:
窄化简单类型转换可能会丢失有关数字型值的整体大小信息,并且可能会丢失精度和范围。
从double到float类型的窄化简单类型转换是受IEEE 754舍入规则支配的。这种转换会丢失精度,还会丢失范围,导致从非0的double值产生float类型的0,从double的有穷大小的值产生float的无穷大的值。double类型的NaN会转换成float类型的NaN,而double的无穷大值会转换成同样符号的float的无穷大值。
有符号整数值向整数类型T的窄化转换将直接丢弃最低的n位数字之外的其他所有数字,其中n是用来表示T类型的位数。除了可能会丢失数字型值的大小信息外,这种方式还可能会导致所产生的值的符号位不同于输入值的符号位。
char类型到整数类型T的窄化转换同样将直接丢弃最低的n位数字之外的其他所有数字,其中n是用来表示T类型的位数。除了可能会丢失数字型值的大小信息外,这种方式还可能会导致所产生的值是负数,尽管字符表示的是16位的无符号整数值。
浮点数向整数类型T的窄化转换有两个步骤:
1)第一步,如果T是long类型,浮点数将按照下面的规则被转换为long, 或者如果T是byte、short、char或int类型,浮点数被转换为int:
2)第二步:
尽管有可能出现向上溢出、向下溢出或信息丢失,但是窄化简单类型转换不会导致运行时异常。
窄化简单类型转换
class Test {
public static void main(String[] args) {
float fmin = Float.NEGATIVE_INFINITY;
float fmax = Float.POSITTVE_TNFINITY;
System.out.prlntln("long: " + (long) fmin + ".."+ (long)fmax);
System.out.prIntln("int: " + (int)fmin + ".."+ (int) fmax);
System.out.println("short: " + (short)fmin + ".."+ (short)fmax);
System.out.println("char: " + (int)(char)fmin + ".." + (int)(char)fmax);
System.out.println("byte: " + (byte)fmin + ".." + (byte)fmax);
}
}
这个程序会产生下面的输出:
long: -9223372036854775808..9223372036854775807
int: -2147483648..2147483647
short: 0..-1
char: 0..65535
byte: 0..-1
char、int 和 long 的处理结果并不奇怪,这些类型各自产生了它们能够表示的最小和最大的值。
byte和short的处理结果丢失了数值的符号和大小信息,并且丢失了精度。通过检查最大和最小的int值的最低位就可以理解为什么会产生这样的结果。最小的int值,用十六进制表示是0x80000000
, 而最大的int值是0x7fffffff
。这就解释了short的结果,它们是这两个值的低16位,即0x0000
和0xffff
; 这也解释了 char的结果,它们也是这两个值的低16位,即'\u0000'
和'\uffff'
;这同样解释了 byte的结果,它们是这些值的低8位,即 0x00
和 0xff
。
丢失信息的窄化简单类型转换
class Test{
public static void main(String[] args) {
// A narrowing of int to short loses high bits:
System.out.println("(short)0xl2345678==0x" + Integer.toHexString((short)0x12345678));
// An int value too big for byte changes sign and magnitude:
System.out.println("(byte)255==" + (byte)255);
//A float value too big to fit gives largest int value:
System.out.println("(int)1e20f==" + (int)1e20f);
// A NaN converted to int yields zero:
System.out.println("(int)NaN==" + (int) Float.NaN);
// A double value too large for float yields infinity:
System.out.println("(float)-1el00==" + (float)-1el00);
// A double value too small for float underflows to zero:
System.out.println("(float)1e-50==" + (float)1e-50);
}
}
这个程序会产生下面的输出:
(short)0xl2345678==0x5678
(byte)255==-l
(int)1e20f=2147483647
(int)NaN==0
(float)-1e100==-Infinity
(float)1e-50=0.0
下面的转换将拓宽和窄化简单类型转换结合在了一起:
首先,byte类型经过拓宽简单类型转换转换成了int类型,然后产生的int类型通过窄化简单类型转换转换成char类型。
如果类型S是类型T的子类型,那么任何S类型的引用被转换成T类型的引用时,都会发生拓宽引用类型转换。
拓宽引用类型转换从来都不要求在运行时执行特殊动作,因此也就从来不会在运行时抛出异常。如果要将一个引用当作其他类型处理,而这种处理方式可以在编译时被证明是正确的,那么拓宽引用类型转换才会发生。
我们把6种转换统称为窄化引用类型转换:
这些转换需要在运行时进行测试,以确认实际的引用值是否是新类型的合法值。如果不是,则抛出 ClassCastException。
真超类型这是按照真子集这样的概念翻译的。即A类型可以是A类型自身的超类型。此时这种转换就不属于窄化引用类型转换了。而只有S确定是T的超类型,且不是T自身时,才是窄化引用类型转换。
装箱转换将简单类型的表达式转换为相应的引用类型表达式。特别地,我们把9种转换统称为装箱转换:
最后这条规则是必需的,因为条件操作符会将装箱转换应用于其操作数的类型,并在后续计算中使用转换所得的结果。
在运行时,装箱转换将按如下方式执行:
r.booleanValue() == p
。r.byteValue() == p
。r.charValue() == p
。r.shortValue() == p
。r.intValue() == p
。r.longValue() == p
。r.floatValue() == p
。r.isNaN()
的值为true。r.doubleValue() == p
。r.isNaN()
的值为true。如果被装箱的p值是一个在-128〜127之间(闭区间)的int类型的整数字面常量,或者是布尔字面常量true或false,或者是一个在’\u0000’ 到-\u007f之间(闭区间)的char字符字面常量,那么若a和b是p的任意两个装箱转换的结果, 则 a == b 总是成立的。
理想状态下,装箱给定的简单类型的值p总是会产生同一个引用。但是在实际中,对于现有实现技术而言,也许并不可行,因此,上面这些规则是务实妥协的产物。上面这段话最后一个子句要求特定的常用值总是会被包装成不可区分的对象,而具体实现可以惰性地或积极地缓存这些对象。对于其他值,这些规则不允许程序员对被包装的值的标识做任何假设。 这将允许(但并非强制)共享部分或全部这些引用。注意,允许(非强制)共享long类型的整数字面常量。
这条规则将确保在最常见的情况中,其行为正是用户想要的,而不会因装箱导致过多的性能损害,这一点在小型设备上尤其重要。例如,内存受限的实现可以缓存所有char 和 short值,以及在-32K〜+32K 之间的所有int和long类型值。
如果需要为某种包装器类(Boolean、Byte、Character、Short、Integer、Long、Float 或Double)的一个新实例分配内存,但是却没有足够的可用内存了,那么装箱转换就会产生OutOfMetnoryError。
拆箱转换将引用类型的表达式转换为相应的简单类型表达式。特别地,以下8种转换统称为拆箱转换:
在运行时,拆箱转换将按如下方式执行:
如果某种类型是数字类型,或者是引用类型,但是可以通过拆箱转换将其转换为数字类型,那么它就被认为是可转换为数字类型的。
如果某种类型是整数类型,或者是引用类型,但是可以通过拆箱转换将其转换为整数类型,那么它就被认为是可转换为整数类型的。
假设G是带有n个类型参数的泛型声明,则:
存在从原生类或接口类型G到任意的形式为 G < T 1 , … , T n > G
存在从原生数组类型G[ ] k ^{k} k到任意的形式为 G < T 1 , … , T n > G
使用非受检转换会导致编译时非受检警告,除非所有类型引元 T i T_{i} Ti ( 1 ≤ i ≤ n 1\leq i \leq n 1≤i≤n) 都是无界通配符,或者非受检警告被SuppressWarning注解限制。
非受检转换被用来保证与遗留代码之间的平滑互操作,这些遗留代码是在泛型被引入到 Java之前编写的,并且用到了后来已经转而使用泛型机制(被称为“泛型化”的过程)的类库。在这种情况下(尤其是java.util中的集合框架的用户),遗留代码使用的是原生类型 (例如使用Collection而不是Collection
)。原生类型表达式会作为引元传递给某些类库方法,这些方法使用的是原生类型的参数化版本,并将其作为相应的形参的类型。
这种调用在使用泛型的类型系统中无法证明是静态类型安全的,而拒绝这种调用会导致大量的现有代码无效,并且会阻止它们使用类库的新版本,进而会打击类库提供商使用泛型机制的积极性。为了防止这种令人不悦的情况发生,原生类型可以转换为对该原生类型所引用的泛型声明的任意调用。尽管这种转换并不完美,但是它作为一种向实用性的妥协,还是可以接受的。在这种情况下,会产生非受检警告。
假设G是带有n个类型参数 A 1 , … , A n A_{1},…,A_{n} A1,…,An,且其相应的边界为 U 1 , … , U n U_{1},…,U_{n} U1,…,Un的泛型声明,则存在从参数化类型 G < T 1 , … , T n > G
在除参数化类型之外的其他任意类型上的捕获转换都起到了标识转换的作用。
捕获转换不能被递归地使用。
捕获转换不需要在运行时执行特殊动作,因此也就不会在运行时抛出异常。
设计捕获转换的目的是使通配符更加有用。为了理解这种设计动机,我们先来了解 java.util.Collection.reverse ()
方法:
public static void reverse (List<?> list);
这个方法会反转作为参数提供的列表对象,它可以应用于任何类型的列表,因此使用通配符类型List>作为形参类型是完全合适的。
现在想想可以怎么实现reverse ():
public static void reverse(List<?> list) { rev(list); }
private static <T> void rev(List<T> list) {
List<T> tmp = new ArrayList<T>(list);
for (int i = 0; i < list.size(); i++) {
list.set(i, tmp.get(list.size() - i - 1));
}
}
这个实现需要复制列表,将元素从副本中抽取出来,并将它们插回到原来的数组中。为了以类型安全的方式实现此目的,我们需要给传递进来的列表的元素类型命名,即T , 在私有的服务方法rev()中我们就是这么做的。这需要我们将类型为List>的传递给reverse的引元列表当作引元传递给rev()。 通常,List>是未知类型的列表,对于任意类型T来说,它 都不是List
的超类型,因为允许这种超类型关系是不妥当的。假如有下面的方法:
public static <T> void fill(List<T> l, T obj)
那么,下面的代码将会损坏类型系统:
List<String> ls = new ArrayList<String>();
List<?> 1 = ls;
Collections.fill(l, new Object()); // not legal - but assume it was!
String s = ls.get(0); // ClassCastExGeption - Is contains
// Objects, not Strings.
这样,毫无例外地,我们可以看到在reverse () 中对rev ()的调用会被禁用。如果情况真是如此,那么不得不将reverse () 的签名写作:
public static <T> void reverse(List<T> list)
这并非我们想要的,因为它将实现的信息暴露给了调用者。更糟的是,API 的设计者可能会认为使用通配符的签名正是API的调用者所需要的,而以后才会意识到类型安全的实现被封杀了。
在reverse ()中调用 rev ()实际上是无害的,但是以List>
和List
之间通常意义上的子类型关系为基础做出判断是不恰当的。这个调用无害是因为传递进来的引元无疑是某种类型(虽然是未知的)的列表。如果我们可以将这种未知类型捕获到类型变量x中,那么 就可以推断T是X。这就是捕获转换的精髓
。本规范当然必须处理各种复杂性,例如非平凡的(并且可能是递归定义的)上界和下界,出现多个引元,等等。
精通数学的读者希望能够将捕获转换与已确立的类型理论关联起来,而不熟悉类型理论的读者可以跳过这项讨论,或者先学习合适的背景资料,例如Benjamin Pierce的《Types and Programming Languages》,然后再重读本节内容。
这里先总结一下捕获转换与已确立的类型理论概念之间的关系。通配符类型是既存类型的受限形式。捕获转换大致相当于既存类型的值的开放操作(opening)。表达式e的捕获转换可以看作是包围e的顶层表达式所构成的作用域中e的open操作。
在既存类型上的经典的open操作要求被捕获的类型变量不能转义被开放的表达式。对应于捕获转换的open总是位于足够大的作用域之上,该作用域大到被捕获的类型变量永远都不可能在该作用域之外可见。这种模式的好处在于不需要执行任何close操作,就像在 Atsushi Igarashi 和 Mirko Viroli 在 16th European Conference on Object Oriented Programming (ECOOP 2002 )上发表的论文《On Variance-Based Subtyping for Parametric Type》中所定义的那样。对于通配符的形式化讨论,可以参阅Mads Torgersen、Erik Ernst和Christian Plesner Hansen 在 12th workshop on Foundations of Object Oriented Programming ( FOOL 2005 )上发表的《Wild FJ》。
任意类型都可以通过字符串转换转换成String类型。
简单类型T的值x首先被转换为引用值,即将其作为引元传递给恰当的类实例创建表达式:
之后,这个引用值通过字符串转换转换为String类型。
现在只需要考虑引用值:
toString方法是由原始类Object定义的。许多类都覆盖了它,特别是 Boolean、 Character、 Integer、 Long、 Floaty 、Double 和 String。
任何不是明确允许的转换都是被禁止的。
值集转换是将一个值集中的浮点值映射到另一个值集,但是不改变其类型的过程。
在不是精确浮点的表达式中,值集转换为Java编程语言的实现提供了多项选择:
在FP-严格的表达式中,值集转换不提供任何选择,所有实现都必须遵循相同的方式:
在精确浮点表达式中,只有在对声明为不是精确浮点的方法进行调用时,才需要对浮点扩展指数值集或双精度扩展指数值集中的值进行映射,并且其实现可以选择将方法调用的结果表示为扩展指数值集的元素。
无论是在精确浮点的代码还是非精确浮点的代码中,值集转换类型总是会保持既不是float类型,也不是double类型的值不变。
赋值上下文允许将表达式的值赋值给变量;表达式的类型必须转换成变量的类型。
赋值上下文允许使用下列转换之一:
在应用了上述转换之后,如果所产生的类型是原生类型,那么可能会进行非受检转换。
另外,如果表达式是byte、short、char或int类型的常量表达式,那么:
常量表达式在编译时的窄化意味着诸如下面的代码:
byte theAnswer = 42;
是允许的。如果没有窄化,那么整数字面常量42的类型为int这个事实,将迫使我们必须将其强制类型转换为byte:
byte theAnswer = (byte)42; // cast is permitted but not required
最后,空类型的值(空引用是唯一的这种值)可以被赋值给任意引用类型,导致产生该类型的一个空引用。
如果转换链中包含两个参数化类型,并且它们之间不存在子类型关系,那么这就是一个编译时错误。
下面是这种非法转换链的一个实例:
Integer, Comparable<Integer>, Comparable, Comparable<String>
这个转换链的前三个元素与拓宽引用类型转换相关,而最后一项是由非受检转换从其前面的转换派生出来的。但是,这并非合法的赋值转换,因为该链条包含两个参数化类型 Comparable
和 Comparable
,它们彼此不是子类型。
如果表达式的类型不能经由在赋值上下文中所允许的类型转换而转换为变量的类型,那 么就会产生编译时错误。
如果表达式的类型可以经由赋值转换被转换为变量的类型,我们就称该表达式(或它的值)是可赋值给该变量的,或者称该表达式的类型与该变量的类型是赋值兼容的。
如果变量的类型是float或double,那么值集转换就会作用于下面的类型转换所产生的结果v上:
赋值转换可能会产生下列异常:
简单类型的赋值转换
class Test {
public static void main(String[] args) {
short s = 12; // narrow 12 to short
float f = s; // widen short to float
System.out.println("f=" + f);
char c = '\u0123';
long 1 = c; // widen char to Long
System.out.priritln("l=0x" + Long.toString (1,16));
f = 1.23f;
double d = f; // widen float to double
System.out.println ("d=" + d);
运行该程序会产生如下输出:
f=12.0
1=0x123
d=l.2300000190734863
但是下面的程序却会产生编译时错误:
class Test {
public static void main(String[] args) {
short s = 123;
char c = s; // error: would require cast
s = c; // error: would require cast
).
)
因为并非所有short值都是char值,同样并非所有char值都是short值。
引用类型的赋值转换
class Point{ int x, y; }
class Point3D extends Point { int z; }
interface Colorable { void setColor(int color); }
class ColoredPoint extends Point implements Colorable {
int color;
public void setColor(int color) { this.color = color; }
}
class Test {
public static void main(String[] args){
// Assignments to variables of class type:
Point p = new Point();
p = new Point3D();
// OK because Point3D is a subclass of Point
Point3D p3d = p;
// Error: will require a cast because a Point
// might not be a Point3D (even though it is,
// dynamically, in this example,)
// Assignments to variables of type Object:
Object o = p; // OK: any object to pbject
int[] a = new int[3];
Object o2 = a; // OK: an array to Object
// Assignments to variables of interface type:
ColoredPoint cp = new ColoredPoint();
Colorable c = cp;
// OK: ColoredPoint implements Colorable
// Assignments to variables of array type:
byte[] b = new byte[4];
a = b;
// Error: these are not arrays of the same primitive type
Point3D[] p3da = new Point3D[3];
Point[] pa = p3da;
// OK: since we can assign a Point3D to a Point
p3da = pa;
// Error: (cast needed) since a Point
// can't be assigned to a Point3D
}
}
下面的测试程序解释了在引用值上进行的赋值转换,但是它不能编译,原因在注释中进行了阐述。可以将这个示例与上面的例子进行对比。
class Point { int x, y; }
interface Colorable { void setColor(int color); }
class ColoredPoint extends Point implements Colorable {
int color;
public void setColor(int color) { this.color = color; }
}
class Test {
public static void main(String[] args) {
Point p = new Point();
ColoredPoint cp = new ColoredPoint();
// Okay because ColoredPoint is a subclass of Point:
p = cp;
// Okay because ColoredPoint implements Colorable:
Colorable c = cp;
// The following cause compile-time errors because
// we cannot be sure they will succeed, depending on
// the run-time type of p; a run-time check will be
// necessary for the needed narrowing conversion and
// must be indicated by including a cast:
cp = p; // p might be neither a ColoredPoint
// nor a subclass of ColoredPoint
c = p; // p might not implement Colorable
}
}
数组类型的赋值转换
class Point{ int x, y; }
class ColoredPoint extends Point { int color; }
class Test {
public static void main(String[] args) {
long[] veclong = new long[100];
Object o = veclong; // okay
Long l = veclong; // compile-time error
short[] vecshort = veclong; // compile-time error
Point[] pvec = new Point[100];
ColoredPoint[] cpvec = new ColoredPoint[100];
pvec = cpvec; // okay
pvec[0] = new Point(); // okay at compile time,
// but would throw an
// exception at run time
cpvec =pvec; // compile-time error
在上面的例子中:
cpvec = (ColoredPoint.[] ) pvec; // OK, but may throw an
// exception at run time
方法调用上下文将方法调用或构造器调用中的引元值赋值给对应的形参。
严格的调用上下文允许使用下列转换:
宽松的调用上下文允许使用更多的转换,因为对于使用它们的特定调用,是无法使用严 格的调用上下文来找到可应用的任何声明的。宽松的调用上下文允许使用下列转换:
在应用了上述用于调用上下文的转换之后,如果所产生的类型是原生类型, 那么就可能会进行非受检转换。
空类型的值(空引用是唯一的这种值)可以赋值给任意引用类型。
如果转换链中包含两个参数化类型,并且它们之间不存在子类型关系,那么这就是一个编译时错误。
如果表达式的类型不能通过宽松的调用上下文中所允许的类型转换而被转换为参数的类型,那么就会产生编译时错误。
如果引元表达式的类型是float或double,那么就会在类型转换之后应用值集转换:
调用上下文可能会抛出的异常包括:
特别地,无论是严格的调用上下文还是宽松的调用上下文,都不包括在赋值上下文中所允许的隐式整数常量表达式窄化转换。Java编程语言的设计者们认为包含这类隐式窄化转换 将会对重载方法的匹配解析规则带来额外的复杂性。
因此,下面的程序:
class Test {
static int m(byte a, int b) { return a+b; }
static int m(short a, short b) { return a-b; }
public static void main(String[] args) {
System.out.println(m(12r 2)); // compile-time error
}
}
将会导致编译时错误,因为整数字面常量12和2都是int类型,因此无论是哪个m方法在重载解析规则之下都不能匹配。包含隐式整数常量窄化转换的语言都需要添加额外的规则来解决像这个示例这样的情况。
字符串上下文只能应用于二元操作符+的两个操作数之一,该操作数不是String,但另一个操作数是String。
在这些上下文中,目标类型总是String,并且非String操作数的String转换总是会发生,然后+操作符会按照第15.18.1节中指定的方式进行计算。
强制类型转换上下文允许强制类型转换操作符的操作数被转换为强制类型转换操作符显式指定的类型。
强制类型转换上下文允许使用下列转换:
在该类型转换之后可以应用值集转换。
强制类型转换的编译时合法性允许下列情况:
表5-1和表5-2枚举了在给定的强制类型转换中使用到的转换,其中,每种转换用一个符号来表示:
在表中,符号中间的逗号表示强制类型转换可以使用某种转换,后面跟着另外一种转换。Object 类型表示除了 8 种包装器类型 Boolean、Byte、Short、Character、 Integer、 Long、Float 和 Double之外的任意引用类型。
表5-1到简单类型的强制类型转换
表5-2到引用类型的强制类型转换
给定编译时引用类型S (源)和T (目标),如果在下列规则中不会产生任何编译时错误, 那么就存在从S到T的强制类型转换。
如果S是类类型:
而且,如果T有超类型X, S有超类型Y,且X和Y都是可证不同的参数化类型,而X和Y的擦除又是相同的,那么就会产生编译时错误。
如果S是接口类型:
如果S是类型变量,那么通过用S的上界来替换S,可以递归地应用这个算法。
如果S是交集类型 A 1 A_{1} A1&…& A n A_{n} An, 若存在某个 A i A_{i} Ai( 1 ≤ i ≤ n 1\leq i \leq n 1≤i≤n),使得 A i A_{i} Ai不能通过这个算法被强制类型转换为T,那么就是一个编译时错误。即,强制类型转换成功与否是由交集类型最严苛组成部分所决定的。
如果S是数组类型SC[],即由sc类型的元素构成的数组:
引用类型的强制类型转换
class Point { int x, y; }
interface Colorable { void setColor(int color); }
class ColoredPoint extends Point implements Colorable {
int color;
public void setColor(int color) { this.color = color; }
}
final class EndPoint extends Point {}
class Test (
public static void main(String[] args) {
Point p = new Point();
ColoredPoint cp = new ColoredPoint();
Colorable c;
// The following may cause errors at run time because
// we cannot be sure they will succeed; this possibility
// is suggested by the casts:
cp = (ColoredPoint)p;
c = (Colorable)p;
// The following are incorrect at compile time because
// they can never succeed as explained in the text:
Long l = (Long)p; // compile-time error #1
EndPoint e = new EndPoint();
c = (Colorable)e; // compile-time error #2
}
}
这里,第一个编译时错误是因为类类型Long和Point是不相关的(即,它们并不相同, 并且互相之间没有子类关系),因此它们之间的强制类型转换总是失败。
第二个编译时错误是因为EndPoint类型的变量不能引用实现了 Colorable接口的值, 因为EndPoint是final类型,而final类型的变量总是持有运行时类型和编译时类型相同的值。因此,变量e的运行时类型必须精确地是EndPoint类型,而EndPoint类型没有实现Colorable。
数组类型的强制类型转换
class Point{
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
public String toString() { return "(" + x + "," + y + ")"; }
}
interface Colorable { void setColor(int color); }
class ColoredPoint extends Point implements Colorable {
int color;
ColoredPoint(int x, int y, int color) {
super(x, y); setColor(color);
}
public void setColor(int color) {
this.color = color;
}
public String toString() {
return super. toString () + "@" + color;
}
}
class Test {
public static void main(String[] args) {
Point[) pa = new ColoredPoint[4];
pa[0] = new ColoredPoint(2, 2, 12);
pa[1] = new ColoredPoint(4, 5, 24);
ColoredPoint[] cpa = (ColoredPoint[])pa;
System.out.print("cpa: {");
for (int i = 0; i < cpa.length; i++)
System.out.print((i == 0 ? " " : ", ") + cpa[i]);
System.out.printin(" }");
这个程序不会产生任何编译时错误,并且会产生如下输出:
cpa: { (2,2)@12, (4,5)@24, null, null )
当且仅当S <: T 时,从类型S到类型T的强制类型转换是静态可知正确的。
从类型S到参数化类型T的强制类型转换是非受检的,除非下面的条件至少满足一条:
从类型S到类型变量T的强制类型转换是非受检的,除非S<:T。
如果存在某个 T i T_{i} Ti( 1 ≤ i ≤ n 1\leq i \leq n 1≤i≤n),使得从S到 T i T_{i} Ti的强制类型转换是非受检的,那么从类型 S到交集类型 T 1 T_{1} T1&…& T n T_{n} Tn的强制类型转换就是非受检的。
如果从|S|到|T|的强制类型转换是静态可知正确的,那么从S到非交集类型T的非受检强制类型转换是完全非受检的,否则,这种类型转换就是部分非受检的。
如果对于所有 i ( 1 ≤ i ≤ n 1\leq i \leq n 1≤i≤n) , 从S到 T i T_{i} Ti的强制类型转换要么是静态可知正确的,要么是完全非受检的,那么从类型S到交集类型 T 1 T_{1} T1&…& T n T_{n} Tn的强制类型转换就是完全非受检的。 否则,这种类型转换就是部分非受检的。
非受检强制类型转换会产生编译时非受检警告,除非用Suppresswarnings注解进行了压制。
如果某个强制类型转换不是静态可知正确的,并且不是非受检的,那么它就是受检的。 如果某个到引用类型的强制类型转换不产生编译时错误,那么它必是以下几种情况之一:
注意 当上述规则首次应用于任意给定的强制类型转换时,R不能是接口,但是如果这些规则后续进行递归地应用,那么R就可以是接口,因为运行时的引用值可以引用元素类型是接口类型的数组。
下面的算法用于检查某个对象的运行时类型R是否与在强制类型转换操作符中命名的类型的擦除具有赋值兼容性。如果该检查会抛出运行时异常,那么应该是 ClassCastException。
如果R是普通类(不是数组类):
如果R是接口:
如果R是表示数组类型RC[ ] 的类,即由RC类型的元素构成的数组:
运行时不兼容的类型
class Point { int x, y; }
interface Colorable { void setcolor(int color); }
class ColoredPoint extends Point implements Colorable {
int color;
public void setcolor(int color) { this.color = color; }
}
class Test {
public static void main(String[] args) {
Point[] pa = new Paint.[100];
// The following line will throw a CLassCastException:
ColoredPoint[] cpa =(ColoredPoint[])pa;
System.out.printin(cpa[0]);
int[] shortvec = new int[2];
Object o = shortvec;
// The following line will throw a ClassCastException:
Colorable c = (Colorable)o;
c.setColor(0);
}
}
这个程序使用了强制类型转换,可以通过编译,但是它会在运行时抛出异常,因为要类型转换的类型之间不兼容。
数字上下文可以应用于算术操作符的操作数。
数字上下文允许使用下列转换之一:
数字提升的处理过程如下:给定算术操作符及其引元表达式,其引元会被转换为推断出来的目标类型T。T是在提升过程中确定的,它使得每个引元表达式都可以被转换为T,并且该算术操作就是针对T类型的值而定义的。
有两种数字提升:一元数字提升和二元数字提升。
某些操作符会将一元数字提升应用于单个操作数,而应用的结果必须产生一个数字类型的值:
在上述转换之后,如果需要,还可以应用值集转换。
一元数字提升是在属于下列情况的表达式上执行的:
一元数字提升
class Test {
public static void main(String[] args) {
byte b = 2;
int a[] = new int[b]; // dimension expression promotion
char c = '\u0001';
a[c] = 1; // index expression promotion
a[0] = -c; // unary 一 promotion
System.out.println("a: " + a[0] + "," + a[1]);
b = -1;
int i = ~b; // bitwise complement promotion
System.out.println("~Ox" + Integer.toHexString(b) + "==0x" + Integer.toHexString(i));
i = b << 4L; // shift promotion (left operand)
System.out.println("0x" + Integer.toHexString(b) + "<<4L=0x" + Integer.toHexString(i));
}
}
这个程序将产生如下输出:
a: -1,1
-Oxffffffff==OxO
Oxffffffff<<4L==0xfffffff0
如果一个操作符将二元数字提升应用于一对操作数,其中每个操作数表示的都必须是能够转换成某种数字类型的值,那么就会依次应用下面的规则:
1 )如果任意一个操作数是引用类型,那么它会接受拆箱转换。
2)拓宽简单类型转换按照下面指定的规则转换其中一个操作数或同时转换两个操作数:
在类型转换之后,如果需要,那么值集转换可以应用于每个操作数。
二元数字提升是在特定操作符的操作数上执行的:
二元数字提升
class Test {
public static void main(String[] args) {
int i = 0;
float f = 1.Of;
double d = 2.0;
// First int*float is promoted to float*float, then
// float==double is promoted to double==double:
if (i * f == d) System.out.println("oops");
// A char&byte is promoted to int&int:
byte b = Ox1f;
char c = 'G';
int control = c & b;
System.out.println(Integer.toHexString(control));
// Here int:float is promoted to float:float:
f = (b==0) ? i : 4.Of;
System.out.println(1.0/f) ;
这个程序将产生如下输出: fl26l
7
0.25
这个示例将ASCII字符G转换成了 ASCII控制字符control-G ( BEL), 方法是屏蔽除该字符的低5位之外的其他所有位。7是这个控制字符的数值。