Java编程思想之类再生

来源:互联网 发布:软件学报 编辑:程序博客网 时间:2024/04/29 04:54

1、合成的语法

   为进行合成,我们只需在新类里简单地置入对象句柄即可。举个例子来说,假定需要在一个对象里容纳几个String对象、两种基本数据类型以及属于另一个类的一个对象。对于非基本类型的对象来说,只需将句柄置于新类即可;而对于基本数据类型来说,则需在自己的类中定义它们。如下所示:

  

class WaterSource {  private String s;  WaterSource() {    System.out.println("WaterSource()");    s = new String("Constructed");  }  public String toString() { return s; }}public class SprinklerSystem {  private String valve1, valve2, valve3, valve4;  WaterSource source;  int i;  float f;  void print() {    System.out.println("valve1 = " + valve1);    System.out.println("valve2 = " + valve2);    System.out.println("valve3 = " + valve3);    System.out.println("valve4 = " + valve4);    System.out.println("i = " + i);    System.out.println("f = " + f);    System.out.println("source = " + source);  }  public static void main(String[] args) {    SprinklerSystem x = new SprinklerSystem();    x.print();  }} 
打印语句:
valve1 = nullvalve2 = nullvalve3 = nullvalve4 = nulli = 0f = 0.0source = null
在类内作为字段使用的基本数据会初始化成零,但对象句柄会初始化成null。
编译器并不只是为每个句柄创建一个默认对象,因为那样会在许多情况下招致不必要的开销。如希望句柄得到初始化,可在下面这些地方进行:(1) 在对象定义的时候。这意味着它们在构建器调用之前肯定能得到初始化。(2) 在那个类的构建器中。(3) 紧靠在要求实际使用那个对象之前。这样做可减少不必要的开销——假如对象并不需要创建的话。 下面向大家展示了所有这三种方法:
class Soap {  private String s;  Soap() {    System.out.println("Soap()");    s = new String("Constructed");  }  public String toString() { return s; }}public class Bath {  private String     // Initializing at point of definition:    s1 = new String("Happy"),     s2 = "Happy",     s3, s4;  Soap castille;  int i;  float toy;  Bath() {    System.out.println("Inside Bath()");    s3 = new String("Joy");    i = 47;    toy = 3.14f;    castille = new Soap();  }  void print() {    // Delayed initialization:    if(s4 == null)      s4 = new String("Joy");    System.out.println("s1 = " + s1);    System.out.println("s2 = " + s2);    System.out.println("s3 = " + s3);    System.out.println("s4 = " + s4);    System.out.println("i = " + i);    System.out.println("toy = " + toy);    System.out.println("castille = " + castille);  }  public static void main(String[] args) {    Bath b = new Bath();    b.print();  }} 

请注意在Bath构建器中,在所有初始化开始之前执行了一个语句。如果不在定义时进行初始化,仍然不能保证能在将一条消息发给一个对象句柄之前会执行任何初始化——除非出现不可避免的运行期违例。
下面是该程序的输出:

Inside Bath()Soap()s1 = Happys2 = Happys3 = Joys4 = Joyi = 47toy = 3.14castille = Constructed

调用print()时,它会填充s4,使所有字段在使用之前都获得正确的初始化。
2、继承的语法
  
class Cleanser {  private String s = new String("Cleanser");  public void append(String a) { s += a; }  public void dilute() { append(" dilute()"); }  public void apply() { append(" apply()"); }  public void scrub() { append(" scrub()"); }  public void print() { System.out.println(s); }  public static void main(String[] args) {    Cleanser x = new Cleanser();    x.dilute(); x.apply(); x.scrub();    x.print();  }}public class Detergent extends Cleanser {  // Change a method:  public void scrub() {    append(" Detergent.scrub()");    super.scrub(); // Call base-class version  }  // Add methods to the interface:  public void foam() { append(" foam()"); }  // Test the new class:  public static void main(String[] args) {    Detergent x = new Detergent();    x.dilute();    x.apply();    x.scrub();    x.foam();    x.print();    System.out.println("Testing base class:");    Cleanser.main(args);  }} ///:~

这个例子向大家展示了大量特性。首先,在Cleanser append()方法里,字串同一个s连接起来。这是用“+=”运算符实现的。同“+”一样,“+=”被Java用于对字串进行“过载”处理。
  2.1  初始化基础类
    由于这儿涉及到两个类——基础类及衍生类,而不再是以前的一个,所以在想象衍生类的结果对象时,可能会产生一些迷惑。从外部看,似乎新类拥有与基础类相同的接口,而且可包含一些额外的方法和字段。但继承并非仅仅简单地复制基础类的接口了事。创建衍生类的一个对象时,它在其中包含了基础类的一个“子对象”。这个子对象就象我们根据基础类本身创建了它的一个对象。从外部看,基础类的子对象已封装到衍生类的对象里了。当然,基础类子对象应该正确地初始化,而且只有一种方法能保证这一点:在构建器中执行初始化,通过调用基础类构建器,后者有足够的能力和权限来执行对基础类的初始化。在衍生类的构建器中,Java会自动插入对基础类构建器的调用。下面这个例子向大家展示了对这种三级继承的应用:
    
class Art {  Art() {    System.out.println("Art constructor");  }}class Drawing extends Art {  Drawing() {    System.out.println("Drawing constructor");  }}public class Cartoon extends Drawing {  Cartoon() {    System.out.println("Cartoon constructor");  }  public static void main(String[] args) {    Cartoon x = new Cartoon();  }} 

该程序的输出显示了自动调用:

Art constructor
Drawing constructor
Cartoon constructor

可以看出,构建是在基础类的“外部”进行的,所以基础类会在衍生类访问它之前得到正确的初始化。
即使没有为Cartoon()创建一个构建器,编译器也会为我们自动合成一个默认构建器,并发出对基础类构建器的调用。

3、合成与继承的结合
  许多时候都要求将合成与继承两种技术结合起来使用。下面这个例子展示了如何同时采用继承与合成技术,从而创建一个更复杂的类,同时进行必要的构建器初始化工作:
  
class Plate {  Plate(int i) {    System.out.println("Plate constructor");  }}class DinnerPlate extends Plate {  DinnerPlate(int i) {    super(i);    System.out.println(      "DinnerPlate constructor");  }}class Utensil {  Utensil(int i) {    System.out.println("Utensil constructor");  }}class Spoon extends Utensil {  Spoon(int i) {    super(i);    System.out.println("Spoon constructor");  }}class Fork extends Utensil {  Fork(int i) {    super(i);    System.out.println("Fork constructor");  }}class Knife extends Utensil {  Knife(int i) {    super(i);    System.out.println("Knife constructor");  }}// A cultural way of doing something:class Custom {  Custom(int i) {    System.out.println("Custom constructor");  }}public class PlaceSetting extends Custom {  Spoon sp;  Fork frk;  Knife kn;  DinnerPlate pl;  PlaceSetting(int i) {    super(i + 1);    sp = new Spoon(i + 2);    frk = new Fork(i + 3);    kn = new Knife(i + 4);    pl = new DinnerPlate(i + 5);    System.out.println(      "PlaceSetting constructor");  }  public static void main(String[] args) {    PlaceSetting x = new PlaceSetting(9);  }} 

尽管编译器会强迫我们对基础类进行初始化,并要求我们在构建器最开头做这一工作,但它并不会监视我们是否正确初始化了成员对象。所以对此必须特别加以留意。
  
4、到底选择合成还是继承
  无论合成还是继承,都允许我们将子对象置于自己的新类中。大家或许会奇怪两者间的差异,以及到底该如何选择。如果想利用新类内部一个现有类的特性,而不想使用它的接口,通常应选择合成。也就是说,我们可嵌入一个对象,使自己能用它实现新类的特性。但新类的用户会看到我们已定义的接口,而不是来自嵌入对象的接口。考虑到这种效果,我们需在新类里嵌入现有类的private对象。有些时候,我们想让类用户直接访问新类的合成。也就是说,需要将成员对象的属性变为public。成员对象会将自身隐藏起来,所以这是一种安全的做法。而且在用户知道我们准备合成一系列组件时,接口就更容易理解。car(汽车)对象便是一个很好的例子:
   
class Engine {  public void start() {}  public void rev() {}  public void stop() {}}class Wheel {  public void inflate(int psi) {}}class Window {  public void rollup() {}  public void rolldown() {}}class Door {  public Window window = new Window();  public void open() {}  public void close() {}}public class Car {  public Engine engine = new Engine();  public Wheel[] wheel = new Wheel[4];  public Door left = new Door(),       right = new Door(); // 2-door  Car() {    for(int i = 0; i < 4; i++)      wheel[i] = new Wheel();  }  public static void main(String[] args) {    Car car = new Car();    car.left.window.rollup();    car.wheel[0].inflate(72);  }} ///:~

由于汽车的装配是故障分析时需要考虑的一项因素(并非只是基础设计简单的一部分),所以有助于客户程序员理解如何使用类,而且类创建者的编程复杂程度也会大幅度降低。
如选择继承,就需要取得一个现成的类,并制作它的一个特殊版本。通常,这意味着我们准备使用一个常规用途的类,并根据特定的需求对其进行定制。只需稍加想象,就知道自己不能用一个车辆对象来合成一辆汽车——汽车并不“包含”车辆;相反,它“属于”车辆的一种类别。“属于”关系是用继承来表达的,而“包含”关系是用合成来表达的
5、protected
  现在我们已理解了继承的概念,protected这个关键字最后终于有了意义。在理想情况下,private成员随时都是“私有”的,任何人不得访问。但在实际应用中,经常想把某些东西深深地藏起来,但同时允许访问衍生类的成员。protected关键字可帮助我们做到这一点。它的意思是“它本身是私有的,但可由从这个类继承的任何东西或者同一个包内的其他任何东西访问”。也就是说,Java中的protected会成为进入“友好”状态。我们采取的最好的做法是保持成员的private状态——无论如何都应保留对基 础的实施细节进行修改的权利。在这一前提下,可通过protected方法允许类的继承者进行受到控制的访问:
   
import java.util.*;class Villain {  private int i;  protected int read() { return i; }  protected void set(int ii) { i = ii; }  public Villain(int ii) { i = ii; }  public int value(int m) { return m*i; }}public class Orc extends Villain {  private int j;  public Orc(int jj) { super(jj); j = jj; }  public void change(int x) { set(x); }} 

可以看到,change()拥有对set()的访问权限,因为它的属性是protected(受到保护的)。
6、final关键字
  由于语境(应用环境)不同,final关键字的含义可能会稍微产生一些差异。但它最一般的意思就是声明“这个东西不能改变”。之所以要禁止改变,可能是考虑到两方面的因素:设计或效率。由于这两个原因颇有些区别,所以也许会造成final关键字的误用。
  我们将讨论final关键字的三种应用场合:数据、方法以及类:
  6.1 final数据
    许多程序设计语言都有自己的办法告诉编译器某个数据是“常数”。常数主要应用于下述两个方面:(1) 编译期常数,它永远不会改变(2) 在运行期初始化的一个值,我们不希望它发生变化对于编译期的常数,编译器(程序)可将常数值“封装”到需要的计算过程里。也就是说,计算可在编译期间提前执行,从而节省运行时的一些开销。在Java中,这些形式的常数必须属于基本数据类型(Primitives),而且要用final关键字进行表达。在对这样的一个常数进行定义的时候,必须给出一个值。无论static还是final字段,都只能存储一个数据,而且不得改变。若随同对象句柄使用final,而不是基本数据类型,它的含义就稍微让人有点儿迷糊了。对于基本数据类型,final会将值变成一个常数;但对于对象句柄,final会将句柄变成一个常数。进行声明时,必须将句柄初始化到一个具体的对象。而且永远不能将句柄变成指向另一个对象。然而,对象本身是可以修改的。Java对此未提供任何手段,可将一个对象直接变成一个常数(但是,我们可自己编写一个类,使其中的对象具有“常数”效果)。这一限制也适用于数组,它也属于对象。下面是演示final字段用法的一个例子:
   
class Value {  int i = 1;}public class FinalData {  // Can be compile-time constants  final int i1 = 9;  static final int I2 = 99;  // Typical public constant:  public static final int I3 = 39;  // Cannot be compile-time constants:  final int i4 = (int)(Math.random()*20);  static final int i5 = (int)(Math.random()*20);    Value v1 = new Value();  final Value v2 = new Value();  static final Value v3 = new Value();  //! final Value v4; // Pre-Java 1.1 Error:                       // no initializer  // Arrays:  final int[] a = { 1, 2, 3, 4, 5, 6 };  public void print(String id) {    System.out.println(      id + ": " + "i4 = " + i4 +       ", i5 = " + i5);  }  public static void main(String[] args) {    FinalData fd1 = new FinalData();    //! fd1.i1++; // Error: can't change value    fd1.v2.i++; // Object isn't constant!    fd1.v1 = new Value(); // OK -- not final    for(int i = 0; i < fd1.a.length; i++)      fd1.a[i]++; // Object isn't constant!    //! fd1.v2 = new Value(); // Error: Can't     //! fd1.v3 = new Value(); // change handle    //! fd1.a = new int[3];    fd1.print("fd1");    System.out.println("Creating new FinalData");    FinalData fd2 = new FinalData();    fd1.print("fd1");    fd2.print("fd2");  }} ///:~

由于i1和I2都是具有final属性的基本数据类型,并含有编译期的值,所以它们除了能作为编译期的常数使用外,在任何导入方式中也不会出现任何不同。I3是我们体验此类常数定义时更典型的一种方式:public表示它们可在包外使用;Static强调它们只有一个而final表明它是一个常数。注意对于含有固定初始化值(即编译期常数)的fianl static基本数据类型,它们的名字根据规则要全部采用大写。也要注意i5在编译期间是未知的,所以它没有大写。
不能由于某样东西的属性是final,就认定它的值能在编译时期知道。i4和i5向大家证明了这一点。它们在运行期间使用随机生成的数字。例子的这一部分也向大家揭示出将final值设为static和非static之间的差异。只有当值在运行期间初始化的前提下,这种差异才会揭示出来。因为编译期间的值被编译器认为是相同的。这种差异可从输出结果中看出:
fd1: i4 = 15, i5 = 9Creating new FinalDatafd1: i4 = 15, i5 = 9fd2: i4 = 10, i5 = 9

注意对于fd1和fd2来说,i4的值是唯一的,但i5的值不会由于创建了另一个FinalData对象而发生改变。那是因为它的属性是static,而且在载入时初始化,而非每创建一个对象时初始化。
从v1到v4的变量向我们揭示出final句柄的含义。正如大家在main()中看到的那样,并不能认为由于v2属于final,所以就不能再改变它的值。然而,我们确实不能再将v2绑定到一个新对象,因为它的属性是final。这便是final对于一个句柄的确切含义。我们会发现同样的含义亦适用于数组,后者只不过是另一种类型的句柄而已。将句柄变成final看起来似乎不如将基本数据类型变成final那么有用。
6.2 空白final
   Java 1.1允许我们创建“空白final”,它们属于一些特殊的字段。尽管被声明成final,但却未得到一个初始值。无论在哪种情况下,空白final都必须在实际使用前得到正确的初始化。而且编译器会主动保证这一规定得以贯彻。然而,对于final关键字的各种应用,空白final具有最大的灵活性。举个例子来说,位于类内部的一个final字段现在对每个对象都可以有所不同,同时依然保持其“不变”的本质。下面列出一个例子:
   
class Poppet { }class BlankFinal {  final int i = 0; // Initialized final  final int j; // Blank final  final Poppet p; // Blank final handle  // Blank finals MUST be initialized  // in the constructor:  BlankFinal() {    j = 1; // Initialize blank final    p = new Poppet();  }  BlankFinal(int x) {    j = x; // Initialize blank final    p = new Poppet();  }  public static void main(String[] args) {    BlankFinal bf = new BlankFinal();  }} 

现在强行要求我们对final进行赋值处理——要么在定义字段时使用一个表达 式,要么在每个构建器中。这样就可以确保final字段在使用前获得正确的初始化。
    6.3 final自变量
     Java 1.1允许我们将自变量设成final属性,方法是在自变量列表中对它们进行适当的声明。这意味着在一个方法的内部,我们不能改变自变量句柄指向的东西。如下所示:
     
class Gizmo {  public void spin() {}}public class FinalArguments {  void with(final Gizmo g) {    //! g = new Gizmo(); // Illegal -- g is final    g.spin();  }  void without(Gizmo g) {    g = new Gizmo(); // OK -- g not final    g.spin();  }  // void f(final int i) { i++; } // Can't change  // You can only read from a final primitive:  int g(final int i) { return i + 1; }  public static void main(String[] args) {    FinalArguments bf = new FinalArguments();    bf.without(null);    bf.with(null);  }} 

注意此时仍然能为final自变量分配一个null(空)句柄,同时编译器不会捕获它。这与我们对非final自变量采取的操作是一样的。
方法f()和g()向我们展示出基本类型的自变量为final时会发生什么情况:我们只能读取自变量,不可改变它。

