JAVA語言規範JAVA SE 8 - 類型轉換與上下文

用Java編程語言編寫的每一個表達式要麼不產生任何結果,要麼有一個可以在編譯時推斷出來的類型。當表達式出現在大多數的上下文中時,它都必須與該上下文所期望的類型兼容,這個類型被稱爲目標類型。爲方便起見,表達式與其周圍上下文的兼容性可以藉助下面兩種方式實現:

  • 首先,對於被稱爲合成表達式的表達式推斷出來的類型可能受到目標類型的影響,因此相同的表達式在不同的上下文中可能會有不同的類型。
  • 其次,在表達式的類型被推斷出來後,會執行從表達式的類型到目標類型的隱式類型轉換。

如果這兩種策略都無法產生恰當的類型,那麼就會產生編譯時錯誤。

確定一個表達式是否是合成表達式的規則,以及在是合成表達式的情況下,其在特定上下文中的類型與兼容性,將依據上下文的種類以及表達式的形式而變化。除了影響表達式的類型之外,目標類型在某些情況下還會影響表達式運行時的行爲,以便產生具有恰當類型的值。

類似地,確定目標類型是否允許隱式轉換的規則,將依據上下文的種類、表達式的類型以及作爲特例的常量表達式的值而變化。從類型S到類型T的轉換將允許S類型的表達式在編譯時被當作T類型進行處理。在某些情況下,這樣做要求在運行時要有相應的動作去檢查這種轉換的有效性,或者將該表達式運行時的值轉譯爲適合新類型T的形式。

編譯時和運行時的類型轉換

  • Object類型到Thread類型的轉換要求進行運行時檢查,以確保運行時的值確實是 Thread類或其子類的實例,如果不是,則拋出異常。
  • Thread類型到Object類型的轉換不要求在運行時做任何動作。Thread是Object的子類,因此任何Thread類型的表達式所產生的引用都是有效的Object類型的引用值。
  • int類型到long類型的轉換要求在運行時對32位整數值進行符號擴展,擴展成爲64 位long表示形式。這種轉換不會丟失任何信息。
  • double類型到long類型的轉換要求進行從64位浮點數值到64位整數表示形式的非平凡轉換。這種轉換有可能會造成信息丟失,具體取決於運行時的實際數值。

在Java編程語言中可能會執行的特定轉換可以分爲以下幾種寬泛的種類:

  • 標識轉換;
  • 拓寬簡單類型轉換;
  • 窄化簡單類型轉換;
  • 拓寬引用類型轉換;
  • 窄化引用類型轉換;
  • 裝箱轉換;
  • 拆箱裝換;
  • 非受檢轉換;
  • 捕獲轉換;
  • 字符串轉換;
  • 值集轉換。

在下列6種轉換上下文中,合成表達式可能會受到上下文的影響,或者會發生隱式轉換。每種上下文對於確定合成表達式的類型有不同的規則,並且都允許執行上述某些種類的轉換,但是同時又會禁止其他種類的轉換。這些上下文是:

  • 賦值上下文:表達式的值被綁定到某個具名變量上。在這裏,可以拓寬簡單類型和引用類型,值可以被裝箱和拆箱,也可以窄化某些簡單常量表達式,還可以進行非受檢轉換。
  • 嚴格的調用上下文:參數被綁定到構造器或方法的形參上。在這裏,可以進行拓寬簡單類型、拓寬引用類型和非受檢轉換。
  • 寬鬆的調用上下文:與嚴格的調用上下文一樣, 引元被綁定到形參上。如果只使用嚴格的調用上下文無法找到任何可應用的聲明, 那麼方法或構造器調用就可能提供這種上下文。除了拓寬轉換和非受檢轉換,該上下文還允許執行裝箱轉換和拆箱轉換。
  • 字符串上下文:任何類型的值都會被轉換爲String類型 的對象。
  • 強制類型轉換上下文:表達式的值被轉換爲由強制類型轉換操作符顯式指定的類型。強制類型轉換上下文比賦值上下文和寬鬆的調用上下文 更具包容性,因爲它允許除字符串轉換之外的任何具體轉換,但是在運行時會檢查某些強制類型轉換到引用類型的操作的正確性。
  • 數字上下文:數字型操作符的操作數會拓寬爲公共類型,以確保操作可以執行。

“轉換”還能用來非特指地描述在某個特定轉換中的任何轉換。例如,我們說某個局部變量的初始化器的表達式將接受“賦值轉換”,這意味着系統會根據賦值上下文的規則隱式地爲該表達式選擇一種具體的轉換。

各種轉換上下文

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種簡單類型轉換統稱爲拓寬簡單類型轉換:

  • byte 轉換爲 short、int、long、float 或 double;
  • short 轉換爲 int、long、float 或 double;
  • char 轉換爲 int、long、float 或 double;
  • int 轉換爲 long、float 或 double;
  • long 轉換爲 float 或 double;
  • float轉換爲double.

