1. 程式人生 > >Beginng_Java7(譯):發現類和物件(第二章3.4.5節)(完)

Beginng_Java7(譯):發現類和物件(第二章3.4.5節)(完)

多型

一些現實世界的實體可以改變他們的形式。 例如,水(在地球上而不是星際空間)自然是液體,但在冷凍時會變成固體,在加熱到沸點時會變成氣體。 諸如經歷變態的蝴蝶之類的昆蟲是另一個例子。

改變形式的能力被稱為多型,並且對於在程式語言中建模是有用的。 例如,通過引入單個Shape類及其draw()方法,並通過為每個Circle例項,Rectangle例項和儲存在陣列中的其他Shape例項呼叫該方法,可以更簡潔地表達繪製任意形狀的程式碼。 當為陣列例項呼叫Shape的draw()方法時,它是被呼叫的Circle,Rectangle或其他Shape例項的draw()方法。 我們說有很多形式的Shape的draw()方法,或者這個方法是多型的。 Java支援四種多型:

•強制:操作通過隱式型別轉換提供多種型別。 例如,除法允許您將整數除以另一個整數,或者將浮點值除以另一個浮點值。 如果一個運算元是一個整數而另一個運算元是浮點值,則編譯器強制(隱式轉換)整數到浮點值,以防止型別錯誤。 (沒有支援整數運算元和浮點運算元的除法運算。)將子類物件引用傳遞給方法的超類引數是強制多型的另一個例子。 編譯器將子類型別強制轉換為超類型別,以將操作限制為超類的操作。

•過載:可以在不同的上下文中使用相同的運算子符號或方法名稱。 例如,+可用於執行整數加法,浮點加法或字串連線,具體取決於其運算元的型別。 此外,具有相同名稱的多個方法可以出現在類中(通過宣告和/或繼承)。

•Parametric:在類宣告中,欄位名稱可以與不同型別關聯,方法名稱可以與不同的引數和返回型別關聯。 然後,欄位和方法可以在每個類例項中採用不同的型別。 例如,欄位可能是java.lang.Integer型別,並且方法可能在一個類例項中返回一個Integer引用,同一個欄位可能是String型別,同一個方法可能在另一個類例項中返回一個String引用。 Java通過泛型支援引數多型,我將在第3章中討論。

•子型別:型別可以作為另一種型別的子型別。 當子型別例項出現在超型別上下文中時,對子型別例項執行超型別操作會導致該操作的子型別版本執行。 例如,假設Circle是Point的子類,並且兩個類都包含draw()方法。 將Circle例項分配給Point型別的變數,然後通過此變數呼叫draw()方法,將導致呼叫Circle的draw()方法。 子型別多型性與實現繼承相關聯。

許多開發人員不認為強制和過載是有效的多型性。 他們認為強制和過載僅僅是型別轉換和語法糖(簡化語言的語法,使其“更甜”)。 相反,引數和子型別被認為是有效的多型性。

本節通過向上轉換和後期綁定向您介紹子型別多型性。 然後,我們繼續抽象類和抽象方法,向下轉換和執行時型別標識,以及協變返回型別。

上行和後期繫結

清單2-27的Point類將一個點表示為x-y對。 因為圓(在此示例中)是表示其中心的x-y對,並且具有表示其範圍的半徑,所以可以使用引入半徑欄位的Circle類來擴充套件Point。 檢視清單2-32。 清單2-32 一個擴充套件Point類的Circle類

class Circle extends Point
{
private int radius;

Circle(int x, int y, int radius)
{
super(x, y);
this.radius = radius;
}
int getRadius()
{
return radius;
}
}

清單2-32的Circle類將Circle描述為具有半徑的Point,這意味著您可以將Circle例項視為Point例項。 通過將Circle例項分配給Point變數來完成此任務,如下所示:

Circle c = new Circle(10, 20, 30);
Point p = c;

