4.面向对象的陷阱

本文探讨了Java面向对象设计中的陷阱,包括instanceof运算符的局限性,构造器的注意事项,如避免无限递归和明确调用哪个重载方法。还提到了非静态内部类的限制,如必须依赖外部类实例,不能有静态成员,以及静态内部类无法访问外部类的非静态成员。
摘要由CSDN通过智能技术生成

Java面向对象的设计过程中也会有一些陷阱

***************************************************************************************

学习笔记如下:

instanceof运算符的陷阱

 instanceof是一个非常简单的运算符。instanceof运算符的前一个操作数通常是一个引用类型的变量,后一个操作数通常是一个类(也可以是接口,可以把接口理解成一种特殊的类),它用于判断前面的对象是否是后面的类或其子类、实现类的实例。如果是,则返回true;否则,返回false。
 

很明显,该程序的这个地方并不能使用instanceof运算符。根据Java语言规范,使用instanceof运算符有一个限制:instanceof运算符前面操作数的编译时类型必须是如下三种情况。
》要么与后面的类相同。
》要么是后面类的父类。
》要么是后面类的子类。

如果前面操作数的编译时类型与后面的类型没有任何关系,程序将没法通过编译

因此,当使用instanceof 运算符的时候,应尽量从编译、运行两个阶段来考虑它—如果instanceof运算符使用不当,程序编译时就会抛出异常;当使用instanceof运算符通过编译后,才能考虑它的运算结果是true,还是false。


一旦instanceof运算符通过了编译,程序进入运行阶段。instanceof运算返回的结果与前一个操作数(引用变量)实际引用的对象的类型有关,如果它实际引用的对象是第二个操作数的实例,或者是第二个操作数的子类、实现类的实例,那么instanceof运算的结果返回true,否则返回false。

在极端情况下,instanceof 前一个操作数所引用对象的实际类型就是后面的类型,但只要它的编译时类型既不是第二个操作数的类型,也不是第二个操作数的父类、子类,程序就没法通过编译


对于Java的强制转型而言,也可以分为编译、运行两个阶段来分析它。
》在编译阶段,强制转型要求被转型变量的编译时类型必须是如下三种情况之一。
·被转型变量的编译时类型与目标类型相同。
·被转型变量的编译时类型是目标类型父类。
·被转型变量的编译时类型是目标类型子类。在这种情况下可以自动向上转型,无须强制转换。
如果被转型变量的编译时类型与目标类型没有任何继承关系,编译器将提示编译错误。通过上面分析可以看出,强制转型的编译阶段只关心引用变量的编译时类型,至于该引用变量实际引用对象的类型,编译器并不关心,也没法关心。

》在运行阶段,被转型变量所引用对象的实际类型必须是目标类型的实例,或者是目标类型的子类、实现类的实例,否则在运行时将引发ClassCastException异常。

***

从上面分析可以看出,对于程序中①行代码,程序编译时不会出现错误,因为str引用变量的编译时类型是Object,它是Math类的父类。至于str所引用对象的实际类型是什么,编译器并不会关心。因此,这行代码完全可以通过编译,但运行这行代码将引发ClassCastException异常。

**


关于instanceof还有一个比较隐蔽的陷阱

String s=null;

System.out.println("null是否是String 类的实例:"+(s instanceof String))

虽然null可以作为所有引用类型变量的值,但对于s引用变量而言,它实际上并未引用一个真正的String对象,因此程序输出false。


使null调用instanceof运算符时返回false是非常有用的行为,因为intanceof运算符有了一个额外的功能: 它可以保证第一个操作数所引用的对象不是null。如果instanceof告知一个引拥变量是某个特定类型的实例,那么就可以将其转型为该类型,并调用该类型的方法,而不用担心会抛出ClassCastException 或NullPointerException 异常。


instanceof运算符除了可以保证某个引用变量是特定类型的实例外,还可以保证该变量没有引用一个nul.这样就可以将该引用变量转型为该类型,并调用该类型的方法,而不用担心会引发ClassCastException 或NullPointerException异常。

——————————————————————————————————————————————


构造器的陷阱

1.构造器之前不要加void

2.构造器创建对象吗?

实际上构造器并不会创建Java对象,构造器只是负责执行初始化,在构造器执行之前,Java对象所需要的内存空间,应该说是由new关键字申请出来的。

绝大部分时候,程序使用new关键字为一个Java对象申请空间之后,都需要使用构造器为这个对象执行初始化。

但在某些时候,程序创建Java对象无须调用构造器,以下面两种方式创建的Java对象无须使用构造器

》使用反序列化的方式恢复Java对象。

》使用clone 方法复制Java对象。

try(
Wolf w=new Wolf("灰太狼");
System.out.print1n("Wo1f对象创建完成~");
Wolf w2=null;
//创建对象输出流
ObjectoutputStream oos=new ObjectOutputStream(new FileOutputStream("a.bin"));
/∥创建对象输入流
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("a.bin"));
)
{
//序列化输出Java对象
oos.writeobject(w);
oos.flush();
//反序列化恢复Java对象
w2=(Wo1f)ois.readobject();

如果真的想保证反序列化时也不会产生多个Java实例,应该为单例类提供readResolve()方法,该方法保证反序列化时得到已有的Java实例

当为单例类提供了readResolve)方法,即使通过反序列化机制来恢复Java实例,依然可以保证程序中只有一个Java实例。

//提供readResolve()方法
private object readResolve()
     throws ObjectStreamException
{
   //得到已有的instance实例
    return instance;
}

上面程序为Singleton类提供了 readResolve0方法,当JVM反序列化地恢复一个新对象时,系统会自动调用这个readResolve0方法返回指定好的对象,从而保证系统通过反序列化机制不会产生多个Java对象。

除了可以使用反序列化机制恢复Java对象无须构造器之外,使用clone()方法复制Java对象也无须调用构造器。

如果希望某个Java类的实例是可复制的,则对该Java类有如下要求:

》让该Java类实现Cloneable接口。

》为该Java类提供clone()方法,该方法负责进行复制。

通过clone0方法复制出来的Dog对象当然和原来的Dog对象具有完全相同的实例变量值,但系统中将会产生两个Dog对象,因此程序判断dog==dog2时将输出false。


3.无限递归的构造器的陷阱

有的时候,尽管类的构造器中没有任何代码,它的构造器中只有一行简单的输出代码。

若在类里声明了一个本类的对象

然后又在代码块里调用构造方初始化该对象(没有这一步,只要不造成递归调用,该类还是安全的

但不要忘记了,不管是定义实例变量时指定的初始值。还是在非静态初始化块中执行的初始化操作,最终都将被提取到构造器中执行,此时就会造成递归调用

所以:无论如何不要导致构造器产生递归调用。也就是说,应该:

》尽量不要在定义实例变量时指定实例变量的值为当前类的实例。
》尽量不要在初始化块中创建当前类的实例。
》尽量不要在构造器内调用本构造器创建Java对象。


4.持有当前类的实例

 public static void main(String[] args)
{ 
    Instancefest in =new Instancerest();
    Instancerest in2=new  Instancefest("测试name");
//让两个对象相互引用
in.inatance =in2;
in2.instance=in;
Syatem.out.printin(in);
System.out.print1n(in2);
}
总之,如果一个类的实制持有当前类的其他实例时需要特别小心,因为程序很容易形成过归调用。 


5.到底调用哪个重载的方法?

当参数不匹配时,Java虚拟机在识别方法时具有一定的“智能”,它可以对调用法的实参进行向上转型,使之适合被调方法的需要。

6.方法重写的陷阱

父类的私有函数

private voidtest(){

...

}

子类的函数

public  voidtest(){

...

}

该方法和父类的ten0方法有相同的方法名、相同的形参列表,并不是重写,只是子类重新定义了一个新方法而已

为了证明Sub类中的test()方法没有重写父类的方法,可以使用@Override 来修饰Sub中的test()方法。再次尝试编译该程序,将看到错误提示.


还有一种情况,如果父类中定义了使用默认访问控制符(也就是不使用访问控制符)修饰的方法,这个方法同样可能无法被重写。
对于不使用访问控制符修饰的方法,它只能被与当前类处于同一个包中的其他类访问,其他包中的子类依然无法访问该方法,此时也无法重写该方法。

————————————————————————————————————

非静态内部类的陷阱

1.非静态内部类并没有无参数的构造器,它的构造器需要一个0uer参数。这符合非静态内部类的规则:非静态内部类必须寄生在外部类的实例中,没有外部类的对象,就不可能产生非静态内部类的对象。

因此,非静态内部类不可能有无参数的构造器一即使系统为非静态内部类提供一个默认的构造器,这个默认的构造器也需要一个外部类形参。

private void test()
throws Exception
  //创建非静态内部类的对象
   System.out.printin(now Inner());//1不会报错
   //使用反射方式来创建Inner对象
   System.out.print1n(Inner.clasa.nowInatance());//2会报错
}

对于上面程序中的①行代码,程序表面上调用lnmer无参数的构造器创建实例,实际上虚拟机底层会将his(代表当前默认的Outer对象)作为实参传入Inner构造器。

至于程序@行代码的效果则不同,程序通过反射指定调用Imer类无参数的构造器,所以引发了运行时异常

2.非静态内部类不允许有静态成员


3.非静态内部类的子类

上面程序错误的关键在于,由于非静态内部类In必须寄生在Out对象之内,因此父类Out.ln根本没有无参数的构造器。而程序定义其子类Out.ln时,也没有定义构造器,那么系统会为它提供一个无参数的构造器。在Outlest无参数的构造器内,编译器会增加代码super)一子类总会调用父类的构造器。对于这个superO调用,指定调用父类Out.In无参数的构器,必然导致偏译错误。为了解决这个问题,应该为OutTest显式定义一个构造器,在该构器中显式调用Out.In父类对应的构造器


4.静态内部类的限制

前面介绍内部类时已经指出,当程序需要使用内部类时,应尽量考虑使用静态内部类,而不是非静态内部类。当程序使用静态内部类时,外部类相当于静态内部类的一个包,因此使用起来比较方便;但另一方面,这也给静态内部类增加了一个限制一—静态内部类不能访问外部类的非静态成员



  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值