在下列情況下,拓寬簡單類型轉換不會丟失有關數字型值的整體大小信息,其中數字型值會被精確地保留:

  • 從一種整數類型轉換爲另一種整數類型;
  • 從byte、short或char類型轉換爲浮點類型;
  • 從int轉換爲double類型;
  • 在strictfp表達式中從float轉換爲double類型

非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種簡單類型轉換統稱爲窄化簡單類型轉換:

  • short 轉換爲 byte 或 char;
  • char 轉換爲 byte 或 short;
  • int 轉換爲 byte、short 或 char;
  • long 轉換爲 byte、short、char 或 int;
  • float 轉換爲 byte、short 、 char、int 或 long;
  • double 轉換爲 byte、short、char、int、long 或 float。

窄化簡單類型轉換可能會丟失有關數字型值的整體大小信息,並且可能會丟失精度和範圍。

從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:

  • 如果該浮點數是NaN,轉換的第一步的結果就是int類型或long類型的0。
  • 否則,如果該浮點數不是無窮大,那麼該浮點值將被舍入到整數值v, 使用IEEE 754的向0舍入模式進行向0舍入。這會產生下面兩種情況:
    a) 如果T是long類型,並且這個整數值可以表示成long類型,那麼第一步的結果就是long類型的值V。
    b) 否則,如果這個整數值可以表示成int類型,那麼第一步的結果就是int類型的值V。
  • 否則,必爲下列兩種情況之一:
    c) 這個值太小了(具有很大絕對值的負值,或者是負無窮大),第一步的結果就是 int 或 long 類型能夠表示的最小值。
    d) 這個值太大了(具有很大絕對值的正值,或者是正無窮大),第一步的結果就是 int 或 long 類型能夠表示的最大值。

2)第二步:

  • 如果T是int或long類型,該轉換的結果就是第一步的結果。
  • 如果T是byte、char或short類型,該轉換的結果就是第一步的結果進行窄化轉換到T類型的結果。

儘管有可能出現向上溢出、向下溢出或信息丟失,但是窄化簡單類型轉換不會導致運行時異常。

窄化簡單類型轉換

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位,即0x00000xffff ; 這也解釋了 char的結果,它們也是這兩個值的低16位,即'\u0000''\uffff';這同樣解釋了 byte的結果,它們是這些值的低8位,即 0x000xff

丟失信息的窄化簡單類型轉換

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轉換爲char

首先,byte類型經過拓寬簡單類型轉換轉換成了int類型,然後產生的int類型通過窄化簡單類型轉換轉換成char類型。

拓寬引用類型轉換

如果類型S是類型T的子類型,那麼任何S類型的引用被轉換成T類型的引用時,都會發生拓寬引用類型轉換。

拓寬引用類型轉換從來都不要求在運行時執行特殊動作,因此也就從來不會在運行時拋出異常。如果要將一個引用當作其他類型處理,而這種處理方式可以在編譯時被證明是正確的,那麼拓寬引用類型轉換纔會發生。

窄化引用類型轉換

我們把6種轉換統稱爲窄化引用類型轉換:

  • 將任意的S類型引用轉換爲任意的T類型引用,如果S是T的真超類型 。
    有一種重要的特例,即從Object類類型到任意其他引用類型的窄化引用類型轉換。
  • 將任意的類類型C轉換爲任意的非參數化接口類型K,如果C不是final的並且沒有實現K。
  • 將任意的接口類型J轉換爲任意的不是final的非參數化類類型C。
  • 如果J不是K的子接口,將任意的接口類型J轉換爲任意的非參數化接口類型K。
  • 將接口類型Cloneable 和 java. io.Serializable 轉換爲任意的數組類型T[ ] 。
  • 如果SC和TC是引用類型,並且存在從SC到TC的窄化引用類型轉化,將任意數組類型SC[ ]轉換爲任意數組類型TC[ ]。

這些轉換需要在運行時進行測試,以確認實際的引用值是否是新類型的合法值。如果不是,則拋出 ClassCastException。

真超類型這是按照真子集這樣的概念翻譯的。即A類型可以是A類型自身的超類型。此時這種轉換就不屬於窄化引用類型轉換了。而只有S確定是T的超類型,且不是T自身時,纔是窄化引用類型轉換。

裝箱轉換

裝箱轉換將簡單類型的表達式轉換爲相應的引用類型表達式。特別地,我們把9種轉換統稱爲裝箱轉換:

  • 從boolean類型轉換爲Boolean類型;
  • 從byte類型轉換爲Byte類型;
  • 從short類型轉換爲Short類型;
  • 從char類型轉換爲Character類型;
  • 從int類型轉換爲Integer類型;
  • 從long類型轉換爲Long類型;
  • 從float類型轉換爲Float類型;
  • 從double類型轉換爲Double類型;
  • 從空類型轉換爲空類型。

最後這條規則是必需的,因爲條件操作符會將裝箱轉換應用於其操作數的類型,並在後續計算中使用轉換所得的結果。

在運行時,裝箱轉換將按如下方式執行:

  • 如果p是boolean類型的值,那麼裝箱轉換將p轉換爲類的引用r ,並且其類型爲Boolean ,使得 r.booleanValue() == p
  • 如果p是byte類型的值,那麼裝箱轉換將p轉換爲類的引用r,並且其類型爲Byte,使得 r.byteValue() == p
  • 如果p是char類型的值,那麼裝箱轉換將p轉換爲類的引用r,並且其類型爲Character,使得 r.charValue() == p
  • 如果p是short類型的值,那麼裝箱轉換將p轉換爲類的引用r,並且其類型爲Short, 使得 r.shortValue() == p
  • 如果p是int類型的值,那麼裝箱轉換將p轉換爲類的引用r,並且其類型爲Integer, 使得 r.intValue() == p
  • 如果p是long類型的值,那麼裝箱轉換將p轉換爲類的引用r,並且其類型爲Long,使得 r.longValue() == p
  • 如果p是float類型的值,那麼:
    -如果p不是NaN,那麼裝箱轉換將p轉換爲類的引用r,並且其類型爲Float, 使 r.floatValue() == p
    -否則,裝箱轉換將p轉換爲類的引用r,並且其類型爲Float,使得r.isNaN()的值爲true。
  • 如果p是double類型的值,那麼:
    -如果p不是NaN,那麼裝箱轉換將p轉換爲類的引用r,並且其類型爲Double,使 得 r.doubleValue() == p
    -否則,裝箱轉換將p轉換爲類的引用工,並且其類型爲Double,使得r.isNaN() 的值爲true。
  • 如果p是任何其他類型的值,那麼裝箱轉換等價於標識轉換。

如果被裝箱的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種轉換統稱爲拆箱轉換:

  • 從Boolean類型轉換爲boolean類型;
  • 從Byte類型轉換爲byte類型;
  • 從Short類型轉換爲short類型;
  • 從Character類型轉換爲char類型;
  • 從Integer類型轉換爲int類型;
  • 從Long類型轉換爲long類型;
  • 從Float類型轉換爲float類型;
  • 從Double類型轉換爲double類型。

在運行時,拆箱轉換將按如下方式執行:

  • 如果r是Boolean類型的引用,那麼拆箱轉換將r轉換爲r.booleanValue();
  • 如果r是Byte類型的引用,那麼拆箱轉換將r轉換爲r r.byteValue();
  • 如果r是Character類型的引用,那麼拆箱轉換將r轉換爲r.characterValue();
  • 如果r是Short類型的引用,那麼拆箱轉換將r轉換爲r.shortValue();
  • 如果r是Integer類型的引用,那麼拆箱轉換將r轉換爲r.intValue();
  • 如果r是Long類型的引用,那麼拆箱轉換將r轉換爲r.longValue();
  • 如果r是Float類型的引用,那麼拆箱轉換將r轉換爲r.floatValue();
  • 如果r是Double類型的引用,那麼拆箱轉換將r轉換爲r.doubleValue();
  • 如果r是null,那麼拆箱轉換會拋NullPointerException。

如果某種類型是數字類型,或者是引用類型,但是可以通過拆箱轉換將其轉換爲數字類型,那麼它就被認爲是可轉換爲數字類型的。

如果某種類型是整數類型,或者是引用類型,但是可以通過拆箱轉換將其轉換爲整數類型,那麼它就被認爲是可轉換爲整數類型的。

非受檢轉換

假設G是帶有n個類型參數的泛型聲明,則:
存在從原生類或接口類型G到任意的形式爲G<T1,,Tn>G<T_{1},…,T_{n}>的參數化類型的非受檢轉換。
存在從原生數組類型G[ ]k^{k}到任意的形式爲G<T1,,Tn>G<T_{1},…,T_{n}> [ ]k^{k}的數組類型的非受檢轉換。([ ]k^{k}代表示左維數組類型。)

使用非受檢轉換會導致編譯時非受檢警告,除非所有類型引元TiT_{i} (1in1\leq i \leq n) 都是無界通配符,或者非受檢警告被SuppressWarning註解限制。

非受檢轉換被用來保證與遺留代碼之間的平滑互操作,這些遺留代碼是在泛型被引入到 Java之前編寫的,並且用到了後來已經轉而使用泛型機制(被稱爲“泛型化”的過程)的類庫。在這種情況下(尤其是java.util中的集合框架的用戶),遺留代碼使用的是原生類型 (例如使用Collection而不是Collection<String> )。原生類型表達式會作爲引元傳遞給某些類庫方法,這些方法使用的是原生類型的參數化版本,並將其作爲相應的形參的類型。