由於通過Point的介面訪問Circle例項是合法的,因此不需要使用轉換操作符將Circle轉換為Point。 畢竟,圓圈至少是一個點。 此賦值稱為向上轉換,因為您隱式地轉換了型別層次結構(從Circle子類到Point超類)。 它也是協方差的一個例子,因為具有更寬範圍值(Circle)的型別被轉換為具有更窄範圍的值(Point)的型別。

在向上轉換Circle to Point後,您無法呼叫Circle的getRadius()方法,因為此方法不是Point的介面的一部分。 在將其縮小為超類後丟失對子型別功能的訪問似乎沒用,但是對於實現子型別多型性是必要的。

除了將子類例項向上轉換為超類型別的變數之外,子型別多型還涉及在超類中宣告一個方法並在子類中重寫此方法。 例如,假設Point和Circle是圖形應用程式的一部分,您需要在每個類中引入draw()方法以分別繪製一個點和一個圓。 最後是清單2-33所示的類結構。

清單2-33 宣告圖形應用程式的Point和Circle類

class Point
{
private int x, y;
Point(int x, int y)
{
this.x = x;
this.y = y;
}
int getX()
{
return x;
}
int getY()
{
return y;
}
@Override
public String toString()

{
return "("+x+", "+y+")";
}
void draw()
{
System.out.println("Point drawn at "+toString ());
}
}
class Circle extends Point
{
private int radius;
Circle(int x, int y, int radius)
{
super(x, y);
this.radius = radius;
}
int getRadius()
{
return radius;
}
@Override
public String toString()
{
return ""+radius;
}
@Override
void draw()
{
System.out.println("Circle drawn at "+super.toString()+
" with radius "+toString());
}
}

清單2-33的draw()方法最終將繪製圖形形狀,但在圖形應用程式的早期測試階段,通過System.out.println()方法呼叫模擬它們的行為就足夠了。

現在你已經暫時完成了Point和Circle,你想在模擬版本的圖形應用程式中測試他們的draw()方法。 要實現此目標,請編寫清單2-34的Graphics類。

清單2-34 用於測試Point和Circle的draw()方法的Graphics類

class Graphics
{
public static void main(String[] args)
{
Point[] points = new Point[] { new Point(10, 20),
new Circle(10, 20, 30) };
for (int i = 0; i < points.length; i++)
points[i].draw();
}
}

程式碼清單2-34的main()方法首先聲明瞭一個Points陣列。 通過首先讓陣列的初始化器例項化Circle類,然後通過將此例項的引用分配給points陣列中的第二個元素來演示向上轉換。

繼續,main()使用for迴圈來呼叫每個Point元素的draw()方法。 因為第一次迭代呼叫Point的draw()方法,而第二次迭代呼叫Circle的draw()方法,所以您會觀察到以下輸出:

Point drawn at (10, 20)
Circle drawn at (10, 20) with radius 30

Java如何“知道”它必須呼叫Circle的 第二次迴圈迭代的draw()方法是不是應該呼叫Point的draw()方法,因為由於upcast,Circle被視為一個Point?

在編譯時,編譯器不知道要呼叫哪個方法。 它所能做的只是驗證超類中是否存在方法,並驗證方法呼叫的引數列表和返回型別是否與超類的方法宣告匹配。

代替知道呼叫哪個方法,編譯器在編譯程式碼中插入一條指令,在執行時,它獲取並使用點[1]中的任何引用來呼叫正確的draw()方法。這個任務稱為後期繫結。

後期繫結用於呼叫非最終例項方法。 對於所有其他方法呼叫,編譯器知道要呼叫哪個方法,並將指令插入到編譯程式碼中,該程式碼呼叫與變數型別相關聯的方法(而不是其值)。 此任務稱為早期繫結。

如果正在向上轉換的陣列是另一個數組的子型別,您也可以從一個數組轉發到另一個數組。 考慮清單2-35。 清單2-35 演示陣列上傳

