面向对象的陷阱——instanceof运算符的陷阱

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Never_Blue/article/details/70809697

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异常。








猜你喜欢

转载自blog.csdn.net/Never_Blue/article/details/70809697
今日推荐