這種調用在使用泛型的類型系統中無法證明是靜態類型安全的,而拒絕這種調用會導致大量的現有代碼無效,並且會阻止它們使用類庫的新版本,進而會打擊類庫提供商使用泛型機制的積極性。爲了防止這種令人不悅的情況發生,原生類型可以轉換爲對該原生類型所引用的泛型聲明的任意調用。儘管這種轉換並不完美,但是它作爲一種向實用性的妥協,還是可以接受的。在這種情況下,會產生非受檢警告。

捕獲轉換

假設G是帶有n個類型參數A1,,AnA_{1},…,A_{n},且其相應的邊界爲U1,,UnU_{1},…,U_{n}的泛型聲明,則存在從參數化類型G<T1,,Tn>G<T_{1},…,T_{n}>到參數化類型G<S1,,Sn>G<S_{1},…,S_{n}>的 捕獲轉換,其中,1in1\leq i \leq n

  • 如果TiT_{i}是形式爲 ?的通配符類型引元,那麼SiS_{i}就是一個全新的類型變量,其上界爲Ui[Ai:=Si,,An:=Sn]U_{i}[A_{i}:=S_{i}, …,A_{n}:=S_{n}],且其下界爲空類型。
  • 如果TiT_{i}是形式爲 ?extends BiB_{i}的通配符類型引元,那麼SiS_{i}就是一個全新的類型變量,
    其上界爲glb(BiB_{i} , Ui[Ai:=Si,,An:=Sn]U_{i}[A_{i}:=S_{i}, …,A_{n}:=S_{n}]),且其下界爲空類型。
    glb(V1V_{1},……,VmV_{m})被定義爲 V1V_{1} &…& VmV_{m}
    對於任意兩個類(而非接口)ViV_{i}VjV_{j},如果ViV_{i}不是VjV_{j}的子類,則這是一種編譯時錯誤,反之亦然。
  • 如果TiT_{i}是形式爲 ?super BiB_{i}的通配符類型引元,那麼 SiS_{i}就是一個全新的類型變量, 其上界爲Ui[Ai:=Si,,An:=Sn]U_{i}[A_{i}:=S_{i}, …,A_{n}:=S_{n}] ,且其下界爲BiB_{i}
  • 否則,SiS_{i} = TiT_{i}

在除參數化類型之外的其他任意類型上的捕獲轉換都起到了標識轉換的作用。

捕獲轉換不能被遞歸地使用。

捕獲轉換不需要在運行時執行特殊動作,因此也就不會在運行時拋出異常。

設計捕獲轉換的目的是使通配符更加有用。爲了理解這種設計動機,我們先來了解 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<T>的超類型,因爲允許這種超類型關係是不妥當的。假如有下面的方法:

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<T>之間通常意義上的子類型關係爲基礎做出判斷是不恰當的。這個調用無害是因爲傳遞進來的引元無疑是某種類型(雖然是未知的)的列表。如果我們可以將這種未知類型捕獲到類型變量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首先被轉換爲引用值,即將其作爲引元傳遞給恰當的類實例創建表達式:

  • 如果T是boolean,那麼就使用new Boolean(x)。
  • 如果T是char,那麼就使用new Character(x)。
  • 如果 T 是 byte、short 或 int,那麼就使用 new Integer(x)。
  • 如果T是long,那麼就使用new Long(x)。
  • 如果T是float,那麼就使用new Float(x)。
  • 如果T是double,那麼就使用new Double(x)。

之後,這個引用值通過字符串轉換轉換爲String類型。

現在只需要考慮引用值:

  • 如果該引用爲null,那麼它將轉換爲字符串"null"(四個ASCII字符n、u、l、l)
  • 否則,執行轉換,即不傳遞任何引元而調用被引用對象上的tostring方法;但是如果調用tostring方法的結果是null,那麼就用字符串"null"替代。

toString方法是由原始類Object定義的。許多類都覆蓋了它,特別是 Boolean、 Character、 Integer、 Long、 Floaty 、Double 和 String。

被禁止的轉換

任何不是明確允許的轉換都是被禁止的。

值集轉換

值集轉換是將一個值集中的浮點值映射到另一個值集,但是不改變其類型的過程。

在不是精確浮點的表達式中,值集轉換爲Java編程語言的實現提供了多項選擇:

  • 如果要轉換的值是浮點擴展指數值集中的元素,那麼其實現方式可以是將該值映射爲浮點值集中最接近的元素。這種轉換可能會產生上溢(此時該值會替換爲與其符號 m 相同的無窮大)或下溢(此時該值會丟失精度,因爲它會被替換爲與其符號相同的非規格化數或0)。
  • 如果要轉換的值是雙精度浮點擴展指數值集中的元素,那麼其實現方式可以是將該值映射爲雙精度浮點值集中最接近的元素。這種轉換可能會產生上溢(此時該值會替 換爲與其符號相同的無窮大)或下溢(此時該值會丟失精度,因爲它會被替換爲與其符號相同的非規格化數或0)。