class Point
{
private int x, y;
Point(int x, int y)
{
this.x = x;
this.y = y;
}
int getX() { return x; }
int getY() { return y; }
}
class ColoredPoint extends Point
{
private int color;
ColoredPoint(int x, int y, int color)
{
super(x, y);
this.color = color;
}
int getColor() { return color; }
}
class UpcastArrayDemo
{
public static void main(String[] args)
{
ColoredPoint[] cptArray = new ColoredPoint[1];

cptArray[0] = new ColoredPoint(10, 20, 5);
Point[] ptArray = cptArray;
System.out.println(ptArray[0].getX()); // Output: 10
System.out.println(ptArray[0].getY()); // Output: 20
// System.out.println(ptArray[0].getColor()); // Illegal
}
}

程式碼清單2-35的main()方法首先建立一個由一個元素組成的ColoredPoint陣列。 然後它例項化該類並將該物件的引用賦予該元素。 因為ColoredPoint []是Point []的子型別,main()能夠將cptArray的ColoredPoint []型別向上轉換為Point []並將其引用分配給ptArray。 然後main()通過ptArray [0]呼叫ColoredPoint例項的getX()和getY()方法。 它無法呼叫getColor(),因為ptArray的範圍比cptArray窄。 換句話說,getColor()不是Point介面的一部分。

抽象類和抽象方法

假設新要求規定您的圖形應用程式必須包含Rectangle類。 此外,此類必須包含draw()方法,並且必須以類似於清單2-34的Graphics類中所示的方式測試此方法。

與Circle(具有半徑的Point)相比,將Rectangle視為具有寬度和高度的Point是沒有意義的。 相反,Rectangle例項可能由指示其原點的Point和指示其寬度和高度範圍的Point組成。

因為圓,點和矩形是形狀的示例,所以使用自己的draw()方法宣告Shape類比指定類Rectangle extends Point更有意義。 清單2-36顯示了Shape的宣告。

清單2-36 宣告一個Shape類

class Shape
{
void draw()
{
}
}

清單2-36的Shape類聲明瞭一個空的draw()方法,該方法僅存在被覆蓋並演示子型別多型性。

您現在可以重構清單2-33的Point類來擴充套件清單2-36的Shape類,將Circle保持原樣,並引入一個擴充套件Shape的Rectangle類。 然後,您可以重構清單2-34的Graphics類的main()方法,以考慮Shape。 檢視以下main()方法:

public static void main(String[] args)
{
Shape[] shapes = new Shape[] { new Point(10, 20), new Circle(10, 20, 30),
new Rectangle(20, 30, 15, 25) };
for (int i = 0; i < shapes.length; i++)
shapes[i].draw();
}

因為Point和Rectangle直接擴充套件Shape,並且因為Circle通過擴充套件Point間接擴充套件Shape,所以main()響應shapes [i] .draw(); 通過呼叫正確的子類的draw()方法。

雖然Shape使程式碼更靈活,但是存在問題。 什麼是阻止某人例項化Shape並將這個毫無意義的例項新增到形狀陣列,如下所示?

Shape[] shapes = new Shape[] { new Point(10, 20), new Circle(10, 20, 30),
new Rectangle(20, 30, 15, 25), new Shape() };

例項化Shape意味著什麼因為這個類描述了一個抽象概念,繪製一個通用形狀意味著什麼? 幸運的是,Java為這個問題提供了一個解決方案,如程式碼清單2-37所示。

清單2-37 抽象Shape類

abstract class Shape
{
abstract void draw(); // semicolon is required
}

清單2-37使用Java的抽象保留字來宣告一個無法例項化的類。 如果您嘗試例項化此類,編譯器將報告錯誤。

■提示養成宣告描述通用類別(例如形狀,動物,載體和帳戶)摘要的類的習慣。 這樣,您就不會無意中例項化它們。

抽象保留字也用於宣告沒有正文的方法 - 編譯器在提供正文或省略分號時報告錯誤。 draw()方法不需要body,因為它無法繪製抽象形狀。

