1. 程式人生 > >面向物件的陷阱——instanceof運算子的陷阱

面向物件的陷阱——instanceof運算子的陷阱

1、instanceof運算子的陷阱

       instanceof是一個非常簡單的運算子。instanceof運算子的前一個運算元通常是一個引用型別的變數,後一個運算元通常是一個類(也可以是介面,可以把介面理解成一種特殊的類),它用於判斷前面的物件是否是後面的類或其子類、實現類的例項。如果是,則返回true;否則,返回false。        根據Java語言規範,使用instanceof運算子有一個限制:instanceof運算子前面運算元的編譯時型別必須是如下三種情況。
  1. 要麼與後面的類相同。
  2. 要麼是後面類的父類。
  3. 要麼是後面類的子類。
       如果前面運算元的編譯時型別與後面的型別沒有任何關係,程式將沒法通過編譯。因此,當時用instanceof運算子的時候,應儘量從編譯、執行兩個階段來考慮它——如果instanceof運算子使用不當,程式編譯時就會丟擲異常;當使用instanceof運算子通過編譯後,才能考慮運算結果是true還是false。           一旦instanceof運算子通過了編譯,程式進入運算階段。instanceof運算返回的結果與前一個運算元(引用變數)實際引用的物件的型別
有關,如果它實際引用的物件是第二個運算元的例項,或者是第二個運算元的子類、實現類的實現,那麼instanceof運算的結果返回true,否則返回false。
       在極端情況之下,instanceof前一個運算元所引用物件的實際型別就是後面的型別,但只要它的編譯時型別既不是第二運算元的型別,也不是第二個運算元的父類、子類,程式就沒法通過編譯。
public class InstanceofTest {
	public static void main(String[] args) {
		Object str = "Java物件";
		Math math = (Math)str;  //①
		System.out.println("字串是否是String的例項:" + (math instanceof String));  //②  報錯:Incompatible conditional operand types Math and String
	}
}
       當編譯器編譯Java程式時,編譯器無法檢查引用變數實際引用物件的型別,它只檢查該變數的編譯時型別。對於②行程式碼來說,math的編譯時型別是Math,Math既不是String型別,也不是String型別的父類,還不是String型別的子類,因此程式沒法通過編譯。至於math實際引用物件的型別是什麼,編譯器並不關心。        至於①行程式碼出為何沒有出現編譯錯誤,這和強制轉型的機制有關。對於Java的強制轉型而言,也可以分為編譯、執行兩個階段來進行分析。         · 編譯階段,強制轉型要求被轉型變數的編譯時型別必須是如下三種情況之一。
  1. 被轉型變數的編譯時型別與目標型別相同。
  2. 被轉型變數的編譯時型別是目標型別父類。
  3. 被轉型變數的編譯時型別是目標型別子類。在這種情況下可以自動向上轉型,無須強制轉換。
       如果被轉型變數的編譯時型別與目標型別沒有任何繼承關係,編譯器將提示編譯錯誤。通過上面分析可以知道,強制轉型的編譯階段只關心引用變數的編譯時型別,至於該引用變數實際引用物件的型別,編譯器並不關心。         · 執行 階段,被轉型變數所引用物件的實際型別必須是目標型別的例項,或者是目標型別的子類、實現類的例項,否則在執行時將引發ClassCastException異常。        從上面的分析可以看出,對於①行程式碼來說,編譯時不會出現錯誤,因為str引用變數的編譯時型別是Object,它是Math類的父類。但是str執行時型別是String,它與Math類沒有任何關係,所以執行時將會引發ClassCastException異常。
public class ConversionTest {
	public static void main(String[] args) {
		Object obj = "Hello";  //obj編譯時型別是Object,執行時型別是String。
		String objStr = (String)obj;  //因為obj編譯時型別是Object,所以可以通過編譯。因為obj執行時型別是String,objStr也是String類,所以可以執行。
		System.out.println(objStr);
		
		Object objPri = new Integer(5);  //objPri編譯時型別是Object,執行時型別是Integer。
		String str = (String)objPri;  //因為objPri編譯時型別是Object,所以可以通過編譯。因為obj執行時型別是Integer,objStr是String類,所以會引發異常。
		System.out.println(str);
		
		String s = "Java物件";  //s編譯時型別和執行時型別都是String。
		Math m = (Math)s;  //String類不是Math的子類,也不是Math的父類,所以會導致編譯錯誤。
	}
}

       關於instanceof還有一個比較隱蔽的陷阱。
public class NullInstaceof {
	public static void main(String[] args) {
		String s = null;
		System.out.println("null是否是String類的例項:"  + (s instanceof String));
	}
}
輸出結果為: null是否是String類的例項:false        雖然null可以作為所有引用型別變數的值,但對於s引用變數而言,它實際上並未引用一個正在的String物件,因此程式會輸出false。使null呼叫instanceof運算子時返回false是非常有用的行為,因為instanceof運算子有一個額外的功能:它可以保證第一個運算元所引用的物件不是null。如果instanceof告知一個引用變數是某個特定型別的例項,那麼就可以將其轉型為該型別,不用擔心會丟擲ClassCastException或NullPointerException異常。