第七章 復(fù)用類
1. 組合語法
1)對于非基本類型的對象,必須將其引用置于新的類中穷躁,但可以定義基本類型數(shù)據(jù)碳褒。
2)每一個非基本類型的對象都有一個toString()方法看疗,而且當(dāng)編譯器需要一個String而你卻只有一個對象時睦授,該方法便會被調(diào)用。
3)初始化一個類中的對象引用有如下四種方式:
- 1.在定義對象的地方怖辆。這意味著它們總能夠在構(gòu)造器調(diào)用之前被初始化删顶。
- 2.在類的構(gòu)造器中。
- 3.就在正要使用這些對象之前特咆,這種方式成為惰性初始化(Delayed initialization)。在生成對象不值得及不必每次都生成對象的情況下腻格,這種方式可以減少額外的負(fù)擔(dān)啥繁。
- 4.使用實例初始化。
以下是這四種方式的示例:
// Constructor initialization with composition.
import static net.mindview.util.Print.*;
class Soap {
private String s;
Soap() {
print("Soap()");
//2.在類的構(gòu)造器中初始化
s = "Constructed";
}
public String toString() { return s; }
}
public class Bath {
private String // 1.在定義對象的地方初始化:
s1 = "Happy",
s2 = "Happy",
s3, s4;
private Soap castille;
private int i;
private float toy;
public Bath() {
print("Inside Bath()");
//2.在類的構(gòu)造器中初始化:
s3 = "Joy";
toy = 3.14f;
castille = new Soap();
}
// 4.實例初始化:
{ i = 47; }
public String toString() {
if(s4 == null) // 3.惰性初始化(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.繼承語法
1)當(dāng)創(chuàng)建一個類時酬核,總是在繼承嫡意,因此亭病,除非已明確指出要從其他類中繼承嘶居,否則就是在隱式地從Java的標(biāo)準(zhǔn)根類Object進(jìn)行繼承。
2)繼承關(guān)鍵字extends
:繼承會自動得到基類中所有的域和方法整袁。
3)調(diào)用另外一個類的main函數(shù)的方式與調(diào)用另外一個類的普通靜態(tài)函數(shù)相同佑吝,即類名.main(args);
,args可以是主調(diào)用類從命令行獲得的參數(shù)芋忿,也可以是其他任意的String數(shù)組疾棵。
4)可以為每個類都創(chuàng)建一個main方法是尔。這種在每個類中都設(shè)置一個main方法的技術(shù)可使每個類的單元測試都變得簡單易行。而且在完成單元測試之后拟枚,也無需刪除main(),可以留待下次測試。
5)即使一個類只具有包訪問權(quán)限恩溅,其public main()仍然是可以訪問的脚乡。
6)為了繼承,一般的規(guī)則是將所有的數(shù)據(jù)成員都指定為private每窖,將所有的方法指定為public(protected members also allow access by derived classes)弦悉。
7)Java用==super==關(guān)鍵字表示超類(父類)。表達(dá)式super.fun();
可以調(diào)用父類中的函數(shù)(此處是調(diào)用函數(shù)fun())瀑志。
2.1 初始化基類
注意:基類=父類污秆;導(dǎo)出類=子類。
1)當(dāng)創(chuàng)建了一個導(dǎo)出類的對象時战得,該對象包含了一個基類的子對象庸推,該子對象被包裝在導(dǎo)出類對象內(nèi)部。
2)基類子對象的初始化:在構(gòu)造器中調(diào)用基類構(gòu)造器來執(zhí)行初始化聋亡。在執(zhí)行基類構(gòu)造器之前际乘,定義處初始化、實例初始化等均會被執(zhí)行罪塔。Java會自動在導(dǎo)出類的構(gòu)造器中插入對基類構(gòu)造器的調(diào)用。
示例代碼:
// Constructor calls during inheritance.
import static io.github.wzzju.util.Print.*;
class Art {
private String art = " test art.\n";
private String artS;
{
artS = " ART";
}
Art() { print("Art constructor"+art+artS); }
}
class Drawing extends Art {
private String draw = " test drawing.\n";
private String drawS;
{
drawS = " DRAW";
}
Drawing() { print("Drawing constructor"+draw+drawS); }
}
public class Cartoon extends Drawing {
public Cartoon() { print("Cartoon constructor"); }
public static void main(String[] args) {
Cartoon x = new Cartoon();
}
} /* Output:
Art constructor test art.
ART
Drawing constructor test drawing.
DRAW
Cartoon constructor
*///:~
可以看出墓拜,構(gòu)建過程是從基類“向外”擴散的请契,所以基類在導(dǎo)出類構(gòu)造器可以訪問它之前,就已經(jīng)完成初始化了涌韩。當(dāng)然臣樱,默認(rèn)構(gòu)造器也會逐層調(diào)度基類的構(gòu)造器腮考。
2.2 帶參數(shù)的構(gòu)造器
- 編譯器可以自動調(diào)用默認(rèn)的構(gòu)造函數(shù),是因為它們沒有任何參數(shù)踩蔚。但是如果沒有默認(rèn)的基類構(gòu)造函數(shù)馅闽,或者想調(diào)用一個帶參數(shù)的基類構(gòu)造函數(shù),必須使用關(guān)鍵字super顯示地編寫調(diào)用基類構(gòu)造函數(shù)的語句福也,并且配以適當(dāng)?shù)膮?shù)列表欲鹏。
- 如果基類沒有默認(rèn)構(gòu)造器(無參構(gòu)造器),導(dǎo)出類不顯式的調(diào)用基類的帶參構(gòu)造器现喳,則編譯器會報錯。
-
格外注意:
- 調(diào)用基類構(gòu)造器必須是在導(dǎo)出類構(gòu)造器中要做的第一件事拿穴,否則編譯器會報錯忧风。
- 而覆蓋父類的方法時,可以在做完子類要做的事情之后再調(diào)用父類對應(yīng)的方法腿宰。Java編程思想中在該種情況下都是放在最后才調(diào)用父類對應(yīng)的方法吃度。
3.代理
代理是第三種復(fù)用代碼的關(guān)系,Java并沒有提供對它的直接支持椿每。它是繼承和組合之間的中庸之道:
- 首先间护,我們需要將一個成員對象置于所要構(gòu)造的類中(就像組合);
- 其次法精,我們需要在新類中暴露該成員對象的所有方法(就像繼承)或該成員對象的所有方法的某個子集搂蜓。
示例代碼:
public class SpaceShipControls {
void up(int velocity) {}
void down(int velocity) {}
void left(int velocity) {}
void right(int velocity) {}
void forward(int velocity) {}
void back(int velocity) {}
void turboBoost() {}
}
public class SpaceShipDelegation {
private String name;
private SpaceShipControls controls =
new SpaceShipControls();
public SpaceShipDelegation(String name) {
this.name = name;
}
// Delegated methods:
public void back(int velocity) {
controls.back(velocity);
}
public void down(int velocity) {
controls.down(velocity);
}
public void forward(int velocity) {
controls.forward(velocity);
}
public void left(int velocity) {
controls.left(velocity);
}
public void right(int velocity) {
controls.right(velocity);
}
public void turboBoost() {
controls.turboBoost();
}
public void up(int velocity) {
controls.up(velocity);
}
public static void main(String[] args) {
SpaceShipDelegation protector =
new SpaceShipDelegation("NSEA Protector");
protector.forward(100);
}
}
使用代理的好處:使用代理時帮碰,我們可以擁有更多的控制力拾积,因為我們可以選擇只提供在成員對象中的方法的某個子集殷勘。
4.結(jié)合使用組合和繼承
同時使用組合和繼承,并配以必要的構(gòu)造器初始化输拇,可以創(chuàng)建更加復(fù)雜的類贤斜。
4.1.確保正確清理
try{
//......
}finally{
x.dispose();
}
上述代碼中的finally子句表示的是“無論發(fā)生什么事策吠,一定要為x調(diào)用dispose()〈袢蓿”
在清理方法(dispose())中猴抹,必須注意對基類清理方法和成員對象清理方法的調(diào)用順序,以防某個子對象依賴于另外一個子對象的情形發(fā)生锁荔。
- 一般蟀给,采用與C++編譯器在其析構(gòu)函數(shù)上所施加的形式:首先,執(zhí)行類的所有特定的清理工作,其順序同生成順序相反(通常這就要求基類元素仍舊存活)跋理;然后調(diào)用基類的清理方法择克。
- 注意:除了內(nèi)存以外,不能依賴?yán)厥掌魅プ鋈魏问虑捌铡H绻枰M(jìn)行清理肚邢,最好編寫自己的清理方法,但是不要使用finalize()拭卿。
4.2名稱屏蔽
- 如果Java的基類擁有某個已被多次重載的方法名稱骡湖,那么在導(dǎo)出類中重新定義該方法名稱并不會屏蔽器在基類中的任何一個版本(這一點與C++不同)峻厚。因此换途,無論是在該層或者它的基類中對方法進(jìn)行定義军拟,重載機制都可以正常工作。
- 如果你只是想覆寫某個方法辫继,但是害怕不留心重載了該方法(而并非覆寫了該方法)時,可以選擇添加
@Override
注解(Java SE5新增)。 - 在一個方法前添加了
@Override
注解酣溃,該方法便只能是覆寫父類的某個方法扛或,若是不留心寫成了重載悲伶,編譯器便會報錯蒂萎。這樣@Override
注解便可以防止你在不想重載時而意外地進(jìn)行了重載五慈。
5.在組合和繼承之間選擇
- 組合技術(shù)通常用于想在新類中使用現(xiàn)有類的功能而非它的接口這種形式。即腋粥,在新類中嵌入某個對象,讓其實現(xiàn)所需要的功能展辞,但是新類的用戶看到的只是為新類所定義的接口,而非所嵌入對象的接口。為取得此效果通殃,需要在新類中嵌入一個現(xiàn)有類的private對象。(特例曲聂,如Car將成員對象(Window/Wheel/Engine...)聲明為public齐疙,一般情況下應(yīng)該使域成為private贞奋。)
- 在繼承的時候勾缭,使用某個現(xiàn)有類俩由,并開發(fā)一個它的特殊版本。通常,這意味著你在使用一個通用類痘系,并為了某種特殊需要而將其特殊化。
- “is-a”(是一個)的關(guān)系是用繼承來表達(dá)的,而“has-a”(有一個)的關(guān)系則是用組合來表達(dá)的。
6.protected關(guān)鍵字
- protected關(guān)鍵字的作用:就類用戶而言呈宇,其是private的炬搭,但對于任何繼承于此類的子類或其他任何位于同一個包內(nèi)的類來說融虽,它卻是可以訪問的。(protected也提供了包內(nèi)訪問權(quán)限倒源。)
- 注意:盡管可以創(chuàng)建protected域腻菇,但是最好的方式還是將域保持為private秘遏;你應(yīng)當(dāng)一直保留“更改底層實現(xiàn)”的權(quán)利。然后通過protected方法來控制類的繼承者的訪問權(quán)限。
class A{
private String name;
protected void set(String name){
this.name = name;
}
public A(String nm){
name = nm;
}
//......
}
class B extends A{
private int number;
public B(String nm, int num){
super(nm);
number = num;
}
public void change(String nm, int num){
set(nm);//因為set()函數(shù)是protected的,故無論B和A是否在一個包內(nèi),在此都可以訪問。
number = num;
}
//......
}
7.向上轉(zhuǎn)型
<div align = "center">
類繼承圖
</div>
由導(dǎo)出類轉(zhuǎn)型成基類烂叔,在繼承涂上是向上移動的蒲讯,因此一般稱為向上轉(zhuǎn)型(upcasting)。由于向上轉(zhuǎn)型是從一個較專用類型向較通用類型轉(zhuǎn)換悦昵,所以總是很安全的。換句話說反粥,導(dǎo)出類是基類的一個超集。它可能比基類含有更多的方法郑气,但它必須至少具備基類中所含有的方法。在向上轉(zhuǎn)型的過程中奏属,類接口中唯一可能發(fā)生的事情是丟失方法忱嘹,而不是獲取它們愧驱。
再討論組合和繼承
相比于繼承,運用組合技術(shù)使用現(xiàn)有類來開發(fā)新的類的做法更常見乐横。應(yīng)該慎用繼承技術(shù)。
- 到底是該用組合還是用繼承今野,一個最清晰的判斷方法就是問一問自己是否需要從新類向基類進(jìn)行向上轉(zhuǎn)型葡公。如果必須向上轉(zhuǎn)型,則繼承是必要的条霜;但是如果不需要,則應(yīng)當(dāng)好好考慮自己是否需要繼承宰睡。
8.final關(guān)鍵字
使用final的含義是"這是不可改變的",不想做改變可能出于兩種理由:設(shè)計或效率旋圆。
final可用作數(shù)據(jù)裆悄、方法和類的修飾臂聋。
8.1 fianl 數(shù)據(jù)
- 恒定不變的數(shù)據(jù)有兩種:
1. 一個永不改變的編譯時常量光稼。- 例子:
private final int valueOne = 9;
private static final int VALUE_TWO = 99;
//Typical public constant
public static final int VALUE_THREE = 89;
2. 一個在運行時被初始化的值,而且不希望被改變孩等。
- 例子:
private static Random rand = new Random(47);
private final int i4 = rand.nextInt(20);
static final int INT_5 = rand.nextInt(20);
- 一個既是static又是final的域只占據(jù)一段不能改變的存儲空間冰垄。注意隅要,帶有恒定初始值(編譯時常量)的static final基本類型全用大寫字母命名要门,且字與字之間用下劃線隔開谴轮。
- 對于基本類型,final使數(shù)值不變;而用于對象引用受啥,final使引用恒定不變藤肢,即,一旦引用被初始化指向一個對象引用最住,就無法再把它指向另一個對象。然而轧粟,對象其自身卻是可以修改的(如對象內(nèi)部的某個字段的值是可以被改變的)脓魏。該規(guī)則同樣適用于數(shù)組,數(shù)組只不過是另一種引用混蔼。
8.1.1 空白final
空白final是指被聲明為final但又未給定初值的域。但空白final必須在構(gòu)造器中用表達(dá)式賦值。例子如下:
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);
}
}
總之奄侠,必須在域的定義外或每個構(gòu)造器中用表達(dá)式對fianl進(jìn)行賦值垄潮。
8.1.2 fianl參數(shù)
Java允許在參數(shù)列表中以聲明的方式將參數(shù)指明為final。這意味著你無法在方法中更改參數(shù)引用所指向的對象(但對象本身卻是可以改變的)或無法在方法中更改基本類型的值牡整。該特性注意用來向匿名內(nèi)部類傳遞數(shù)據(jù)藐吮。
void with(final Gizmo g) {
g.i++;//OK
//! 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; }
8.2 final方法
-
使用final方法的原因有兩個:
- 把方法鎖定,以防任何繼承類修改它的含義迫摔。這是出于設(shè)計的考慮:確保在繼承類中使方法行為保持不變,并且不會被覆蓋泥从。
- 效率:類似于C++的inline機制句占,早期的虛擬機需要,現(xiàn)在不需要了躯嫉,故現(xiàn)在不需要使用final方法進(jìn)行優(yōu)化了纱烘。
綜上,僅當(dāng)你想顯式地阻止覆蓋該方法時祈餐,才使該方法成為final的凹炸。
-
final和private關(guān)鍵字
- 類中所有的private方法都隱式地指定為是final的≈绲埽可以對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();
}
} /* Output:
OverridingPrivate2.f()
OverridingPrivate2.g()
*///:~
上例中舱痘,我們試圖覆蓋一個private方法(隱含是final的)变骡,似乎是奏效的,而且編譯器也不給出錯誤信息芭逝。但是上面的代碼并沒有進(jìn)行對基類方法的覆寫塌碌,僅是生成了一個新的方法,所以不可運用多態(tài)的性質(zhì)旬盯。
注意: "覆蓋"只有在某方法是基類的接口(public方法)的一部分時才會出現(xiàn)台妆。即,必須能將一個對象向上轉(zhuǎn)型為它的基類并可調(diào)用相同的方法胖翰。
8.3 final類
final置于類的定義之前表示該類不允許被繼承接剩。這樣做的原因如下:
- 出于某種考慮,你對該類的設(shè)計永不需要做任何變動萨咳;
- 出于安全的考慮懊缺,你不希望它有子類。
關(guān)于final類:
- final類的域可以根據(jù)個人意愿選擇為是或不是final培他。不論類是否被定義無final鹃两,相同的規(guī)則都適用于定義為final的域,如final類的非final域是可以被重新賦值的舀凛;
- 由于final類禁止繼承俊扳,所以final類中所有的方法都隱式指定為final的,因為本來就無法覆蓋它們猛遍。當(dāng)然馋记,在final類中可以給方法添加final修飾号坡,但這不會添加任何額外的意義。
現(xiàn)代Java的容器庫用ArrayList替代了Vector抗果,用HashMap代替了HashTable筋帖。
9.初始化及類的加載
- 每個類的編譯代碼都存在于它自己的獨立的文件中。該文件只有在需要使用程序代碼時才會被加載冤馏。
- 類代碼在初次使用時才加載日麸。即,類加載發(fā)生于創(chuàng)建類的第一個對象之時逮光,但是當(dāng)訪問static域或static方法時代箭,也會發(fā)生加載。
- 初次使用之處也是static初始化發(fā)生之處涕刚。所有的static對象和static代碼段都會在加載時依程序中的順序(即嗡综,定義類時的書寫順序)而依次初始化。當(dāng)然杜漠,定義為static的東西只會被初始化一次极景。
類的構(gòu)造器也是static方法(即使static關(guān)鍵字沒有顯式地寫出來)。因此更準(zhǔn)確地講驾茴,類是在其他任何static成員被訪問時加載的盼樟。
初始化及類的加載過程(點擊查看)
- 示例代碼如下:
import static io.github.wzzju.util.Print.*;
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();
}
} /* Output:
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
*///:~