■注意當您嘗試宣告一個抽象和最終的類時,編譯器會報告錯誤。 例如,抽象的final類Shape是一個錯誤,因為無法例項化抽象類,並且無法擴充套件最終類。 當您將方法宣告為抽象但未宣告其類是抽象的時,編譯器也會報告錯誤。 例如,從清單2-37中的Shape類的標題中刪除abstract會導致錯誤。 這種刪除是一個錯誤,因為當一個非抽象(具體)類包含一個抽象方法時,它不能被例項化。 最後,當您擴充套件抽象類時,擴充套件類必須覆蓋所有抽象類的抽象方法,否則擴充套件類本身必須宣告為抽象類; 否則,編譯器將報告錯誤。

除抽象方法之外或代替抽象方法,抽象類可以包含非抽象方法。 例如,清單2-22的Vehicle類可以被宣告為abstract。 即使您無法例項化生成的類,建構函式仍將存在,以初始化私有欄位。

向下轉型和執行時型別識別

通過向上轉換向上移動型別層次結構會導致無法訪問子型別功能。 例如,將Circle例項分配給Point變數p意味著您不能使用p來呼叫Circle的getRadius()方法。

但是,可以通過執行顯式強制轉換操作再次訪問Circle例項的getRadius()方法; 例如,Circle c =(Circle)p;。 此分配稱為向下轉換,因為您明確地向下移動型別層次結構(從Point超類到Circle子類)。 它也是逆變的一個例子,因為具有較窄值範圍(Point)的型別被轉換為具有更寬範圍值(Circle)的型別。

儘管upcast始終是安全的(超類的介面是子類介面的子集),但同樣不能說是向下轉換。 清單2-38顯示了在錯誤地使用向下轉換時可以遇到的問題。

清單2-38 向下轉型的麻煩

class A
{
}
class B extends A
{
void m() {}
}
class DowncastDemo
{
public static void main(String[] args)
{
A a = new A();
B b = (B) a;
b.m();
}
}

清單2-38顯示了一個類層次結構,它由一個名為A的超類和一個名為B的子類組成。雖然A沒有宣告任何成員,但B聲明瞭一個m()方法。

名為DowncastDemo的第三個類提供了一個main()方法,該方法首先例項化A,然後嘗試將此例項向下轉換為B並將結果分配給變數b。 編譯器不會抱怨,因為從超類向同一型別層次結構中的子類的向下轉換是合法的。

但是,如果允許賦值,則在嘗試執行b.m();時,應用程式無疑會崩潰。 發生崩潰是因為JVM將嘗試呼叫不存在的方法 - 類A沒有m()方法。

幸運的是,這種情況永遠不會發生,因為JVM會驗證演員是否合法。 因為它檢測到A沒有m()方法,所以它不允許通過丟擲ClassCastException類的例項來進行強制轉換。

JVM的強制轉換驗證說明了執行時型別標識(或簡稱RTTI)。 強制轉換驗證通過檢查強制轉換運算子的運算元的型別來執行RTTI,以檢視是否應該允許強制轉換。 顯然,不應該允許演員表演。

第二種形式的RTTI涉及instanceof運算子。 此運算子檢查左運算元以檢視它是否是右運算元的例項,如果是這種情況則返回true。 以下示例將清單2-38的instanceof引入以防止ClassCastException:

if (a instanceof B)
{
B b = (B) a;
b.m();
}

instanceof運算子檢測到變數a的例項未從B建立,並返回false以指示此事實。 因此,執行非法轉換的程式碼將不會執行。 (過度使用instanceof可能表示軟體設計不佳。)

因為子型別是一種超型別,所以instanceof將在其左運算元是子型別例項或其右運算元超型別的超型別例項時返回true。 以下示例演示:

A a = new A();
B b = new B();
System.out.println(b instanceof A); // Output: true
System.out.println(a instanceof A); // Output: true

此示例假定清單2-38中顯示的類結構並例項化超類A和子類B.第一個System.out.println()方法呼叫輸出true,因為b的引用標識A的子類的例項; 第二個System.out.println()方法呼叫輸出true,因為一個引用標識了一個超類A的例項。

如果向下轉換的陣列是另一個數組的超型別,並且其元素型別是子型別,則也可以從一個數組轉發到另一個數組。 考慮清單2-39。

清單2-39 演示陣列向下轉換

