Polymorphism【2】

Constructors and polymorphism

As usual, constructors are different from other kinds of methods. This is also true when polymorphism is involved. Even though constructors are not polymorphic (they’re actually static methods, but the static declaration is implicit), it’s important to understand the way constructors work in complex hierarchies and with polymorphism. This understanding will help you avoid unpleasant entanglements.

一般來講,構造函數是與其它方法存在差別的,當涉及到多態的時候也不例外。儘管構造方法不是多態的(它們是static方法,儘管關鍵詞並沒有聲明),關於理解構造方法在複雜的繼承類以及多態中是如何工作的是很重要的。理解了這些以後可以幫助你避免很多不愉快的糾纏。

Order of constructor calls

The order of constructor calls was briefly discussed in Chapter 4 and again in Chapter 6, but that was before polymorphism was introduced.

關於構造方法的調用順序在第四章的時候曾經簡要的提到,第六章也再次提及,但是這些都是在多態機制沒有產生之前提到的。

A constructor for the base class is always called during the construction process for a derived class, chaining up the inheritance hierarchy so that a constructor for every base class is called. This makes sense because the constructor has a special job: to see that the object is built properly. A derived class has access to its own members only, and not to those of the base class (whose members are typically private). Only the base-class constructor has the proper knowledge and access to initialize its own elements. Therefore, it’s essential that all constructors get called, otherwise the entire object wouldn’t be constructed. That’s why the compiler enforces a constructor call for every portion of a derived class. It will silently call the default constructor if you don’t explicitly call a base-class constructor in the derived-class constructor body. If there is no default constructor, the compiler will complain. (In the case where a class has no constructors, the compiler will automatically synthesize a default constructor.)

一個基類的構造方法在它的派生類的對象創建過程中是會被調用的,根據繼承關係圖一層層追溯,所有基類的構造方法都會被調用的。造成這種情況是因爲構造方法有特殊的功能:驗證對象是否被完全的創建了。派生類只能訪問自己的成員,並不能訪問基類的諸如private訪問級別的成員。只有基類的構造方法擁有正確的內容並且訪問和初始化這些元素。因此,所有的構造方法被調用是必然的,否則對象將不能被創建。這就是爲什麼編譯器要調用所有的基類的構造方法了。這樣及時你不聲明調用基類的構造方法它也會調用基類的默認的構造方法,但是如果那裏沒有默認的構造方法,編譯器將出錯。(如果某個類沒有構造方法那麼編譯器將自動爲你做一個構造方法)。

Let’s take a look at an example that shows the effects of composition, inheritance, and polymorphism on the order of construction:

讓我們看一個例子,給我們展示了合成,繼承以及多態在調用順序上對程序的影響。

package c07;

import com.bruceeckel.simpletest.*;

 

class Meal {

  Meal() { System.out.println("Meal()"); }

}

 

class Bread {

  Bread() { System.out.println("Bread()"); }

}

 

class Cheese {

  Cheese() { System.out.println("Cheese()"); }

}

 

class Lettuce {

  Lettuce() { System.out.println("Lettuce()"); }

}

 

class Lunch extends Meal {

  Lunch() { System.out.println("Lunch()"); }

}

 

class PortableLunch extends Lunch {

  PortableLunch() { System.out.println("PortableLunch()");}

}

 

public class Sandwich extends PortableLunch {

  private static Test monitor = new Test();

  private Bread b = new Bread();

  private Cheese c = new Cheese();

  private Lettuce l = new Lettuce();

  public Sandwich() {

    System.out.println("Sandwich()");

  }

  public static void main(String[] args) {

    new Sandwich();

    monitor.expect(new String[] {

      "Meal()",

      "Lunch()",

      "PortableLunch()",

      "Bread()",

      "Cheese()",

      "Lettuce()",

      "Sandwich()"

    });

  }

}