在FP-嚴格的表達式中,值集轉換不提供任何選擇,所有實現都必須遵循相同的方式:

  • 如果要轉換的值是float類型,並且它不是浮點值集中的元素,那麼其實現方式必須將該值映射爲浮點值集中最接近的元素。這種轉換可能會產生上溢或下溢。
  • 如果要轉換的值是double類型,並且它不是雙精度浮點值集中的元素,那麼其實現方式必須將該值映射爲雙精度浮點值集中最接近的元素。這種轉換可能會產生上溢或下溢。

在精確浮點表達式中,只有在對聲明爲不是精確浮點的方法進行調用時,才需要對浮點擴展指數值集或雙精度擴展指數值集中的值進行映射,並且其實現可以選擇將方法調用的結果表示爲擴展指數值集的元素。

無論是在精確浮點的代碼還是非精確浮點的代碼中,值集轉換類型總是會保持既不是float類型,也不是double類型的值不變。

賦值上下文

賦值上下文允許將表達式的值賦值給變量;表達式的類型必須轉換成變量的類型。

賦值上下文允許使用下列轉換之一:

  • 標識轉換。
  • 拓寬簡單類型轉換。
  • 拓寬引用類型轉換。
  • 裝箱轉換,後跟可選的拓寬引用類型轉換。
  • 拆箱轉換,後跟可選的拓寬簡單類型轉換。

在應用了上述轉換之後,如果所產生的類型是原生類型,那麼可能會進行非受檢轉換。

另外,如果表達式是byte、short、char或int類型的常量表達式,那麼:

  • 如果變量的類型是byte, short或char,並且常量表達式的值是可以用該變量的類型表示的,那麼就可以使用窄化簡單類型轉換。
  • 如果變量的類型是下列情形之一,那麼就可以使用窄化簡單類型轉換,後跟裝箱轉換:
    -Byte,並且常量表達式的值可以用byte類型表示。
    -Short,並且常量表達式的值可以用short類型表示。
    -Character,並且常量表達式的值可以用char類型表示。

常量表達式在編譯時的窄化意味着諸如下面的代碼:

byte theAnswer = 42;

是允許的。如果沒有窄化,那麼整數字面常量42的類型爲int這個事實,將迫使我們必須將其強制類型轉換爲byte:

byte theAnswer =byte42;	// cast is permitted but not required

最後,空類型的值(空引用是唯一的這種值)可以被賦值給任意引用類型,導致產生該類型的一個空引用。

如果轉換鏈中包含兩個參數化類型,並且它們之間不存在子類型關係,那麼這就是一個編譯時錯誤。

下面是這種非法轉換鏈的一個實例:

Integer, Comparable<Integer>, Comparable, Comparable<String>

這個轉換鏈的前三個元素與拓寬引用類型轉換相關,而最後一項是由非受檢轉換從其前面的轉換派生出來的。但是,這並非合法的賦值轉換,因爲該鏈條包含兩個參數化類型 Comparable<Integer>Comparable<String> ,它們彼此不是子類型。

如果表達式的類型不能經由在賦值上下文中所允許的類型轉換而轉換爲變量的類型,那 麼就會產生編譯時錯誤。

如果表達式的類型可以經由賦值轉換被轉換爲變量的類型,我們就稱該表達式(或它的值)是可賦值給該變量的,或者稱該表達式的類型與該變量的類型是賦值兼容的。

如果變量的類型是float或double,那麼值集轉換就會作用於下面的類型轉換所產生的結果v上:

  • 如果v是float類型,並且是浮點擴展指數值集的元素,那麼Java語言的實現必須將v映射爲浮點值集中最接近的元素。這種轉換可能會產生上溢或下溢。
  • 如果v是double類型,並且是雙精度浮點擴展指數值集的元素,那麼Java語言的實現必須將v映射爲雙精度值集中最接近的元素。這種轉換可能會產生上溢或下溢。

賦值轉換可能會產生下列異常:

  • ClassCastException,如果在應用上述轉換後所產生的值是對象,但是它不是待賦值的變量的類型的擦除的子類或子接口的實例,那麼就會產生該異常。
    這種情況只有在堆污染時纔會發生。在實踐中,Java語言的實現只有在訪問參教化類型的對象的域或方法時,當被訪問域的擦除類型或被訪問方法的擦除結果類型與它們的未擦除類型不同時,才需要執行強制類型轉換。
  • OutOfMemoryError,在裝箱轉換時有可能會發生。
  • NullPointerException,在空引用上進行拆箱轉換時會發生。
  • ArrayStoreException,在涉及數組元素或域訪問的特例中有可能會發生。

簡單類型的賦值轉換

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

