Java 圍繞“類”(Class)來解決問題。我們可以直接使用別人構建或調試過的代碼,而非創建新類、重新開始。
如何在不污染源代碼的前提下使用現存代碼是需要技巧的。在本章裏,你將學習到兩種方式來達到這個目的:
- 第一種方式直接了當。在新類中創建現有類的對象。這種方式叫做 “組合”(Composition),通過這種方式複用代碼的功能,而非其形式。
- 第二種方式更爲微妙。創建現有類類型的新類。照字面理解:採用現有類形式,又無需在編碼時改動其代碼,這種方式就叫做 “繼承”(Inheritance),編譯器會做大部分的工作。繼承是面向對象編程(OOP)的重要基礎之一。
Java不直接支持的第三種重用關係稱爲委託。這介於繼承和組合之間,因爲你將一個成員對象放在正在構建的類中(比如組合),但同時又在新類中公開來自成員對象的所有方法(比如繼承)
1、組合
僅需要把對象的引用(object references)放置在一個新的類裏,這就使用了組合。
編譯器不會爲每個引用創建一個默認對象,這是有意義的,因爲在許多情況下,這會導致不必要的開銷。初始化引用有四種方法:
- 當對象被定義時。這意味着它們總是在調用構造函數之前初始化。
- 在該類的構造函數中。
- 在實際使用對象之前。這通常稱爲延遲初始化。在對象創建開銷大且不需要每次都創建對象的情況下,它可以減少開銷。
- 使用實例初始化。
// reuse/Bath.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Constructor initialization with composition
class Soap {
private String s;
Soap() {
System.out.println("Soap()");
s = "Constructed";
}
@Override
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() {
System.out.println("Inside Bath()");
s3 = "Joy";
toy = 3.14f;
castille = new Soap();
}
// Instance initialization:實例初始化
{ i = 47; }
@Override
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();
System.out.println(b);
}
}
/* Output:
Inside Bath()
Soap()
s1 = Happy
s2 = Happy
s3 = Joy
s4 = Joy
i = 47
toy = 3.14
castille = Constructed
*/
如果你試圖對未初始化的引用調用方法,則未初始化的引用將產生運行時異常。
2、繼承
在創建類時總是要繼承,因爲除非顯式地繼承其他類,否則就隱式地繼承 Java 的標準根類對象(Object)。使用關鍵字 extends 後跟基類的名稱。當你這樣做時,你將自動獲得基類中的所有字段和方法。
即使程序中有很多類都有 main() 方法,惟一運行的只有在命令行上調用的 main()。
Java的 super 關鍵字引用了當前類繼承的“超類”(基類)
繼承時,你不受限於使用基類的方法。你還可以像向類添加任何方法一樣向派生類添加新方法:只需定義它。
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 {
public Cartoon() {
System.out.println("Cartoon constructor");
}
public static void main(String[] args) {
Cartoon x = new Cartoon();
}
}
/* Output:
Art constructor
Drawing constructor
Cartoon constructor
*/
2.2、帶參構造函數
如果沒有無參數的基類構造函數(當你編寫了無參構造函數時,編譯器不會自動爲你創建無參構造函數),或者必須調用具有參數的基類構造函數,則必須使用 super 關鍵字和適當的參數列表顯式地編寫對基類構造函數的調用
3、委託
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、結合組合與繼承
儘管編譯器強制你初始化基類,並要求你在構造函數的開頭就初始化基類,但它並不監視你以確保你初始化了成員對象。注意類是如何幹淨地分離的。你甚至不需要方法重用代碼的源代碼。你最多隻導入一個包。(這對於繼承和組合都是正確的。)
4.1、保持適當的清理
始化和清理章節提到,你無法知道垃圾收集器何時會被調用,甚至它是否會被調用。因此,如果你想爲類清理一些東西,必須顯式地編寫一個特殊的方法來完成它,並確保客戶端程序員知道他們必須調用這個方法。最重要的是——正如在"異常"章節中描述的——你必須通過在 *finally *子句中放置此類清理來防止異常。
class Shape {
Shape(int i) {
System.out.println("Shape constructor");
}
void dispose() {
System.out.println("Shape dispose");
}
}
class Circle extends Shape {
Circle(int i) {
super(i);
System.out.println("Drawing Circle");
}
@Override
void dispose() {
System.out.println("Erasing Circle");
super.dispose();
}
}
class Triangle extends Shape {
Triangle(int i) {
super(i);
System.out.println("Drawing Triangle");
}
@Override
void dispose() {
System.out.println("Erasing Triangle");
super.dispose();
}
}
class Line extends Shape {
private int start, end;
Line(int start, int end) {
super(start);
this.start = start;
this.end = end;
System.out.println(
"Drawing Line: " + start + ", " + end);
}
@Override
void dispose() {
System.out.println(
"Erasing Line: " + start + ", " + end);
super.dispose();
}
}
public class CADSystem extends Shape {
private Circle c;
private Triangle t;
private Line[] lines = new Line[3];
public CADSystem(int i) {
super(i + 1);
for(int j = 0; j < lines.length; j++)
lines[j] = new Line(j, j*j);
c = new Circle(1);
t = new Triangle(1);
System.out.println("Combined constructor");
}
@Override
public void dispose() {
System.out.println("CADSystem.dispose()");
// The order of cleanup is the reverse
// of the order of initialization:
t.dispose();
c.dispose();
for(int i = lines.length - 1; i >= 0; i--)
lines[i].dispose();
super.dispose();
}
public static void main(String[] args) {
CADSystem x = new CADSystem(47);
try {
// Code and exception handling...
} finally {
x.dispose();
}
}
}
/* Output:
Shape constructor
Shape constructor
Drawing Line: 0, 0
Shape constructor
Drawing Line: 1, 1
Shape constructor
Drawing Line: 2, 4
Shape constructor
Drawing Circle
Shape constructor
Drawing Triangle
Combined constructor
CADSystem.dispose()
Erasing Triangle
Shape dispose
Erasing Circle
Shape dispose
Erasing Line: 2, 4
Shape dispose
Erasing Line: 1, 1
Shape dispose
Erasing Line: 0, 0
Shape dispose
Shape dispose
*/
還必須注意基類和成員對象清理方法的調用順序,以防一個子對象依賴於另一個子對象。首先,按與創建的相反順序執行特定於類的所有清理工作。(一般來說,這要求基類元素仍然是可訪問的。) 然後調用基類清理方法。
除了內存回收外,你不能依賴垃圾收集來做任何事情。如果希望進行清理,可以使用自己的清理方法,不要使用 finalize()。
4.2、名稱隱藏
如果 Java 基類的方法名多次重載,則在派生類中重新定義該方法名不會隱藏任何基類版本。不管方法是在這個級別定義的,還是在基類中定義的,重載都會起作用。
5、組合與繼承的選擇
- 當你想在新類中包含一個已有類的功能時,使用組合,而非繼承。也就是說,在新類中嵌入一個對象(通常是私有的),以實現其功能。新類的使用者看到的是你所定義的新類的接口,而非嵌入對象的接口。
有時讓類的用戶直接訪問到新類中的組合成分是有意義的。只需將成員對象聲明爲 public 即可(可以把這當作“半委託”的一種)。
// reuse/Car.java
// Composition with public objects
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);
}
}
因爲在這個例子中 car 的組合也是問題分析的一部分(不是底層設計的部分),所以聲明成員爲 public 有助於客戶端程序員理解如何使用類,且降低了類創建者面臨的代碼複雜度。但是,記住這是一個特例。通常來說,屬性還是應該聲明爲 private。
- 當使用繼承時,使用一個現有類並開發出它的新版本。通常這意味着使用一個通用類,併爲了某個特殊需求將其特殊化。
“是一個”的關係是用繼承來表達的,而“有一個“的關係則用組合來表達。
6、protected
想把一個事物儘量對外界隱藏,而允許派生類的成員訪問。
關鍵字 protected 就起這個作用。它表示“就類的用戶而言,這是 private 的。但對於任何繼承它的子類或在同一包中的類,它是可訪問的。”(protected 也提供了包訪問權限)
儘管可以創建 protected 屬性,但是最好的方式是將屬性聲明爲 private 以一直保留更改底層實現的權利。