This example creates a complex class out of other classes, and each class has a constructor that announces itself. The important class is Sandwich, which reflects three levels of inheritance (four, if you count the implicit inheritance from Object) and three member objects. You can see the output when a Sandwich object is created in main( ). This means that the order of constructor calls for a complex object is as follows:

  1. The base-class constructor is called. This step is repeated recursively such that the root of the hierarchy is constructed first, followed by the next-derived class, etc., until the most-derived class is reached.  

  2. Member initializers are called in the order of declaration.  

  3. The body of the derived-class constructor is called.

這個例子使用其它的類創建了一個組合的類,並且每個類都有構造方法來描述自己。這裏最重要的類是sandwich,這個類用到了三層繼承(如果考慮暗含的從Object繼承的話就是四層)和三個對象。你可以看到在Main方法中創建sandwich對象的時候的輸出,這就表明了一個組合的類的對象創建的時候構造方法的調用是如下這樣的:

1、      調用基類的構造方法。這個步驟一直循環遞歸直到繼承的根對象被創建之後,首先第一層繼承類的對象被創建,知道最後一層繼承類的對象被創建;

2、      成員的調用順序和成員的成名順序相同;

3、      執行繼承類的構造方法的正文;

The order of the constructor calls is important. When you inherit, you know all about the base class and can access any public and protected members of the base class. This means that you must be able to assume that all the members of the base class are valid when you’re in the derived class. In a normal method, construction has already taken place, so all the members of all parts of the object have been built. Inside the constructor, however, you must be able to assume that all members that you use have been built. The only way to guarantee this is for the base-class constructor to be called first. Then when you’re in the derived-class constructor, all the members you can access in the base class have been initialized. Knowing that all members are valid inside the constructor is also the reason that, whenever possible, you should initialize all member objects (that is, objects placed in the class using composition) at their point of definition in the class (e.g., b, c, and l in the preceding example). If you follow this practice, you will help ensure that all base class members and member objects of the current object have been initialized. Unfortunately, this doesn’t handle every case, as you will see in the next section.

關於構造方法的調用順序是很重要的。當你採用繼承技術的時候你知道所有基類,並且可以訪問所有訪問基類中所有的publicprotected的成員。但是這以爲這你必須要保證基類中所有的成員在繼承類中都是有效的。對於普通的過程來講,構造方法已經執行結束後,這個類的所有成員也就被創建了。但是在構造方法中,你必須確保你所使用的所有成員都已經被創建。而確保這樣的只能是所有的基類構造方法已經被首先調用了,這樣派生類的構造方法執行時,基類的構造方法已經被調用而且數據成員已經創建了,可以提供給你訪問。知道了哪些成員在構造方法中已經是創建好了你也就知道了何時可用在定義對象的時候對對象進行初始化,如果你這麼作的話那麼就可以保證所有的基類成員以及基類對象都被初始化了,但是這並不能解決所有的情況,下面的章節中我們會講到。

Inheritance and cleanup

When using composition and inheritance to create a new class, most of the time you won’t have to worry about cleaning up; subobjects can usually be left to the garbage collector. If you do have cleanup issues, you must be diligent and create a dispose( ) method (the name I have chosen to use here; you may come up with something better) for your new class. And with inheritance, you must override dispose( ) in the derived class if you have any special cleanup that must happen as part of garbage collection. When you override dispose( ) in an inherited class, it’s important to remember to call the base-class version of dispose( ), since otherwise the base-class cleanup will not happen. The following example demonstrates this:

當你使用繼承和組合來創建新的類的時候,大部分時候你不必擔心清理;派生類對象通常都由垃圾回收器去處理。如果你確實需要清理的話,你必須要爲你的新類創建一個dispose()方法(我在這裏選擇了這個名字,你可以選擇更好的名字),如果使用了繼承的話,如果你希望在執行垃圾回收清理的時候作一些其它的事情,那麼你必需要要在派生類中覆寫這個方法,當你在派生類中覆寫這個方法的時候,你需要記住需要調用基類的dispose()方法,這是非常重要的,否則基類的垃圾回收清理工作將不被執行,下面的例子論證了這點:

import com.bruceeckel.simpletest.*;

 