在上面的例子中:

  • veclong的值不能賦值給一個Long變量,因爲Long是類類型,而不是Object類型。數組只能賦值給兼容的數組類型的變量,或Object、Cloneable或java.io.Serializable 類型的變量。
  • veclong的值不能賦值給vecshort,因爲它們是簡單類型的數組,並且short和long不是相同的簡單類型。
  • cpvec可以賦值給pvec, 因爲對於任意引用,如果它可以是ColoredPoint類型的表達式的值,那麼就可以是Point類型的變量的值。隨後將新的Point賦值給pvec元素的語句會拋出ArrayStoreException異常(假設該程序經過修正,能夠通過編譯),因爲ColoredPoint數組不能將一個Point實例作爲其元素的值。
  • pvec的值不能賦給cpvec ,因爲對於所有可以是ColoredPoint類型的表達式的值的引用,並非都可以是正確的Point類型的變量的值。如果pvec的值在運行時是對 Point[]實例的引用,並且將其賦值給cpvec是允許的,那麼對cpvec的元素的直接引用,例如cpvec[0],就可以返回一個Point,而Point並非ColoredPoint。因此, 如果允許這樣的賦值,那麼就是允許對類型系統的違例。可以使用強制類型轉換來確保pvec引用的是ColoredPoint():
cpvec = (ColoredPoint.[] ) pvec; 	// OK, but may throw an
									// exception at run time

方法調用上下文

方法調用上下文將方法調用或構造器調用中的引元值賦值給對應的形參。

嚴格的調用上下文允許使用下列轉換:

  • 標識轉換。
  • 拓寬簡單類型轉換。
  • 拓寬引用類型轉換。

寬鬆的調用上下文允許使用更多的轉換,因爲對於使用它們的特定調用,是無法使用嚴 格的調用上下文來找到可應用的任何聲明的。寬鬆的調用上下文允許使用下列轉換:

  • 標識轉換。
  • 拓寬簡單類型轉換。
  • 拓寬引用類型轉換。
  • 裝箱轉換,後跟可選的拓寬引用類型轉換。
  • 拆箱轉換,後跟可選的拓寬簡單類型轉換。

在應用了上述用於調用上下文的轉換之後,如果所產生的類型是原生類型, 那麼就可能會進行非受檢轉換。

空類型的值(空引用是唯一的這種值)可以賦值給任意引用類型。

如果轉換鏈中包含兩個參數化類型,並且它們之間不存在子類型關係,那麼這就是一個編譯時錯誤。

如果表達式的類型不能通過寬鬆的調用上下文中所允許的類型轉換而被轉換爲參數的類型,那麼就會產生編譯時錯誤。

如果引元表達式的類型是float或double,那麼就會在類型轉換之後應用值集轉換:

  • 如果要轉換的float類型的引元值是浮點擴展指數值集中的元素,那麼其實現方式必須將該值映射爲浮點值集中最接近的元素。這種轉換可能會產生上溢或下溢。
  • 如果要轉換的double類型的引元值是雙精度浮點擴展指數值集中的元素,那麼其實現方式必須將該值映射爲雙精度浮點值集中最接近的元素。這種轉換可能會產生上溢或下溢。

調用上下文可能會拋出的異常包括:

  • ClassCastException,如果在應用上述類型轉換後所產生的值是對象,但是它不是對 應的形參類型的擦除的子類或子接口的實例,那麼就會產生該異常。
  • OutOfMemoryError,在裝箱轉換時有可能會發生。
  • NullPointerException ,在空引用上進行拆箱轉換時會發生。

特別地,無論是嚴格的調用上下文還是寬鬆的調用上下文,都不包括在賦值上下文中所允許的隱式整數常量表達式窄化轉換。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.5.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是類類型,那麼要麼|S| <: |T|,要麼|T| <: |S|,否則,就會產生編譯時錯誤。

而且,如果T有超類型X, S有超類型Y,且X和Y都是可證不同的參數化類型,而X和Y的擦除又是相同的,那麼就會產生編譯時錯誤。

  • 如果T是接口類型:
    ■如果S不是final類,那麼如果T有超類型X, S有超類型Y,且X和Y都是可證不同的參數化類型,而X和Y的擦除又是相同的,那麼就會產生編譯時錯誤。
    否則,這種強制類型轉換在編譯時總是合法的(因爲即使S沒有實現T, S的某個子類也可能會實現T)。
    ■如果S是final類,那麼S必須實現T,否則會產生編譯時錯誤。
  • 如果T是類型變量,那麼通過用T的上界來替換T,可以遞歸地應用這個算法。
  • 如果T是數組類型,那麼S必須是Object類,否則會產生編譯時錯誤。
  • 如果T是交集類型T1T_{1}&…&TnT_{n},若存在某個TiT_{i}(1in1\leq i \leq n), 使得S不能通過這個算法被強制類型轉換爲TiT_{i}那麼就是一個編譯時錯誤。即,強制類型轉換成功與否是由交集類型最嚴苛的組成部分所決定的。

如果S是接口類型:

  • 如果T是數組類型,那麼S必須是java.io.Serializable類型或Cloneable類型(僅由數組實現的接口),否則會產生編譯時錯誤。
  • 如果T是非final的類或接口類型,那麼如果T有超類型X, S有超類型 Y,且X和Y都是可證不同的參數化類型,而X和Y的擦除又是相同的,那麼就會產生編譯時錯誤。
    否則,這種強制類型轉換在編譯時總是合法的(因爲即使S沒有實現T, S的某個子類也可能會實現T)。
  • 如果T是final的類類型,那麼:
    ■ 如果S不是參數化類型或原生類型,那麼T必須實現S,否則會產生編譯時錯誤。
    ■ 如果S是參數化類型或原生類型,那麼S要麼是對某個泛型聲明G的調用而產生的參數化類型,要麼是相應的泛型聲明G的原生類型,這樣就必然存在T的超類型X,使得X是G的一個調用,否則會產生編譯時錯誤。
    而且,如果S和X是可證不同的參數化類型,那麼就會產生編譯時錯誤。
  • 如果T是類型變量,那麼通過用T的上界來替換T,可以遞歸地應用這個算法。
  • 如果T是交集類型T1T_{1}&…&TnT_{n},若存在某個TiT_{i}(1in1\leq i \leq n)使得S不能通過這個算法被強制類型轉換爲TiT_{i},那麼就是一個編譯時錯誤。

如果S是類型變量,那麼通過用S的上界來替換S,可以遞歸地應用這個算法。

如果S是交集類型A1A_{1}&…&AnA_{n}, 若存在某個AiA_{i}(1in1\leq i \leq n),使得AiA_{i}不能通過這個算法被強制類型轉換爲T,那麼就是一個編譯時錯誤。即,強制類型轉換成功與否是由交集類型最嚴苛組成部分所決定的。

如果S是數組類型SC[],即由sc類型的元素構成的數組:

  • 如果T是類類型,那麼如果T不是Object,那麼就會產生編譯時錯誤(因爲Object 是唯一的可以將數組賦值給它的類類型)。
  • 如果T是接口類型,那麼除非T是java.io.Serializable或Cloneable類型(僅由數 組實現的接口),否則就會產生編譯時錯誤。
  • 如果T是類型變量,那麼通過用T的上界來替換T,可以遞歸地應用這個算法。
  • 如果T是數組類型TC[],即由TC類型的元素構成的數組,那麼除非下列情況有一種爲真,否則就會產生編譯時錯誤:
    ■ TC和SC是相同的簡單類型。
    ■ TC和SC都是引用類型,並且SC類型可以經過強制類型轉換而轉換成TC。
  • 如果T是交集類型T1T_{1}&…&TnT_{n},那麼如果存在某個TiT_{i}(1in1\leq i \leq n),使得S不能通過這 個算法被強制類型轉換爲TiT_{i},那麼就是一個編譯時錯誤。

引用類型的強制類型轉換

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
  • T的所有類型引元都是無界通配符。
  • T <: S,並且對於S除T之外的任何子類型X,不存在X的類型引元未包含在T的類型引元中的情況。

從類型S到類型變量T的強制類型轉換是非受檢的,除非S<:T。

如果存在某個TiT_{i}(1in1\leq i \leq n),使得從S到TiT_{i}的強制類型轉換是非受檢的,那麼從類型 S到交集類型T1T_{1}&…&TnT_{n}的強制類型轉換就是非受檢的。

如果從|S|到|T|的強制類型轉換是靜態可知正確的,那麼從S到非交集類型T的非受檢強制類型轉換是完全非受檢的,否則,這種類型轉換就是部分非受檢的。

如果對於所有 i (1in1\leq i \leq n) , 從S到TiT_{i}的強制類型轉換要麼是靜態可知正確的,要麼是完全非受檢的,那麼從類型S到交集類型T1T_{1}&…&TnT_{n}的強制類型轉換就是完全非受檢的。 否則,這種類型轉換就是部分非受檢的。

非受檢強制類型轉換會產生編譯時非受檢警告,除非用Suppresswarnings註解進行了壓制。

如果某個強制類型轉換不是靜態可知正確的,並且不是非受檢的,那麼它就是受檢的。 如果某個到引用類型的強制類型轉換不產生編譯時錯誤,那麼它必是以下幾種情況之一:

  • 該強制類型轉換是靜態可知正確的。
    對這種強制類型轉換不需要執行任何運行時動作。
  • 該強制類型轉換是完全非受檢強制轉型。
    對這種強制類型轉換不需要執行任何運行時動作。
  • 該強制類型轉換是部分非受檢或受檢的到交集類型的強制類型轉換。
    其中交集類型爲T1T_{1}&…&TnT_{n}, 對於所有的 i (1in1\leq i \leq n) 任何從S到TiT_{i}的強制類型轉換所必需的運行時檢查,對於強制類型轉換到該交集類型也是必需的。
  • 該強制類型轉換是到非交集類型的部分非受檢強制類型轉換。
    這種強制類型轉換要求在運行時進行有效性檢查。這項檢查執行時就像是該強制類型 轉換是|S|和|T|之間的受檢強制類型轉換一樣,就像下面將要描述的那樣。
  • 該強制類型轉換是到非交集類型的受檢強制類型轉換。
    這種強制類型轉換要求進行運行時有效性檢查。如果被類型轉換的值在運行時是 null,那麼該強制類型轉換是允許的。否則,設R是運行時引用值所引用的對象的類,T是在強制類型轉換操作符中命名的類型的擦除,那麼該強制類型轉換必須在運行時通過第5.5.3節中描述的算法檢查類R與類型T之間的賦值兼容性。

注意 當上述規則首次應用於任意給定的強制類型轉換時,R不能是接口,但是如果這些規則後續進行遞歸地應用,那麼R就可以是接口,因爲運行時的引用值可以引用元素類型是接口類型的數組。

運行時的受檢強制類型轉換

下面的算法用於檢查某個對象的運行時類型R是否與在強制類型轉換操作符中命名的類型的擦除具有賦值兼容性。如果該檢查會拋出運行時異常,那麼應該是 ClassCastException。

如果R是普通類(不是數組類):

  • 如果T是類類型,那麼R必須要麼是與T相同的類,要麼是T的子類, 否則會拋出運行時異常。
  • 如果T是接口類型,那麼R必須實現接口 T,否則會拋出運行時異常。
  • 如果T是數組類型,那麼會拋出運行時異常。

如果R是接口:

  • 如果T是類類型,那麼T必須是Object,否則會拋出運行時異常。
  • 如果T是接口類型,那麼R必須要麼是與T相同的接口,要麼是T的子接口,否則會拋出運行時異常。
  • 如果T是數組類型,那麼會拋出運行時異常。

如果R是表示數組類型RC[ ] 的類,即由RC類型的元素構成的數組:

  • 如果T是類類型,那麼T必須是Object,否則會拋出運行時異常。
  • 如果T是接口類型,那麼除非T是java.io.Serializable或Cloneable類型(僅有的由數組實現的接口),否則會拋出運行時異常。
    這種情況可能會悄然跳過編譯時檢查,例如,存儲在Oktject類型的變量中的對數組的引用。
  • 如果T是數組類型TC[ ],即由TC類型的元素構成的數組,那麼除非下列情況有一種爲真,否則會拋出運行時異常:
    ■ TC和RC是相同的簡單類型。
    ■ TC和RC都是引用類型,並且RC類型可以通過遞歸地應用針對強制類型轉換的運行時規則,經過強制類型轉換而轉換成TC。

運行時不兼容的類型

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類型的值而定義的。

有兩種數字提升:一元數字提升和二元數字提升。

一元數字提升

某些操作符會將一元數字提升應用於單個操作數,而應用的結果必須產生一個數字類型的值:

  • 如果該操作數的編譯時類型爲Byte、Short、Character或Integer,那麼它會接受拆箱轉換。轉換的結果之後通過拓寬簡單類型轉換或標識轉換被提升爲int類型的值。
  • 如果該操作數的編譯時類型爲Long、Float或Double,那麼它會接受拆箱轉換。
  • 如果該操作數的編譯時類型爲byte、short或char,那麼它會通過拓寬簡單類型轉換被提升爲int類型的值。
  • 否則,一元數字操作數會保持原樣而不進行轉換。

在上述轉換之後,如果需要,還可以應用值集轉換。

一元數字提升是在屬於下列情況的表達式上執行的:

  • 數組創建表達式中的每一維表達式。
  • 數組訪問表達式中的索引表達式。
  • 一元加號操作符+ 的操作數。
  • 一元減號操作符-的操作數。
  • 按位取反操作符~ 的操作數。
  • 移位操作符>>、>>>和<<對應的操作數。
  • long類型的移位距離(右操作數)不會導致被移位的值(左操作數)被提升到long。

一元數字提升

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)拓寬簡單類型轉換按照下面指定的規則轉換其中一個操作數或同時轉換兩個操作數:

  • 如果任意一個操作數是double類型,那麼另一個就被轉換爲double。
  • 否則,如果任意一個操作數是float類型,那麼另一個就被轉換爲float。
  • 否則,如果任意一個操作數是long類型,那麼另一個就被轉換爲long。
  • 否則,兩個操作數都被轉換爲int類型。

在類型轉換之後,如果需要,那麼值集轉換可以應用於每個操作數。

二元數字提升是在特定操作符的操作數上執行的:

  • 乘除操作符* 、/ 和%
  • 用於數字類型的加法和減法操作符+和-
  • 數字比較操作符<、<=、> 和 >=
  • 數字判等操作符==和!=
  • 整數位操作符 &、^ 和 I
  • 在特定情況下的條件操作符 ?:

二元數字提升

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是這個控制字符的數值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章