      6.4 final方法
      之所以要使用final方法,可能是出于对两方面理由的考虑。第一个是为方法“上锁”,防止任何继承类改变它的本来含义。设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可以采取这种做法。采用final方法的第二个理由是程序执行的效率。将一个方法设成final后,编译器就可以把对那个方法的所有调用都置入“嵌入”调用里。只要编译器发现一个final方法调用,就会(根据它自己的判断)忽略为执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除堆栈自变量;最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替换方法调用。这样做可避免方法调用时的系统开销。当然,若方法体积太大,那么程序也会变得雍肿,可能受到到不到嵌入代码所带来的任何性能提升。因为任何提升都被花在方法内部的时间抵消了。Java编译器能自动侦测这些情况,并颇为“明智”地决定是否嵌入一个final方法。然而,最好还是不要完全相信编译器能正确地作出所有判断。通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。类内所有private方法都自动成为final。由于我们不能访问一个private方法,所以它绝对不会被其他方法覆盖(若强行这样做,编译器会给出错误提示)。可为一个private方法添加final指示符,但却不能为那个方法提供任何额外的含义。
     6.5 final类
    如果说整个类都是final(在它的定义前冠以final关键字),就表明自己不希望从这个类继承,或者不允许其他任何人采取这种操作。换言之,出于这样或那样的原因,我们的类肯定不需要进行任何改变;或者出于安全方面的理由,我们不希望进行子类化(子类处理)。除此以外,我们或许还考虑到执行效率的问题,并想确保涉及这个类各对象的所有行动都要尽可能地有效。如下所示:   
class SmallBrain {}final class Dinosaur {  int i = 7;  int j = 1;  SmallBrain x = new SmallBrain();  void f() {}}//! class Further extends Dinosaur {}// error: Cannot extend final class 'Dinosaur'public class Jurassic {  public static void main(String[] args) {    Dinosaur n = new Dinosaur();    n.f();    n.i = 40;    n.j++;  }} 

注意数据成员既可以是final,也可以不是,取决于我们具体选择。应用于final的规则同样适用于数据成员,无论类是否被定义成final。将类定义成final后,结果只是禁止进行继承——没有更多的限制。然而,由于它禁止了继承,所以一个final类中的所有方法都默认为final。因为此时再也无法覆盖它们。所以与我们将一个方法明确声明为final一样,编译器此时有相同的效率选择。
可为final类内的一个方法添加final指示符,但这样做没有任何意义。



0 0