class Point
{
private int x, y;
Point(int x, int y)
{
this.x = x;
this.y = y;
}
int getX() { return x; }
int getY() { return y; }
}
class ColoredPoint extends Point
{
private int color;
ColoredPoint(int x, int y, int color)
{
super(x, y);
this.color = color;
}
int getColor() { return color; }
}
class DowncastArrayDemo
{
public static void main(String[] args)
{
ColoredPoint[] cptArray = new ColoredPoint[1];
cptArray[0] = new ColoredPoint(10, 20, 5);
Point[] ptArray = cptArray;
System.out.println(ptArray[0].getX()); // Output: 10
System.out.println(ptArray[0].getY()); // Output: 20
// System.out.println(ptArray[0].getColor()); // Illegal
if (ptArray instanceof ColoredPoint[])
{
ColoredPoint cp = (ColoredPoint) ptArray[0];
System.out.println(cp.getColor());
}
}
}

清單2-39與清單2-35類似,不同之處在於它還演示了向下轉換。 注意它使用instanceof來驗證ptArray的引用物件是ColoredPoint []型別。 如果此運算子返回true,則可以安全地將ptArray [0]從Point轉換為ColoredPoint並將引用分配給ColoredPoint。

到目前為止,您遇到了兩種形式的RTTI。 Java還支援第三種形式,稱為反射。 當我在第4章討論反思時,我將向您介紹這種形式的RTTI。

協變返回型別

協變返回型別是一種方法返回型別,在超類的方法宣告中,它是子類重寫方法宣告中返回型別的超型別。 清單2-40演示了此功能。

清單2-40 協變返回型別的演示

class SuperReturnType
{
@Override
public String toString()
{
return "superclass return type";
}
}
class SubReturnType extends SuperReturnType
{
@Override
public String toString()
{
return "subclass return type";
}
}
class Superclass
{
SuperReturnType createReturnType()
{
return new SuperReturnType();
}
}
class Subclass extends Superclass
{

@Override
SubReturnType createReturnType()
{
return new SubReturnType();
}
}
class CovarDemo
{
public static void main(String[] args)
{
SuperReturnType suprt = new Superclass().createReturnType();
System.out.println(suprt); // Output: superclass return type
SubReturnType subrt = new Subclass().createReturnType();
System.out.println(subrt); // Output: subclass return type
}
}

清單2-40聲明瞭SuperReturnType和Superclass超類,以及SubReturnType和Subclass子類; Superclass和Subclass中的每一個都聲明瞭一個createReturnType()方法。 Superclass的方法將其返回型別設定為SuperReturnType,而Subclass的重寫方法將其返回型別設定為SubReturnType,它是SuperReturnType的子類。

協變返回型別最小化向上轉換和向下轉換。 例如,Subclass的createReturnType()方法不需要將其SubReturnType例項向上轉換為其SubReturnType返回型別。 此外,在分配給變數subrt時,不需要將此例項向下轉換為SubReturnType。

在沒有協變返回型別的情況下,最終會出現清單2-41。

清單2-41 在沒有協變返回型別的情況下向上轉發和向下轉換

class SuperReturnType
{
@Override
public String toString()
{
return "superclass return type";
}
}
class SubReturnType extends SuperReturnType
{
@Override
public String toString()
{
return "subclass return type";
}
}
class Superclass
{
SuperReturnType createReturnType()
{
return new SuperReturnType();
}
}

class Subclass extends Superclass
{
@Override
SuperReturnType createReturnType()
{
return new SubReturnType();
}
}
class CovarDemo
{
public static void main(String[] args)
{
SuperReturnType suprt = new Superclass().createReturnType();
System.out.println(suprt); // Output: superclass return type
SubReturnType subrt = (SubReturnType) new Subclass().createReturnType();
System.out.println(subrt); // Output: subclass return type
}
}


在清單2-41中,第一個粗體程式碼顯示從SubReturnType到SuperReturnType的向上轉換,第二個粗體程式碼在分配給subrt之前使用所需的(SubReturnType)強制轉換運算子從SuperReturnType向下轉換為SubReturnType。