读thinking in java笔记(六):复用类

1. 组合语法
    本书到目前为止,已多次使用组合技术。只需将对象引用置于新类中即可。例如:假设你需要某个对象,它要具有多个String对象、几个基本类型数据,以及另一个类的对象。对于非基本类型的对象,必须将其引用置于新的类中,但可以直接定义基本类型数据:

class WaterSource {
  private String s;
  WaterSource() {
    System.out.println("WaterSource()");
    s = "Constructed";
  }
  public String toString() { return s; }
}   

public class SprinklerSystem {
  private String valve1, valve2, valve3, valve4;
  private WaterSource source = new WaterSource();
  private int i;
  private float f;
  public String toString() {
    return
      "valve1 = " + valve1 + " " +
      "valve2 = " + valve2 + " " +
      "valve3 = " + valve3 + " " +
      "valve4 = " + valve4 + "\n" +
      "i = " + i + " " + "f = " + f + " " +
      "source = " + source;
  } 
  public static void main(String[] args) {
    SprinklerSystem sprinklers = new SprinklerSystem();
    System.out.println(sprinklers);
  }
} /* Output:
WaterSource()
valve1 = null valve2 = null valve3 = null valve4 = null
i = 0 f = 0.0 source = Constructed
*/

    在上面两个类所定义的方法中,有一个很特殊:toString()。每一个非基本类型的对象都有一个toString()方法,而且当编译器需要一个String而你却只有一个对象时,该方法便会被调用。所以在SprinklerSystem.toString()的表达式中:“source = ” + source;编译器将会得知你想要将一个String对象(“source=”)同WaterSource相加。由于只能将一个String对象和另一个String对象相加,因此编译器会告诉你:”我将调用toString()把source转换成为一个String!” 这样做之后,它就能够将两个String连接到一起并将结果传递给print();每当想要所创建的类具备这样的行为时,仅需要编写一个toString()即可。
    正如我们在第二章中所提到的,类中域为基本类型时能够自动被初始化为零。但是对象引用被被初始化为null,而且如果你试图为它们调用任何方法,都会得到一个异常—运行时错误。很方便的是,在不抛出异常的情况下仍旧可以打印一null引用。
    编译器并不是简单地为每一个引用都创建默认的对象,如果想初始化这些引用,可以在代码中的下列位置进行:
     1. 在定义对象的地方。这意味着它们总是能够在构造器调用之前被初始化。
     2. 在类的构造器中。
     3. 就在正要使用这些对象之前,这种方式称为惰性初始化。在生成对象不值得及不必每次都生成对象的情况下,这种方式可以减少额外的负担。
     4. 使用实例初始化。
    四种实例初始化的示例:

class Soap {
  private String s;
  Soap() {
    print("Soap()");
    s = "Constructed";
  }
  public String toString() { return s; }
}   

public class Bath {
  private String // Initializing at point of definition:
    s1 = "Happy",
    s2 = "Happy",
    s3, s4;
  private Soap castille;
  private int i;
  private float toy;
  public Bath() {
    print("Inside Bath()");
    s3 = "Joy";
    toy = 3.14f;
    castille = new Soap();
  } 
  // Instance initialization:
  { i = 47; }
  public String toString() {
    if(s4 == null) // Delayed initialization:
      s4 = "Joy";
    return
      "s1 = " + s1 + "\n" +
      "s2 = " + s2 + "\n" +
      "s3 = " + s3 + "\n" +
      "s4 = " + s4 + "\n" +
      "i = " + i + "\n" +
      "toy = " + toy + "\n" +
      "castille = " + castille;
  } 
  public static void main(String[] args) {
    Bath b = new Bath();
    print(b);
  }
} /* Output:
Inside Bath()
Soap()
s1 = Happy
s2 = Happy
s3 = Joy
s4 = Joy
i = 47
toy = 3.14
castille = Constructed
*/

2. 继承语法
    继承是所有OOP语言和Java语言不可缺少的组成部分。当创建一个类时,总是在继承,因此,除非已明确指出用从其他类中继承,否则就是咋隐式地从Java的标准根类Object进行继承。
    继承的声明是通过extends关键字实现的,例如:

class Cleanser {
  private String s = "Cleanser";
  public void append(String a) { s += a; }
  public void dilute() { append(" dilute()"); }
  public void apply() { append(" apply()"); }
  public void scrub() { append(" scrub()"); }
  public String toString() { return s; }
  public static void main(String[] args) {
    Cleanser x = new Cleanser();
    x.dilute(); x.apply(); x.scrub();
    print(x);
  }
}   

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();
    print(x);
    print("Testing base class:");
    Cleanser.main(args);
  } 
} /* Output:
Cleanser dilute() apply() Detergent.scrub() scrub() foam()
Testing base class:
Cleanser dilute() apply() scrub()
*///:~

    Cleanser 中所有的方法都必须是public的,这一点非常重要。请记住,如果没有加任何访问权限修饰词,那么成员默认的访问权限是包访问权限,它仅允许包内的成员访问。因此,在此类的方法中,如果没有访问权限修饰符,在此包中的任何人都可以使用这些方法。例如:Detergent就不成问题。但是,其他包中的某个类若要从Cleanser中继承,则只要访问public成员。所以,为了继承一般的规则是将所有的数据成员都指定为private的,将所有的刚都指定为public,当然在特殊情况下,必须做出调整,但是上述方法的确是一个很有用的规则。
    在Cleanser的类中有一组方法:append()、dilute()、apply()、scrub()和toString()。由于Detergent是由关键字extends从Cleanser导出的,所以他可以自动的获得这些方法,尽管并不能看到这些方法在Detergent中显式定义。因此,可以将继承视作是对类的复用。
    正如我们在scrub()中所见,使用基类中定义的方法及对它进行修改是可行的。在此例中,你可能想要在新版本中调用从基类继承而来的方法。但是在scurb()中,并不能直接调用scurb()。Java用super关键字表示超类的意思,当前类就是从超类继承来的。为此,表达式super.scrub()将调用基类版本的scrub()方法。
    在继承的过程中,并不一定非得使用基类的方法。也可以在导出类中添加新方法,其添加方式与在类中添加任意方法一样,即对其加以定义即可。
  2.1 初始化基类
    由于现在涉及基类和导出类这两个类,而不是只有一个类,所以要试着想象导出类所产生的结果对象,会有点困惑。从外部来看,它就像是一个与基类具有相同接口的新类,或许还会有一些额外的方法和域。但继承并不只是复制基类的接口。当创建了一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象与你用基类直接创建的对象是一样的。二者区别在于,后者来自外部,而基类的子对象被包装在导出类对象内部。
    当然对基类子对象的正确初始化也是至关重要的,而且也仅有一种方法来保证这一点;在构造器中调用基类构造器来执行初始化,而基类构造器具有执行基类初始化所需要的所有知识和能力。Java会自动在导出类的构造器中插入对基类构造器的调用。

class Art {
  Art() { print("Art constructor"); }
}

class Drawing extends Art {
  Drawing() { print("Drawing constructor"); }
}

public class Cartoon extends Drawing {
  public Cartoon() { print("Cartoon constructor"); }
  public static void main(String[] args) {
    Cartoon x = new Cartoon();
  }
} /* Output:
Art constructor
Drawing constructor
Cartoon constructor
*/

    我们发现,构建过程是从基类“向外”扩散的,所以基类在导出类构造器可以访问它之前,就已经完成了初始化。即使你不为Cartoon()创建构造器,编译器也会为你合成一个默认的构造器,该构造器将调用基类的构造器。
  2.2 带参数的构造器
    上例中各个类都含有默认的构造器,即这些构造器都不带参数。编译器可以轻松的调用它们是因为不必考虑要传递什么样的参数的问题。但是,如果没有默认的基类构造器,或者想调用一个带有参数的基类构造器,就必须用关键字super显式地编写调用基类构造器的语句,并且配以适当的参数列表:

class Game {
  Game(int i) {
    print("Game constructor");
  }
}

class BoardGame extends Game {
  BoardGame(int i) {
    super(i);
    print("BoardGame constructor");
  }
}   

public class Chess extends BoardGame {
  Chess() {
    super(11);
    print("Chess constructor");
  }
  public static void main(String[] args) {
    Chess x = new Chess();
  }
} /* Output:
Game constructor
BoardGame constructor
Chess constructor
*/

    如果不在BoardGame()中调用基类构造器,编译器将“抱怨”无法找到符合Game()形式的构造器。而且,调用基类构造器必须是你在导出类构造器中要做的第一件事(如果你做错了,编译器会提醒你)。
3. 组合使用组合和继承
    同时使用组合和继承是很常见的事,下例就展示了同时使用这两种技术,并配以必要的构造器进行初始化,来创建更加复杂的类:

class Plate {
  Plate(int i) {
    print("Plate constructor");
  }
}

class DinnerPlate extends Plate {
  DinnerPlate(int i) {
    super(i);
    print("DinnerPlate constructor");
  }
}   

class Utensil {
  Utensil(int i) {
    print("Utensil constructor");
  }
}

class Spoon extends Utensil {
  Spoon(int i) {
    super(i);
    print("Spoon constructor");
  }
}

class Fork extends Utensil {
  Fork(int i) {
    super(i);
    print("Fork constructor");
  }
}   

class Knife extends Utensil {
  Knife(int i) {
    super(i);
    print("Knife constructor");
  }
}

// A cultural way of doing something:
class Custom {
  Custom(int i) {
    print("Custom constructor");
  }
}   

public class PlaceSetting extends Custom {
  private Spoon sp;
  private Fork frk;
  private Knife kn;
  private DinnerPlate pl;
  public 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);
    print("PlaceSetting constructor");
  }
  public static void main(String[] args) {
    PlaceSetting x = new PlaceSetting(9);
  }
} /* Output:
Custom constructor
Utensil constructor
Spoon constructor
Utensil constructor
Fork constructor
Utensil constructor
Knife constructor
Plate constructor
DinnerPlate constructor
PlaceSetting constructor
*/

    虽然编译器强制你去初始化基类,并且要求你要在构造器起始处就要这么做,但是它并不监督你必须将成员对象也初始化,因此在这一点上你必须注意。
    这些类如此清晰的分离着实让人惊讶。甚至不需要这些方法的源代码就可以复用这些代码,我们至多只需要导入一个包。(对于继承和组合来说,都是如此。)
  3.1 名称屏蔽
    如果Java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽在基类中的任何版本。因此,无论是在该层或者它的基类中对方法进行定义,重载机制都可以正常工作:

class Homer {
    char doh(char c) {
      System.out.println("doh(char)");
      return 'd';
    }
    float doh(float f) {
      System.out.println("doh(float)");
      return 1.0f;
    }
  }

  class Milhouse {}

  class Bart extends Homer {
    void doh(Milhouse m) {
      System.out.println("doh(Milhouse)");
    }

    @Override
    char doh(char c) {
        // TODO Auto-generated method stub
        System.out.println("当被重写(覆盖)后将调用子类的方法!");
        return 'e';
    }
  }

  public class Hide {
    public static void main(String[] args) {
      Bart b = new Bart();
      b.doh(1);
      b.doh('x');
      b.doh(1.0f);
      b.doh(new Milhouse());
    }
  } 
  /**
   * doh(float)
     当被重写(覆盖)后将调用子类的方法!
     doh(float)
     doh(Milhouse)
   */

    可以看到,虽然Bart引入了一个新的重载方法,但是在Bart中Homer的所有重载方法都是可用的。但是,如果你重写(即方法签名(方法名+ 参数列表)相同)了该方法,那么将会调用子类中的方法。
4. 在组合和继承之间选择
    在继承的时候,使用某个现有类,并开发它的一个特殊版本。通常,这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化。略微思考一下就会发现,用一个“交通工具”对象来构成一部“车子”是毫无意义的,因为“车子”并不包含“交通工具”,它仅是一种交通工具(is-a关系)。“is-a”(是一个)的关系是用继承来表达的,而“has-a”(有一个)则是用组合来表达的。下面给出一个很好的表达了组合关系的例子:

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
  public 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就是起这个作用的。它指明“就类用户而言,这是private的,但对于任何继承于此类的导出类或其他任何位于同一包内的类来说,它却是可以访问的。(protected也是提供了包内访问权限)”
    尽管可以创建protected域,但是最好的方式还是将域保持为private;你应当一直保留“更改底层实现的权利”。然后通过protected方法来控制类的继承者的访问权限。
6. 向上转型
    “为新的类提供方法”并不是继承技术中最重要的方面,其最重要的方面是用来表现新类和基类之间的关系。这种关系可以用“新类是现有类的一种类型”这句话加以概括。
    这个描述并非只是一个解释继承的华丽的方式,这直接是由语言所支撑的。例如,假设有一个称为Instrument的代表乐曲的基类和一个称为Wind的导出类。由于继承可以确保基类中所有的方法在导出类中也同样有效,所以能够向基类发送的所有信息同样也可以向导出类发送。如果Instrument类具有一个play()方法,那么Wind乐器也将同样具备。这意味着我们可以准确地说Wind对象也是一种类型的Instrument。

class Instrument {
  public void play() {}
  static void tune(Instrument i) {
    // ...
    i.play();
  }
}

// Wind objects are instruments
// because they have the same interface:
public class Wind extends Instrument {
  public static void main(String[] args) {
    Wind flute = new Wind();
    Instrument.tune(flute); // Upcasting
  }
}

    在此例当中,tune()方法可以接受Instrument引用,但在Wind.main()中,传递给tune()方法的是一个Wind引用。鉴于Java对类型的检查十分严格,接受某种类型的方法同样可以接受另外一种类型就会显得非常奇怪,除非你认识到Wind对象同样也是一种Instrument对象,而且也不存在任何tune()方法是可以通过Instrument来调用,同时又不存在于Wind之中。在tune()中,程序代码可以对Instrument和它所有的导出类起作用,这种将Wind引用转换为Instrument引用的动作,我们称为向上转型。
  6.1 再讨论组合与继承
    在面向对象编程中,生成和使用程序代码最有可能采用的方法就是直接将数据和方法包装进一个类中,并使用该类的对象。也可以运用组合技术使用现有类来开发新的类;而继承技术其实是不太常用的。因此,尽管在教授OOP的过程中我们多次强调继承,但这并不意味着要尽可能使用它。相反,应当慎用这一技术,其使用场合仅限于你确定使用该技术确实有效的情况。到底是该用组合还是用继承,一个最清晰的判断方法就是问自己是否需要从新类向基类进行向上转型。如果必须向上转型,则继承是必要的。但如果不需要,则应当好好考虑自己是否需要继承。
7. final关键字
    根据上下文环境,关键字final的含义存在着细微的差别,但通常它指的是“这是无法改变的。”不想做改变可能处于两种原因:设计或效率。由于这两个原因相差很远,所以关键字final可能会被误用。可能使用final的三种情况:数据、方法、类。
  7.1 final数据
    许多编程语言都有某种方法,来向编译器告知一块数据时恒定不变的。有时数据的恒定不变是很有用的,比如:
    1. 一个永不改变的编译时常量。
    2. 一个在运行时初始化的值,而你不希望它被改变。
    对于编译器常量这种情况,编译器可以将该常量值代入任何可能用到它的计算式中,也就是说,可以在编译时执行计算式,这减轻了一些运行时的负担。在Java中这类常量必须是基本数据类型,并且以关键字final表示。在对这个常量定义的时候,必须对其进行赋值。
    一个既是static又是final的域只占据一段不能改变的存储空间。
    当对对象引用而不是基本类型运用final时,其含义会有一点令人迷惑。对于基本类型,final使数值恒定不变;而用于对象引用,final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象。然而,对象其自身却是可以被修改的,Java并未提供使任何对象恒定不变的途径。这一限制同样适用数组,它也是对象。
    下面的示例师范了final域的情况,注意,根据惯例,既是static又是final的域(编译器常量)将用大写表示,并使用下划线分割各个单词:

class Value {
  int i; // Package access
  public Value(int i) { this.i = i; }
}

public class FinalData {
  private static Random rand = new Random(47);
  private String id;
  public FinalData(String id) { this.id = id; }
  // Can be compile-time constants:
  private final int valueOne = 9;
  private static final int VALUE_TWO = 99;
  // Typical public constant:
  public static final int VALUE_THREE = 39;
  // Cannot be compile-time constants:
  private final int i4 = rand.nextInt(20);
  static final int INT_5 = rand.nextInt(20);
  private Value v1 = new Value(11);
  private final Value v2 = new Value(22);
  private static final Value VAL_3 = new Value(33);
  // Arrays:
  private final int[] a = { 1, 2, 3, 4, 5, 6 };
  public String toString() {
    return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5;
  }
  public static void main(String[] args) {
    FinalData fd1 = new FinalData("fd1");
    //! fd1.valueOne++; // Error: can't change value
    fd1.v2.i++; // Object isn't constant!
    fd1.v1 = new Value(9); // OK -- not final
    for(int i = 0; i < fd1.a.length; i++)
      fd1.a[i]++; // Object isn't constant!
    //! fd1.v2 = new Value(0); // Error: Can't
    //! fd1.VAL_3 = new Value(1); // change reference
    //! fd1.a = new int[3];
    print(fd1);
    print("Creating new FinalData");
    FinalData fd2 = new FinalData("fd2");
    print(fd1);
    print(fd2);
  }
}

    由于valueOne和VALUE_TWO都是带有编译时数值的final基本类型,所以它们二者均可以用作编译器常量,并且没有重大区别。VAL_THREE是一种更加典型的对常量进行定义的方式:定义为public,则可以被用于包之外;定义为static,则强调只有一份;定义为final,则说明它是一个常量。请注意,定有恒定初始值的static final基本类型全用大写字母命名,并且词之间用下划线隔开。
    我们不能因为某数据是final的就认为在编译时可以知道它的值。在运行时使用随机生成的数值来初始化i4和INT_5就说明了这一点。此区别只有当数值在运行时被初始化才会显现。i4的值会随着创建的新对象而改变,INT_5的值不能随着不同对象而改变,这是因为INT_5是static的,在装载时已被初始化,而不是每次创建新对象时都初始化。
    v1到VAL_3这些变量说明了final引用的意义。正如在main中所看到的,不能因为v2是final的,就认为无法改变它的值。由于它是一个引用,final意味着无法将v2再次指向另一个新的对象,这对数组具有同样的意义。看起来,使引用成为final没有使基本类型成final的用处大。
    7.1.1 空白final
    Java允许生成“空白final”,所谓空白final是指被声明为final但又未给定初值的域。无论什么情况,编译器都确保空白final在使用前必须被初始化。但是,空白final在关键字final的使用上提供了更大的灵活性,为此,一个类中的final域就可以做到根据对象而有所不同,却又保持其恒定不变的特性。例子:

class Poppet {
  private int i;
  Poppet(int ii) { i = ii; }
}

public class BlankFinal {
  private final int i = 0; // Initialized final
  private final int j; // Blank final
  private final Poppet p; // Blank final reference
  // Blank finals MUST be initialized in the constructor:
  public BlankFinal() {
    j = 1; // Initialize blank final
    p = new Poppet(1); // Initialize blank final reference
  }
  public BlankFinal(int x) {
    j = x; // Initialize blank final
    p = new Poppet(x); // Initialize blank final reference
  }
  public static void main(String[] args) {
    new BlankFinal();
    new BlankFinal(47);
  }
}

    必须在域的定义处或者每个构造器中用表达式对final进行赋值,这正是final域在使用前总是被初始化的原因所在。
    7.1.2 final参数
    Java允许在参数列表中以声明的方式将参数指明为final。这意味着你无法再方法中更改参数引用所指向的对象:

class Gizmo {
  public void spin() {}
}

public class FinalArguments {
  void with(final Gizmo g) {
    //! g = new Gizmo(); // Illegal -- g is final
  }
  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);
  }
}

    方法f()和g()展示了当基本类型的参数被指明为final时所出现的结果:你可以读参数,但却无法修改参数。这一特性主要用来向匿名内部类传递参数。
  7.2 final方法
    使用final方法的原因有两个。第一个原因是处于设计的考虑:想要确保在继承中使方法行为保持不变,并且不会被覆盖。第二个原因是效率。但是在Java SE5/6之后,应该让编译器和JVM去处理效率问题,只有在想要明确禁止覆盖时,才将方法设置为final的。
    7.2.1 final和private关键字
    类中所有的private方法都隐式地指定为final的。由于无法取用private方法,所以也就无法覆盖它。可以对private方法添加final修饰词,但这并不能给方法增加任何额外的意义。

class WithFinals {
  // Identical to "private" alone:
  private final void f() { print("WithFinals.f()"); }
  // Also automatically "final":
  private void g() { print("WithFinals.g()"); }
}

class OverridingPrivate extends WithFinals {
  private final void f() {
    print("OverridingPrivate.f()");
  }
  private void g() {
    print("OverridingPrivate.g()");
  }
}

class OverridingPrivate2 extends OverridingPrivate {
  public final void f() {
    print("OverridingPrivate2.f()");
  }
  public void g() {
    print("OverridingPrivate2.g()");
  }
}

public class FinalOverridingIllusion {
  public static void main(String[] args) {
    OverridingPrivate2 op2 = new OverridingPrivate2();
    op2.f();
    op2.g();
    // You can upcast:
    OverridingPrivate op = op2;
    // But you can't call the methods:
    //! op.f();
    //! op.g();
    // Same here:
    WithFinals wf = op2;
    //! wf.f();
    //! wf.g();
  }
}

    “覆盖”,只有在某方法是基类的一部分时才会出现。即,必须能将一个对象向上转型为它的基本类型并调用相同的方法。如果某方法为private,它就不是基类接口的一部分。它仅是一些隐藏于类中的程序代码,只不过是具有相同的名称而已。但如果在导出类中以相同的名称生成一个public、protected或包访问权限的方法的话,该方法就不会产生在基类中出现的“仅具有相同名称”的情况。此时你并没有覆盖该方法,仅是生成了一个新的方法。由于private方法无法触及而且能有效隐藏,所以,除了把它看成是因为它所归属的类的组织结构的原因而存在外,其他任何事物都不需要考虑它。
  7.3 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修饰词,但这并不会增添任何意义。
8. 初始化及类的加载
    Java中加载是众多变得更加容易的动作之一,因为Java中的所有事物都是对象。请记住,每个类的编译代码都存在于它自己的独立的文件中。该文件只在需要使用程序代码时才会被加载。一般来说,可以说:“类的代码在初次使用时才加载。”这通常是指加载发生于创建类的第一个对象之时,但是当访问static域或static方法时,也会发生加载。(构造器也是static方法,尽管static关键字并没有显式的写出来。因此更准确的讲,类是在其任何static成员被访问时加载的)
    初次使用之处也是static初始化发生之处。所有的static对象和static代码段都会在加载时依程序中的顺序(即,定义类时的书写顺序)而以此初始化。当然定义为static的东西只会被初始化一次。
  8.1 继承与初始化

例子:
class Insect {
  private int i = 9;
  protected int j;
  Insect() {
    print("i = " + i + ", j = " + j);
    j = 39;
  }
  private static int x1 =
    printInit("static Insect.x1 initialized");
  static int printInit(String s) {
    print(s);
    return 47;
  }
}

public class Beetle extends Insect {
  private int k = printInit("Beetle.k initialized");
  public Beetle() {
    print("k = " + k);
    print("j = " + j);
  }
  private static int x2 =
    printInit("static Beetle.x2 initialized");
  public static void main(String[] args) {
    print("Beetle constructor");
    Beetle b = new Beetle();
  }
}
/**
  * static Insect.x1 initialized
  * static Beetle.x2 initialized
  * Beetle constructor
  * i = 9, j = 0
  * Beetle.k initialized
  * k = 47
  * j = 39
*/

    在Bettle上运行Java时,所发生的第一件事就是试图访问Bettle.main()(一个static方法),于是加载器开始启动并找出Bettle类的编译代码(在名为Bettle.class文件之中)。在对它进行加载的过程中,编译器注意到它有一个基类(这是由关键字extends得知的),于是它继续进行加载。不管你是否打算产生一个该基类的对象,这都要发生(请尝试将对象创建代码注释掉,以证明这一点)。
    如果该基类还有其自身的基类,那么第二个基类就会被加载,如此类推。接下来,根基类中的static初始化(在此例中为Insert)即会被执行,然后是下一个导出类,以此类推。这种方式很重要,因为导出类的static初始化可能会依赖于基类成员能否被正确的初始化。
    到此为止,必要的类都已加载完毕,对象就可以被创建了。首先,对象中所有的基本类型都会被设为默认值,对象引用被设为null,这是通过将对象内存设为二进制值而一举生成的。然后,基类的构造器会被调用。在本例中,它是被自动调用的。但也可以用super来指定对基类构造器的调用(正如在Bettle()构造器中的第一步操作)。基类构造器和导出类的构造器一样,以相同的顺序来经历相同的过程。在基类构造器完成之后,实例变量按其次序被初始化。最后,构造器的其余部分被执行。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值