读Java编程思想随笔の复用类
复用代码是Java众多引人注目的功能之一。但要想成为极具革命性的语言,仅仅能够复制代码并对之加以改变是不够的,它还必须能够做更多的事情。
代码复用第一种方法非常直观:只需在新的类中产生现有类的对象。由于新的类是由现有类的对象组成,所以这种方法称为组合。该方法只是复用了现有程序代码的功能,而非它的形式。
第二种方法则更细致一些,它按照现有类的类型创建新类。无需改变现有类的形式,采用现有类的形式并在其中添加新代码。这种神奇的方式称为继承,而且编译器能够完成其中大部分工作。
先看一下组合语法,代码如下:
1 public class SprinklerSystem { 2 private String value1,value2,value3,value4; 3 private WaterSource waterSource = new WaterSource(); 4 private int i; 5 private float f; 6 public String toString (){ 7 return 8 "value1="+value1+"\n"+ 9 "value2="+value2+"\n"+ 10 "value3="+value3+"\n"+ 11 "value4="+value4+"\n"+ 12 "i="+i+"\n"+ 13 "f="+f+"\n"+ 14 "waterSource="+waterSource; 15 } 16 public static void main (String [] args){ 17 SprinklerSystem ss = new SprinklerSystem(); 18 System.out.println(ss); 19 } 20 } 21 class WaterSource { 22 private String s; 23 WaterSource () { 24 System.out.println("WaterSource ()"); 25 s = "Constructed"; 26 27 } 28 public String toString (){ 29 return s; 30 } 31 } 32 //WaterSource () 33 //value1=null 34 //value2=null 35 //value3=null 36 //value4=null 37 //i=0 38 //f=0.0 39 //waterSource=Constructed
编译器并不是简单地为每一个引用都创建默认对象,这一点是很有意义的,因为要真要那样做的话,就会在许多情况下增加不必要的负担。如果想要初始化这些应用,可以在代码中下列位置进行:
1、在定义对象的地方。这意味着它们总是能够在构造器调用之前被初始化。
2、在类的构造器中
3、就在正要使用这些对象之前,这种方式称为惰性初始化。在生成对象不值得及不必每次都生成对象的情况下,这种方式可以减少额外的负担。
4、使用实例初始化
以下是四种方式的示例:
1 public class Bath { 2 private String s1 = "Happy",s2="Happy",s3,s4; 3 private Soap catlille; 4 private int i; 5 private float toy; 6 public Bath () { 7 System.out.println("Inside Bath()"); 8 s3 = "Joy"; 9 toy = 3.14f; 10 catlille = new Soap(); 11 } 12 13 @Override 14 public String toString() { 15 if (s4==null) 16 s4 = "Joy"; 17 return 18 "s1="+s1+"\n"+"s2="+s2+"\n"+"s3="+s3+"\n"+"s4="+s4+"\n"+ "i="+i+"\n"+ "toy="+toy+"\n"+"catlille="+catlille; 19 } 20 21 public static void main(String[] args) { 22 Bath bath = new Bath(); 23 System.out.println(bath); 24 } 25 26 } 27 class Soap { 28 private String s; 29 Soap () { 30 System.out.println("Soap ()"); 31 s = "Constructed"; 32 } 33 public String toString (){ 34 return s; 35 } 36 } 37 //Inside Bath() 38 //Soap () 39 //s1=Happy 40 //s2=Happy 41 //s3=Joy 42 //s4=Joy 43 //i=0 44 //toy=3.14 45 //catlille=Constructed
再看继承语法,示例代码如下:
1 public class Detergent extends Cleanser{ 2 //change a method 3 public void scrub () { 4 append(" Detergent scrub ()"); 5 super.scrub();//call base-class version 防止递归 6 } 7 //Add methods to the interface 8 public void foam () { 9 append(" foam ()"); 10 } 11 12 public static void main(String[] args) { 13 Detergent x = new Detergent(); 14 x.dilute();x.apply();x.scrub();x.foam(); 15 System.out.println(x); 16 System.out.println("Testing base class:"); 17 Cleanser.main(args); 18 } 19 } 20 class Cleanser { 21 public String s = "Cleanser "; 22 public void append (String a) { 23 s += a; 24 } 25 public void dilute () { 26 append("dilute ()"); 27 } 28 public void apply () { 29 append(" apply ()"); 30 } 31 public void scrub () { 32 append(" scrub ()"); 33 } 34 35 @Override 36 public String toString() { 37 return s; 38 } 39 public static void main(String[] args) { 40 Cleanser x = new Cleanser (); 41 x.dilute();x.apply();x.scrub(); 42 System.out.println(x); 43 } 44 } 45 //Cleanser dilute () apply () Detergent scrub () scrub () foam () 46 //Testing base class: 47 //Cleanser dilute () apply () scrub ()
从上述代码,我们可以看出以下几点:
1、在Detergent类中,肯定有一个默认构造器,此构造器的写法是:
public Detergent(){ super(); }
2、Detergent和Cleanser均含有main方法。可以为每个类都创建一个main方法,这种在每个类中都创建main方法的技术可使每个类的单元测试变得简单易行。
3、即使一个程序中含有多个类,也只有命令行调用的那个类的main方法才会被调用,在此例中,如果命令行时java Detergent,则Detergent的main方法会被调用,那么即使Cleanser类不是public类,那么命令行调用java Cleanser,Cleanser的main方法也会被调用。
4、为了继承,一般规则是将所有数据成员都定义为private,将所有方法都指定为public。
初始化基类
1 public class Cartoon extends Drawing{ 2 Cartoon () { 3 4 super(); 5 System.out.println("Cartoon ()"); 6 } 7 public static void main(String[] args) { 8 Cartoon c = new Cartoon (); 9 } 10 } 11 class Drawing extends Art{ 12 Drawing () { 13 super(); 14 System.out.println("Drawing ()"); 15 } 16 } 17 class Art { 18 Art () { 19 System.out.println("Art ()"); 20 } 21 } 22 23 //Art () 24 //Drawing () 25 //Cartoon ()
构建过程是从“基类”向外扩散的。所以基类在导出类构造器可以访问它之前,就已经完成了初始化。即使你不为Cartoon创建构造器,编译器也会为你合成一个默认的构造器,该构造器将调用基类的构造器。
确保正确清理
在清理方法dispose中,还必须注意对基类清理方法和成员对象清理方法的调用顺序,以防某个子对象依赖于另一个子对象情形的发生。一般而言,所采用的形式应该与C++编译器在其析构函数上所施加的形式相同:首先,执行类的所有特定清理动作,其顺序同生产顺序相反;然后,就如我们示范的那样,调用基类的清理方法。
许多情况下,清理并不是问题,仅需让垃圾回收器完成该动作就行。但当必须亲自处理清理时,就得多加努力和小心。因为,一旦涉及垃圾回收,能够信赖的事就不会很多了。垃圾回收器可能永远也无法被调用,即使被调用,它也可能以任何它想要的顺序来回收对象。最好的办法是除了内存以外,不能依赖垃圾回收器去做任何事情。如果需要进行清理,最好是编写你自己的清理方法,但不要使用finalize()。
名称屏蔽
1 public class Hide { 2 public static void main(String[] args) { 3 Bart b = new Bart(); 4 b.doh(1); 5 b.doh('x'); 6 b.doh(1.0f); 7 b.doh(new Milhouse ()); 8 } 9 } 10 class Homer { 11 char doh (char c) { 12 System.out.println("doh(char)"); 13 return c; 14 } 15 float doh (float c) { 16 System.out.println("doh(float)"); 17 return 1.0f; 18 } 19 } 20 class Bart extends Homer { 21 void doh (Milhouse m) { 22 System.out.println("doh(Milhouse)"); 23 } 24 } 25 class Milhouse { 26 27 } 28 /* 29 doh(float) 30 doh(char) 31 doh(float) 32 doh(Milhouse) 33 */
使用与基类完全相同的特征签名及返回类型来覆盖具有相同名称的方法,是一件极其平常的事。但它也令人迷惑。
Java SE5新增了@override注解,它并不是关键字,但是可以把它当作关键字使用。当你想要复写某个方法时,可以选择添加这个注解,在你不留心重载而并非覆盖该方法时,编译器就会报出一条错误信息。
向上转型
“为新的类提供方法”并不是继承技术中最重要的方面,其最重要的方面是用来表现新类和基类之间的关系。这种关系可用“新类是现有类的一种类型”这句话加以概括。
1 public class Wind extends Instrument{ 2 public static void main(String[] args) { 3 Wind flute = new Wind(); 4 Instrument.tune(flute); 5 } 6 } 7 class Instrument { 8 public void play () { 9 System.out.println("upper cast"); 10 } 11 static void tune (Instrument i) { 12 i.play(); 13 } 14 } 15 //upper cast
由于向上转型是从一个较专用类型向较通用类型转换,所以总是很安全的。也就是说,导出类是基类的一个超集。它可能比基类含有更多的方法,但它至少必须具备基类中的所有方法。
组合和继承的选择
到底是该用组合还是用继承,一个最清晰的判断办法就是问一问自己是否需要从新类向基类进行向上转型。如果必须向上转型,则继承是必要的;但如果不需要,则应该好好考虑继承是不是必须的。
final关键字
1、一个永不改变的编译时常量
2、一个在运行时被初始化的值,而你不希望它被改变
一个既是static有时final的域只占据一段不能改变的存储空间。
当对对象引用而不是基本类型运用final时,其含义就会有一点令人迷惑。对于基本类型,final使数值恒定不变;而对于对象引用,final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象。然而,对象其自身却是可以被修改的,Java并未提供使任何对象恒定不变的途径。这一限制,同样适合于数组。
1 public class FinalData { 2 private static Random rand = new Random(); 3 private String id; 4 public FinalData (String id) { 5 this.id= id; 6 } 7 private final int valueOne = 9; 8 private static final int VALUE_TWO = 99; 9 public static final int VALUE_THREE = 99; 10 11 private final int i4 = rand.nextInt(20); 12 static final int INT_5 = rand.nextInt(20); 13 private Value v1 = new Value(11); 14 private final Value v2 = new Value(22); 15 private static final Value V_3 = new Value(33); 16 private final int [] a= {1,2,3,4,5,6}; 17 @Override 18 public String toString() { 19 return id+":"+"i4="+i4+",INT_5="+INT_5; 20 } 21 public static void main(String[] args) { 22 FinalData fd1 = new FinalData ("fd1"); 23 fd1.v2.i++; 24 fd1.v1 = new Value(9); 25 for (int i = 0; i < fd1.a.length; i++) { 26 fd1.a[i]++; 27 } 28 System.out.println(fd1); 29 System.out.println("Creating new FinalData"); 30 FinalData fd2 = new FinalData ("fd2"); 31 System.out.println(fd1); 32 System.out.println(fd2); 33 } 34 35 } 36 class Value { 37 int i; 38 public Value (int i) { 39 this.i = i; 40 } 41 } 42 //fd1:i4=6,INT_5=8 43 //Creating new FinalData 44 //fd1:i4=6,INT_5=8 45 //fd2:i4=16,INT_5=8
空白final
必须在域的定义处或者每个构造器中用表达式对final进行赋值,这正是final域在使用前总是被初始化的原因所在。
如以下代码:
1 public class BlankFinal { 2 private final int i = 0; 3 private final int j;//blank final 4 private final Poppet p;//blank final reference 5 //blank final must be initialized in the constructor 6 public BlankFinal () { 7 j = 1;//initialize blank final 8 p = new Poppet (1);//initialize blank final reference 9 } 10 public BlankFinal (int x) { 11 j = x; 12 p = new Poppet (x); 13 } 14 15 public static void main(String[] args) { 16 new BlankFinal (); 17 new BlankFinal (47); 18 } 19 } 20 class Poppet { 21 private int i; 22 Poppet (int ii) { 23 i = ii; 24 } 25 }
final参数
你可以读参数,但却无法修改参数。这一特性主要用来向匿名内部类传递数据。
1 public class FinalArguments { 2 void with (final Gizmo g) { 3 //g = new Gizmo();//-bug 4 System.out.println(g); 5 //g.spin(); 6 } 7 void without (Gizmo g) {g = new Gizmo(); g.spin();} 8 int g (final int i) {return i+1;} 9 10 public static void main(String[] args) { 11 FinalArguments bf = new FinalArguments (); 12 bf.with(null); 13 bf.without(null); 14 } 15 } 16 class Gizmo{ 17 public void spin () { 18 System.out.println("spin()"); 19 } 20 }
final方法
使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类来修改它的含义。这是出于设计考虑:想要确保在继承中使方法行为保持不变,并且不会被覆盖。第二个原因是效率。
类中所有private方法都是隐式地指定为final的。由于无法取用private方法,所以也就无法覆盖它。
“覆盖”只有在某方法是基类的接口的一部分时才会出现。即,必须将一个对象向上转型为它的基本类型并调用相同方法。如果某方法为private,它就不是基类接口的一部分。它仅是一些隐藏于类中的程序代码,只不过是具有相同的名称而已。但如果在导出类中以相同的名称生成public、protected或包访问权限的方法话,该方法就不会产生在基类中出现的“仅有相同名称”的情况。此时你并没有覆盖该方法,仅是生成了一个新的方法。
final类
当将某个类的整体定义为final时,就表明了你不打算继承该类,而且也不允许别人这样做。换句话说,出于某种考虑,你对该类的设计永不需要做任何变动,或者出于安全考虑,你不希望它有子类。
请注意,final类的域可以根据个人的意愿选择为是或不是final。不论类是否被定义为final,相同的规则都适用于定义为final的域。然而,由于final禁止继承,所以final类中所有的方法都隐式指定为final,因为无法覆盖它们。
初始化和类的加载
在许多传统语言中,程序被作为启动过程的一部分立刻被加载的。然后是初始化,紧接着程序开始运行。这些语言的初始化过程必须小心控制,以确保定义为static的东西,其初始化顺序不会造成麻烦。
而在Java中就不会出现这个问题,因为它采用了一种不同的加载方式。加载是众多变得更加容易的动作之一,因为Java中所有事物都是对象。请记住,每个类的编译代码都存在于它自己的独立文件中。该文件只在需要使用程序代码时才会被加载。一般来说,“类的代码在初次使用时才加载。”这通常是指加载发生于创建类的第一个对象之时,但是当访问static域或static方法时,也会发生加载。
初次使用之处也是static初始化发生之处。所有static对象和static代码段都会在加载时依程序中顺序而依次初始化。当然定义为static的东西只会被初始化一次。
1 public class Beetle extends Insect{ 2 private int k = printInit("Beetle.k initialized"); 3 public Beetle () { 4 super(); 5 System.out.println("k="+k); 6 System.out.println("j="+j); 7 } 8 private static int x2 = printInit("static Beetle.x2 initialized"); 9 10 public static void main(String[] args) { 11 System.out.println("Beetle constructor"); 12 Beetle b = new Beetle(); 13 } 14 } 15 class Insect { 16 private int i = 9; 17 protected int j; 18 Insect () { 19 System.out.println("i = "+i+", j="+j); 20 j = 39; 21 } 22 private static int x1 = printInit("static Insect.x1 initialized"); 23 static int printInit (String s) { 24 System.out.println(s); 25 return 47; 26 } 27 } 28 //static Insect.x1 initialized 29 //static Beetle.x2 initialized 30 //Beetle constructor 31 //i=9,j=0 32 //Beetle.k initialized 33 //k=47 34 //j=39 35 //static变量最先初始化,而初始化导出类前,先要初始化基类;然后执行main方法,创建对象,而创建导出类对象之前先创建基类;然后初始化构造方法之前,先初始化全局变量