class Characteristic {

  private String s;

  Characteristic(String s) {

    this.s = s;

    System.out.println("Creating Characteristic " + s);

  }

  protected void dispose() {

    System.out.println("finalizing Characteristic " + s);

  }

}

 

class Description {

  private String s;

  Description(String s) {

    this.s = s;

    System.out.println("Creating Description " + s);

  }

  protected void dispose() {

    System.out.println("finalizing Description " + s);

  }

}

 

class LivingCreature {

  private Characteristic p = new Characteristic("is alive");

  private Description t =

    new Description("Basic Living Creature");

  LivingCreature() {

    System.out.println("LivingCreature()");

  }

  protected void dispose() {

    System.out.println("LivingCreature dispose");

    t.dispose();

    p.dispose();

  }

}

 

class Animal extends LivingCreature {

  private Characteristic p= new Characteristic("has heart");

  private Description t =

    new Description("Animal not Vegetable");

  Animal() {

    System.out.println("Animal()");

  }

  protected void dispose() {

    System.out.println("Animal dispose");

    t.dispose();

    p.dispose();

    super.dispose();

  }

}

 

class Amphibian extends Animal {

  private Characteristic p =

    new Characteristic("can live in water");

  private Description t =

    new Description("Both water and land");

  Amphibian() {

    System.out.println("Amphibian()");

  }

  protected void dispose() {

    System.out.println("Amphibian dispose");

    t.dispose();

    p.dispose();

    super.dispose();

  }

}

 

public class Frog extends Amphibian {

  private static Test monitor = new Test();

  private Characteristic p = new Characteristic("Croaks");

  private Description t = new Description("Eats Bugs");

  public Frog() {

    System.out.println("Frog()");

  }

  protected void dispose() {

    System.out.println("Frog dispose");

    t.dispose();

    p.dispose();

    super.dispose();

  }

  public static void main(String[] args) {

    Frog frog = new Frog();

    System.out.println("Bye!");

    frog.dispose();

    monitor.expect(new String[] {

      "Creating Characteristic is alive",

      "Creating Description Basic Living Creature",

      "LivingCreature()",

      "Creating Characteristic has heart",

      "Creating Description Animal not Vegetable",

      "Animal()",

      "Creating Characteristic can live in water",

      "Creating Description Both water and land",

      "Amphibian()",

      "Creating Characteristic Croaks",

      "Creating Description Eats Bugs",

      "Frog()",

      "Bye!",

      "Frog dispose",

      "finalizing Description Eats Bugs",

      "finalizing Characteristic Croaks",

      "Amphibian dispose",

      "finalizing Description Both water and land",

      "finalizing Characteristic can live in water",

      "Animal dispose",

      "finalizing Description Animal not Vegetable",

      "finalizing Characteristic has heart",

      "LivingCreature dispose",

      "finalizing Description Basic Living Creature",

      "finalizing Characteristic is alive"

    });

  }

}

Each class in the hierarchy also contains a member objects of types Characteristic and Description, which must also be disposed. The order of disposal should be the reverse of the order of initialization, in case one subobject is dependent on another. For fields, this means the reverse of the order of declaration (since fields are initialized in declaration order). For base classes (following the form that’s used in C++ for destructors), you should perform the derived-class cleanup first, then the base-class cleanup. That’s because the derived-class cleanup could call some methods in the base class that require the base-class components to be alive, so you must not destroy them prematurely. From the output you can see that all parts of the Frog object are disposed in reverse order of creation.

在繼承關係中的每個類都包含了CharacteristticDescription類的很多對象,它們同樣也需要被釋放。對象之間是存在依賴關係的,因此釋放的順序和初始化的順序是正好相反的,也就是說釋放的順序應該和聲明的順序正好相反,因爲數據項是按照聲明的辦法初始化的。對於基類(它採用了C++的析構函數的形式),你需要現執行派生類的清理工作然後再是基類的清理工作。這是因爲派生類的對象可能會調用基類的某些方法,所以需要基類對象必須存在,所以你不能過早的釋放掉它們。通過輸出的結果你可用年點Frog對象釋放的順序和創建的順序相反。

From this example, you can see that although you don’t always need to perform cleanup, when you do, the process requires care and awareness.

通過這個例子,雖然你不需要經常的執行清理動作,但是當你需要的時候,這個過程你還是需要知道和小心的。

Behavior of polymorphic methods inside constructors

The hierarchy of constructor calls brings up an interesting dilemma. What happens if you’re inside a constructor and you call a dynamically-bound method of the object being constructed? Inside an ordinary method, you can imagine what will happen: The dynamically-bound call is resolved at run time, because the object cannot know whether it belongs to the class that the method is in or some class derived from it. For consistency, you might think this is what should happen inside constructors.

構造方法的調用順序引出了一個很有趣的現象。如果你再一個構造方法中調用正在創建的這個對象的一個動態綁定的方法會發生什麼呢?如果是在一個普通的方法中,你可以設想應該是這樣:動態綁定的調用只有在運行時才決定,因爲對象不知道它屬於當前所在類的方法還是在這個類的派生類中。按照一致性考慮的話,你可能認爲構造方法內也應該是這樣執行。

This is not exactly the case. If you call a dynamically-bound method inside a constructor, the overridden definition for that method is used. However, the effect can be rather unexpected and can conceal some difficult-to-find bugs.

這不是真正的情況。如果你在構造方法中調用了一個動態綁定的方法,那麼會用覆寫後的那個方法。然而,這樣的影響超出了我們的預想,並且會隱藏很多難以發現的bugs

Conceptually, the constructor’s job is to bring the object into existence (which is hardly an ordinary feat). Inside any constructor, the entire object might be only partially formed—you can know only that the base-class objects have been initialized, but you cannot know which classes are inherited from you. A dynamically bound method call, however, reaches “outward” into the inheritance hierarchy. It calls a method in a derived class. If you do this inside a constructor, you call a method that might manipulate members that haven’t been initialized yet—a sure recipe for disaster.

概念性的來講,構造方法只是爲了創建對象(這並不是很簡單的事情)。在構造方法中,對象很可能只是創建了一部分,你知道的僅僅是基類部分初始化了,但是不知道又繼承出了什麼。動態綁定方法的調用,然而,這將涉及到繼承類圖。它調用了它派生類中的方法。如果你在構造方法中這麼作的話可能會訪問到一個沒有創建初始化的成員,  這肯定是會出問題的。

You can see the problem in the following example:

import com.bruceeckel.simpletest.*;

abstract class Glyph {

  abstract void draw();

  Glyph() {

    System.out.println("Glyph() before draw()");

    draw();

    System.out.println("Glyph() after draw()");

  }

}

 

class RoundGlyph extends Glyph {

  private int radius = 1;

  RoundGlyph(int r) {

    radius = r;

    System.out.println(

      "RoundGlyph.RoundGlyph(), radius = " + radius);

  }

  void draw() {

    System.out.println(

      "RoundGlyph.draw(), radius = " + radius);

  }

}

 

public class PolyConstructors {

  private static Test monitor = new Test();

  public static void main(String[] args) {

    new RoundGlyph(5);

    monitor.expect(new String[] {

      "Glyph() before draw()",

      "RoundGlyph.draw(), radius = 0",

      "Glyph() after draw()",

      "RoundGlyph.RoundGlyph(), radius = 5"

    });

  }

}

In Glyph, the draw( ) method is abstract, so it is designed to be overridden. Indeed, you are forced to override it in RoundGlyph. But the Glyph constructor calls this method, and the call ends up in RoundGlyph.draw( ), which would seem to be the intent. But if you look at the output, you can see that when Glyph’s constructor calls draw( ), the value of radius isn’t even the default initial value 1. It’s 0. This would probably result in either a dot or nothing at all being drawn on the screen, and you’d be left staring, trying to figure out why the program won’t work.

Glyph類中,draw()方法是abstract的,所以它設計的目的就是爲了被覆寫,事實上也確實在RoundGlyph中對該方法盡心了覆寫。但是在基類Glyph中對該方法進行了調用,而這個方法最終以調用RoundGlay.draw()方法而結束,這一切看起來是正常的。但是你可以看到當Glyph調用draw()的時候radius的值不是默認的初始值,而是0。這會造成結果在屏幕上可能是個點或者什麼都沒有,而你只能目不轉睛的試圖找出爲什麼程序會這樣執行。

The order of initialization described in the earlier section isn’t quite complete, and that’s the key to solving the mystery. The actual process of initialization is:

關於初始化的順序的描述在以前早些的章節中描述的並不是很完善,而這恰恰是揭開這層神祕面紗的關鍵。完整的初始化的過程應該是如下這樣:

  1. The storage allocated for the object is initialized to binary zero before anything else happens.  

  2. The base-class constructors are called as described previously. At this point, the overridden draw( ) method is called (yes, before the RoundGlyph constructor is called), which discovers a radius value of zero, due to Step 1.  

  3. Member initializers are called in the order of declaration.  

  4. The body of the derived-class constructor is called.

1. 在進行所有的工作之前所有的對象分配的內存會被初始化爲二進制的0

2. 在前面的章節提到的會先調用基類的構造方法,在這點上,派生類覆寫的draw()方法被調用,而且是在RoundGlyph構造方法被執行之前,受到第一步的影響radius的值是0

3. 成員的初始化和聲明的順序相同;

4. 調用派生類的構造方法;

There’s an upside to this, which is that everything is at least initialized to zero (or whatever zero means for that particular data type) and not just left as garbage. This includes object references that are embedded inside a class via composition, which become null. So if you forget to initialize that reference, you’ll get an exception at run time. Everything else gets zero, which is usually a telltale value when looking at output.

所有的都被初始化爲0這是有一定的優勢的(無論什麼類型都被初始化爲0),這樣不會留下垃圾。這也包含哪些採用合成技術生成的對象,被初始化爲null,所以當你忘記初始化這個對象的時候,在運行時你會得到一個異常信息。其它所有的都被初始化爲0,這樣在查看輸出結果的時候是有據可循的。

On the other hand, you should be pretty horrified at the outcome of this program. You’ve done a perfectly logical thing, and yet the behavior is mysteriously wrong, with no complaints from the compiler. (C++ produces more rational behavior in this situation.) Bugs like this could easily be buried and take a long time to discover.

從另一方面來說,你會因爲程序的這個輸出結果感到很驚詫。你作了一個完美的邏輯但是這個輸出的結果卻很奇怪,編譯器也沒有提示任何信息(C++在這樣的情況下會產生更理性的行爲輸出),錯誤很容易的就發生了但是可能需要你化肥很長的時間去解決。

As a result, a good guideline for constructors is, “Do as little as possible to set the object into a good state, and if you can possibly avoid it, don’t call any methods.” The only safe methods to call inside a constructor are those that are final in the base class. (This also applies to private methods, which are automatically final.) These cannot be overridden and thus cannot produce this kind of surprise.

總結的來說構造方法應該完成的是“作儘可能少的事情把對象創建好,儘可能的去避免調用任何方法”,在構造方法中調用那些標識爲final的安全的方法,當然也包含private的方法因爲默認爲final了,這些方法不能被覆寫所以也就不會產生這麼奇怪的問題了。

Designing with inheritance

Once you learn about polymorphism, it can seem that everything ought to be inherited, because polymorphism is such a clever tool. This can burden your designs; in fact, if you choose inheritance first when you’re using an existing class to make a new class, things can become needlessly complicated.

一旦你學習了多態,看起來就像所有的都應該採用繼承技術,因爲多態是如此智能的工具。這樣會加大你設計的負擔;事實上,當你遇到使用一個存在的類來創建一個新類的時候你首先選擇的是繼承,事情往往會變得很複雜。

A better approach is to choose composition first, especially when it’s not obvious which one you should use. Composition does not force a design into an inheritance hierarchy. But composition is also more flexible since it’s possible to dynamically choose a type (and thus behavior) when using composition, whereas inheritance requires an exact type to be known at compile time. The following example illustrates this:

最好的辦法是首先採用合成技術,尤其是你不知道繼承哪個類的時候。合成不會讓你的設計陷入一個繼承關係圖中。但是合成還是很靈活的,因爲它是動態的選擇類型和行爲的,而繼承則必須在編譯期間就需要明確的類型。下面例子論證了這點:

import com.bruceeckel.simpletest.*;

 

abstract class Actor {

  public abstract void act();

}

 

class HappyActor extends Actor {

  public void act() {

    System.out.println("HappyActor");

  }

}

 

class SadActor extends Actor {

  public void act() {

    System.out.println("SadActor");

  }

}

 

class Stage {

  private Actor actor = new HappyActor();

  public void change() { actor = new SadActor(); }

  public void performPlay() { actor.act(); }

}

 

public class Transmogrify {

  private static Test monitor = new Test();

  public static void main(String[] args) {

    Stage stage = new Stage();

    stage.performPlay();

    stage.change();

    stage.performPlay();

    monitor.expect(new String[] {

      "HappyActor",

      "SadActor"

    });

  }

}

A Stage object contains a reference to an Actor, which is initialized to a HappyActor object. This means performPlay( ) produces a particular behavior. But since a reference can be rebound to a different object at run time, a reference for a SadActor object can be substituted in actor, and then the behavior produced by performPlay( ) changes. Thus you gain dynamic flexibility at run time. (This is also called the State Pattern. See Thinking in Patterns (with Java) at www.BruceEckel.com.) In contrast, you can’t decide to inherit differently at run time; that must be completely determined at compile time.

Stage對象中包含了一個Actor對象,這個對象被初始化爲了一個HappyActor的對象。這意味着performPlay()會產生特定的行爲。但是在運行期間它可以被賦值爲其它的對象,它可以被SadActor對象替換,因爲它們都是Actor的對象,這時performPlay()產生的行爲就發生了變化。因此你嚐到了多態在運行時綁定的甜頭。反過來看,在運行時你不能再去繼承不同的類,這些必須在編譯的時候就確定下來。

A general guideline is “Use inheritance to express differences in behavior, and fields to express variations in state.” In the preceding example, both are used; two different classes are inherited to express the difference in the act( ) method, and Stage uses composition to allow its state to be changed. In this case, that change in state happens to produce a change in behavior.

一個一般的準則是“使用繼承來表示不同的行爲,而數據成員則來表示不同的狀態”,在上面的章節中我們都用到了。兩個不同的繼承下來的類在act()方法中行爲確實不同,而Stage類則採用合成技術允許它們互換,而狀態上的變換則帶來了行爲上的變化。

Pure inheritance vs. extension

When studying inheritance, it would seem that the cleanest way to create an inheritance hierarchy is to take the “pure” approach. That is, only methods that have been established in the base class or interface are to be overridden in the derived class, as seen in this diagram:

This can be called a pure “is-a” relationship because the interface of a class establishes what it is. Inheritance guarantees that any derived class will have the interface of the base class and nothing less. If you follow this diagram, derived classes will also have no more than the base-class interface.

當你學習繼承的時候,研究繼承的最好的辦法就是創建一個純繼承關係圖。在基類或者接口中只是聲明這些不允許被覆寫的方法,就像下面這張圖:

This can be thought of as pure substitution, because derived class objects can be perfectly substituted for the base class, and you never need to know any extra information about the subclasses when you’re using them:

這可以被看作是純的替換,因爲派生類的對象可以完美的被替換爲基類的對象,當你使用的時候你不需要知道派生類的額外的一些信息。

That is, the base class can receive any message you can send to the derived class because the two have exactly the same interface. All you need to do is upcast from the derived class and never look back to see what exact type of object you’re dealing with. Everything is handled through polymorphism.

基類對象可用接受到所有的你發送給派生類對象的指示,因爲兩個對象擁有完全相同的接口。你只需要將對象上傳轉化爲基類對象不需要知道額外的信息,而這些都是通過多態實現的。

When you see it this way, it seems like a pure is-a relationship is the only sensible way to do things, and any other design indicates muddled thinking and is by definition broken. This too is a trap. As soon as you start thinking this way, you’ll turn around and discover that extending the interface (which, unfortunately, the keyword extends seems to encourage) is the perfect solution to a particular problem. This could be termed an “is-like-a” relationship, because the derived class is like the base class—it has the same fundamental interface—but it has other features that require additional methods to implement:

這樣看來的話,“is-a”的關係是一種很明智的思路來作事情,而其它的設計則是混亂無章的。這是一種誤解,當你開始這樣思考問題,你會回過頭來發現擴展接口對很多問題來講也是很好的解決方案。這可以被看作是“is like a”的關係,因爲派生類的對象像基類的對象,它擁有同樣的接口,而派生類對象又擁有一些特徵的方法來實現:

While this is also a useful and sensible approach (depending on the situation), it has a drawback. The extended part of the interface in the derived class is not available from the base class, so once you upcast, you can’t call the new methods:

當這稱爲一個很有用而且可以來解決某些問題的明智的手段的時候,它也有一些弊端。這些在派生類中擴展出來的部分在基類是無效的,所以你一旦上傳轉化你則不能再調用這些方法。

If you’re not upcasting in this case, it won’t bother you, but often you’ll get into a situation in which you need to rediscover the exact type of the object so you can access the extended methods of that type. The following section shows how this is done.

如果你不需要上傳轉化的話,這個問題不會纏繞你,但是大部分時候你會遇到你需要得到對象的確切類型信息依賴訪問派生類中擴展的方法。下面的章節中會展現:

Downcasting and run-time type identification

Since you lose the specific type information via an upcast (moving up the inheritance hierarchy), it makes sense that to retrieve the type information—that is, to move back down the inheritance hierarchy—you use a downcast. However, you know an upcast is always safe; the base class cannot have a bigger interface than the derived class. Therefore, every message you send through the base class interface is guaranteed to be accepted. But with a downcast, you don’t really know that a shape (for example) is actually a circle. It could instead be a triangle or square or some other type.

通過採用上傳轉化的技術你丟失了對象本來所屬的類型信息,那就意味着如果你需要重新得到對象的類型的話就的使用downcast順着繼承的關係下移。然而你是知道的上傳轉化是安全的,因爲基類不可能擁有比派生類更寬泛的接口,所以發送給基類接口的信息都會被正確的傳達。但是下傳轉化你則不清楚shape是否是一個circle。它可能是一個triangle或者suqare再或者其它的形狀。

To solve this problem, there must be some way to guarantee that a downcast is correct, so that you won’t accidentally cast to the wrong type and then send a message that the object can’t accept. This would be quite unsafe.

要解決這個問題,你必須有一些方式能夠保證下傳轉化是正確的,這樣就不會發生轉化爲錯誤的類型,並且發送這個對象無法接受的信息,這將很不安全。

In some languages (like C++) you must perform a special operation in order to get a type-safe downcast, but in Java, every cast is checked! So even though it looks like you’re just performing an ordinary parenthesized cast, at run time this cast is checked to ensure that it is in fact the type you think it is. If it isn’t, you get a ClassCastException. This act of checking types at run time is called run-time type identification (RTTI). The following example demonstrates the behavior of RTTI:

在諸如C++的語言中,你必須使用一個特殊的操作符來獲得一個安全的下傳轉化,但是在Java中所有的轉換都是要校驗的,所以及時看起來僅僅是加上一個括號操作符的轉化,在運行期間也是要校驗的來確保這是你所期望的類。如果不是的話你將會得到一個ClassCastException錯誤。在運行時校驗類型的方法叫做run-time type identification (RTTI)。下面的例子就是來論證這個觀點的:

class Useful {

  public void f() {}

  public void g() {}

}

 

class MoreUseful extends Useful {

  public void f() {}

  public void g() {}

  public void u() {}

  public void v() {}

  public void w() {}

}

 

public class RTTI {

  public static void main(String[] args) {

    Useful[] x = {

      new Useful(),

      new MoreUseful()

    };

    x[0].f();

    x[1].g();

    ((MoreUseful)x[1]).u();

    ((MoreUseful)x[0]).u();

  }

}

As in the diagram, MoreUseful extends the interface of Useful. But since it’s inherited, it can also be upcast to a Useful. You can see this happening in the initialization of the array x in main( ). Since both objects in the array are of class Useful, you can send the f( ) and g( ) methods to both, and if you try to call u( ) (which exists only in MoreUseful), you’ll get a compile-time error message.

正如

正如圖中所表示的MoreUseful擴展了Useful的接口。因爲採用了繼承所以你可以將他的對象上傳轉化爲Useful的對象。你可以在初始化x數組的時候看到。因爲數組中的兩個對象都是Useful的,所以你可以給兩個對象發送f()g()的方法,但是如果你想調用u()(只是在派生類MoreUseful中存在),你將會得到一個編譯時錯誤。

If you want to access the extended interface of a MoreUseful object, you can try to downcast. If it’s the correct type, it will be successful. Otherwise, you’ll get a ClassCastException. You don’t need to write any special code for this exception, since it indicates a programmer error that could happen anywhere in a program.

如果你希望訪問擴展接口後的MoreUseful的對象,你可以採用下傳轉化。如果是正確的類型,這將會成功。否則你將會得到一個ClassCastException異常信息。你不需要爲該異常信息寫特殊的代碼,因爲它提示程序錯誤,它會發生在程序的任何一個地方。

There’s more to RTTI than a simple cast. For example, there’s a way to see what type you’re dealing with before you try to downcast it. All of Chapter 10 is devoted to the study of different aspects of Java run-time type identification.

RTTI比這種簡單的轉化更加複雜。舉例來說,有一個辦法,可以讓你獲得在進行下船轉化之前你操作的對象的類型,將在本書的第十章詳細的介紹運行時類型鑑別。

Summary

Polymorphism means “different forms.” In object-oriented programming, you have the same face (the common interface in the base class) and different forms using that face: the different versions of the dynamically bound methods.

多態意味着不同的形式,在面向對象編程中,擁有同樣的名稱和不同的形式:各個版本的動態綁定方法。

You’ve seen in this chapter that it’s impossible to understand, or even create, an example of polymorphism without using data abstraction and inheritance. Polymorphism is a feature that cannot be viewed in isolation (like a switch statement can, for example), but instead works only in concert, as part of a “big picture” of class relationships. People are often confused by other, non-object-oriented features of Java, like method overloading, which are sometimes presented as object-oriented. Don’t be fooled: If it isn’t late binding, it isn’t polymorphism.

在本章中如果你不具有抽象和繼承的知識的話那麼理解多態,甚至創建多態的例子幾乎是不可能的。多態的特性是不能孤立存在的(例如switch語法),多態只有在類關係的大背景下才能發揮出威力。人們經常把它同那些非面向對象的特徵混淆,例如重載,經常被作爲面向對象的特徵。不要上當:如果不是事後綁定的話就不是多態。

To use polymorphism—and thus object-oriented techniques—effectively in your programs, you must expand your view of programming to include not just members and messages of an individual class, but also the commonality among classes and their relationships with each other. Although this requires significant effort, it’s a worthy struggle, because the results are faster program development, better code organization, extensible programs, and easier code maintenance.

如果希望在程序中有效的使用了面向對象的多態機制,你必須擴展自己的視野,不僅僅侷限在類成員和消息上,要去理解類之間的共性以及彼此之間的關係。雖然這個要求很高,但是還是值得的,因爲它加快了開發進度,優化了代碼結構,增強了擴展性,使得維護更簡單。

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章