编写高质量代码:改善Java程序的151个建议(13)

  建议41:让多重继承成为现实

  在Java中一个类可以多重实现,但不能多重继承,也就是说一个类能够同时实现多个接口,但不能同时继承多个类。但有时候我们确实需要继承多个类,比如希望拥有两个类的行为功能,就很难使用单继承来解决问题了(当然,使用多层继承是可以解决的)。幸运的是Java中提供的内部类可以曲折地解决此问题,我们来看一个案例,定义一个父亲、母亲接口,描述父亲强壮、母亲温柔的理想情形,代码如下:

  

    //父亲 interfaceFather{ publicintstrong(); } //母亲 interfaceMother{ publicintkind(); }

  其中strong和kind的返回值表示强壮和温柔的指数,指数越高强壮度和温柔度也就越高,这与在游戏中设置人物的属性值是一样的。我们继续来看父亲、母亲这两个实现:

  

    classFatherImplimplementsFather{ //父亲的强壮指数是8 publicintstrong(){ return8; } } classMotherImplimplementsMother{ //母亲的温柔指数是8 publicintkind(){ return8; } }

  父亲强壮指数是8,母亲温柔指数也是8,门当户对,那他们生的儿子、女儿一定更优秀了,我们先来看儿子类,代码如下:

  

    classSonextendsFatherImplimplementsMother{ @Override publicintstrong(){ //儿子比父亲强壮 returnsuper.strong()+1; } @Override publicintkind(){ returnnewMotherSpecial().kind(); } privateclassMotherSpecialextendsMotherImpl{ publicintkind(){ //儿子温柔指数降低了 returnsuper.kind()-1; } } }

  儿子继承自父亲,变得比父亲更强壮了(覆写父类strong方法),同时儿子也具有母亲的优点,只是温柔指数降低了。注意看,这里构造了MotherSpecial类继承母亲类,也就是获得了母亲类的行为方法,这也是内部类的一个重要特性:内部类可以继承一个与外部类无关的类,保证了内部类的独立性,正是基于这一点,多重继承才会成为可能。MotherSpecial的这种内部类叫做成员内部类(也叫做实例内部类,Instance Inner Class)。我们再来看看女儿类,代码如下:

  

    classDaughterextendsMotherImplimplementsFather{ @Override publicintstrong(){ returnnewFatherImpl(){ @Override publicintstrong(){ //女儿的强壮指数降低了 returnsuper.strong()-2; } }.strong(); } }

  女儿继承了母亲的温柔指数,同时又覆写父类的强壮指数,不多解释。注意看覆写的strong方法,这里是创建了一个匿名内部类(Anonymous Inner Class)来覆写父类的方法,以完成继承父亲行为的功能。

  多重继承指的是一个类可以同时从多于一个的父类那里继承行为与特征,按照这个定义来看,我们的儿子类、女儿类都实现了从父亲类、母亲类那里所继承的功能,应该属于多重继承。这要完全归功于内部类,诸位在需要用到多重继承时,可以思考一下内部类。

  在现实生活中,也确实存在多重继承的问题,上面的例子是说后人即继承了父亲也继承了母亲的行为和特征,再比如我国的特产动物“四不像”(学名麋鹿),其外形“似鹿非鹿,似马非马,似牛非牛,似驴非驴”,这你要是想用单继承表示就麻烦了,如果用多继承则可以很好地解决问题:定义鹿、马、牛、驴四个类,然后建立麋鹿类的多个内部类,继承它们即可。

  建议42:让工具类不可实例化

  Java项目中使用的工具类非常多,比如JDK自己的工具类java.lang.Math、java.util.Collections等都是我们经常用到的。工具类的方法和属性都是静态的,不需要生成实例即可访问,而且JDK也做了很好的处理,由于不希望被初始化,于是就设置构造函数为private访问权限,表示除了类本身外,谁都不能产生一个实例,我们来看一下java.lang.Math代码:

  

    publicfinalclassMath{ /** *Don’tletanyoneinstantiatethisclass. */ privateMath(){} }

  之所以要将“Don抰 let anyone instantiate this class.”留下来,是因为Math的构造函数设置为private了:我就是一个工具类,我只想要其他类通过类名来访问,我不想你通过实例对象访问。这在平台型或框架型项目中已经足够了。但是如果已经告诉你不能这么做了,你还要生成一个Math实例来访问静态方法和属性(Java的反射是如此的发达,修改个构造函数的访问权限易如反掌),那我就不保证正确性了,隐藏问题随时都有可能爆发!那我们在项目开发中有没有更好的限制办法呢?有,即不仅仅设置成private访问权限,还抛异常,代码如下:

  

    publicclassUtilsClass{ privateUtilsClass(){ thrownewError(“不要实例化我!”); } }

  如此做才能保证一个工具类不会实例化,并且保证所有的访问都是通过类名来进行的。需要注意一点的是,此工具类最好不要做继承的打算,因为如果子类可以实例化的话,那就要调用父类的构造函数,可是父类没有可以被访问的构造函数,于是问题就会出现。

  注意:如果一个类不允许实例化,就要保证“平常”渠道都不能实例化它。

  建议43:避免对象的浅拷贝

  我们知道一个类实现了Cloneable接口就表示它具备了被拷贝的能力,如果再覆写clone()方法就会完全具备拷贝能力。拷贝是在内存中进行的,所以在性能方面比直接通过new生成对象要快很多,特别是在大对象的生成上,这会使性能的提升非常显著。但是对象拷贝也有一个比较容易忽略的问题:浅拷贝(ShadowClone,也叫做影子拷贝)存在对象属性拷贝不彻底的问题。我们来看这样一段代码:

  

    publicclassClient{ publicstaticvoidmain(String[]args){ //定义父亲 Personf=newPerson(“父亲”); //定义大儿子 Persons1=newPerson(“大儿子”,f); //小儿子的信息是通过大儿子拷贝过来的 Persons2=s1.clone(); s2.setName(“小儿子”); System.out.println(s1.getName()+”的父亲是”+s1.getFather().getName()); System.out.println(s2.getName()+”的父亲是”+s2.getFather().getName()); } } classPersonimplementsCloneable{ //姓名 privateStringname; //父亲 privatePersonfather; publicPerson(String_name){ name=_name; } publicPerson(String_name,Person_parent){ name=_name; father=_parent; } /*name和parent的getter/setter方法省略*/ //拷贝的实现 @Override publicPersonclone(){ Personp=null; try{ p=(Person)super.clone(); }catch(CloneNotSupportedExceptione){ e.printStackTrace(); } returnp; } }

  程序中,我们描述了这样一个场景:一个父亲,有两个儿子,大小儿子同根同种,所以小儿子对象就通过拷贝大儿子对象来生成,运行输出的结果如下:

  

    大儿子的父亲是父亲 小儿子的父亲是父亲

  这很正确,没有问题。突然有一天,父亲心血来潮想让大儿子去认个干爹,也就是大儿子的父亲名称需要重新设置一下,代码如下:

  

    publicstaticvoidmain(String[]args){ //定义父亲 Personf=newPerson(“父亲”); //定义大儿子 Persons1=newPerson(“大儿子”,f); //小儿子的信息是通过大儿子拷贝过来的 Persons2=s1.clone(); s2.setName(“小儿子”); //认干爹 s1.getFather().setName(“干爹”); System.out.println(s1.getName()+”的父亲是”+s1.getFather().getName()); System.out.println(s2.getName()+”的父亲是”+s2.getFather().getName()); }

  上面仅仅修改了加粗字体部分,大儿子重新设置了父亲名称,我们期望的输出是:将大儿子父亲的名称修改为干爹,小儿子的父亲名称保持不变。下面来检查一下结果是否如此:

  

    大儿子的父亲是干爹 小儿子的父亲是干爹

  怎么回事,小儿子的父亲也成了“干爹”?两个儿子都没有,岂不是要气死“父亲”了!出现这个问题的原因就在于clone方法,我们知道所有类都继承自Object,Object提供了一个对象拷贝的默认方法,即上面代码中的super.clone方法,但是该方法是有缺陷的,它提供的是一种浅拷贝方式,也就是说它并不会把对象的所有属性全部拷贝一份,而是有选择性的拷贝,它的拷贝规则如下:

  (1)基本类型

  如果变量是基本类型,则拷贝其值,比如int、float等。

  (2)对象

  如果变量是一个实例对象,则拷贝地址引用,也就是说此时新拷贝出的对象与原有对象共享该实例变量,不受访问权限的限制。这在Java中是很疯狂的,因为它突破了访问权限的定义:一个private修饰的变量,竟然可以被两个不同的实例对象访问,这让Java的访问权限体系情何以堪!

  (3)String字符串

  这个比较特殊,拷贝的也是一个地址,是个引用,但是在修改时,它会从字符串池(String Pool)中重新生成新的字符串,原有的字符串对象保持不变,在此处我们可以认为String是一个基本类型。(有关字符串的知识详见第4章。)

  明白了这三个规则,上面的例子就很清晰了,小儿子对象是通过拷贝大儿子产生的,其父亲都是同一个人,也就是同一个对象,大儿子修改了父亲名称,小儿子也就跟着修改了—于是,父亲的两个儿子都没了!其实要更正也很简单,clone方法的代码如下:

  

    publicPersonclone(){ Personp=null; try{ p=(Person)super.clone(); p.setFather(newPerson(p.getFather().getName())); }catch(CloneNotSupportedExceptione){ e.printStackTrace(); } returnp; }

  然后再运行,小儿子的父亲就不会是“干爹”了。如此就实现了对象的深拷贝(Deep Clone),保证拷贝出来的对象自成一体,不受“母体”的影响,和new生成的对象没有任何区别。

  注意:浅拷贝只是Java提供的一种简单拷贝机制,不便于直接使用。

  相关链接:

  编写高质量代码:改善Java程序的151个建议(1)

  编写高质量代码:改善Java程序的151个建议(2)

  编写高质量代码:改善Java程序的151个建议(3)

  编写高质量代码:改善Java程序的151个建议(4)

  编写高质量代码:改善Java程序的151个建议(5)

  编写高质量代码:改善Java程序的151个建议(6)

  编写高质量代码:改善Java程序的151个建议(7)

  编写高质量代码:改善Java程序的151个建议(8)

  编写高质量代码:改善Java程序的151个建议(9)

  编写高质量代码:改善Java程序的151个建议(10)

  编写高质量代码:改善Java程序的151个建议(11)

  编写高质量代码:改善Java程序的151个建议(12)

生活会变成什么样子?正因为时光流逝一去不复返,

编写高质量代码:改善Java程序的151个建议(13)

相关文章:

你感兴趣的文章:

标签云: