# 4.2 方法重載
在任何程序設計語言中,一項重要的特性就是名字的運用。我們創建一個對象時,會分配到一個保存區域的名字。方法名代表的是一種具體的行動。通過用名字描述自己的系統,可使自己的程序更易人們理解和修改。它非常象寫散文——目的是與讀者溝通。
我們用名字引用或描述所有對象與方法。若名字選得好,可使自己及其他人更易理解自己的代碼。
將人類語言中存在細致差別的概念“映射”到一種程序設計語言中時,會出現一些特殊的問題。在日常生活中,我們用相同的詞表達多種不同的含義——即詞的“重載”。我們說“洗襯衫”、“洗車”以及“洗狗”。但若強制象下面這樣說,就顯得很愚蠢:“襯衫洗 襯衫”、“車洗 車”以及“狗洗 狗”。這是由于聽眾根本不需要對執行的行動作任何明確的區分。人類的大多數語言都具有很強的“冗余”性,所以即使漏掉了幾個詞,仍然可以推斷出含義。我們不需要獨一無二的標識符——可從具體的語境中推論出含義。
大多數程序設計語言(特別是C)要求我們為每個函數都設定一個獨一無二的標識符。所以絕對不能用一個名為`print()`的函數來顯示整數,再用另一個`print()`顯示浮點數——每個函數都要求具備唯一的名字。
在Java里,另一項因素強迫方法名出現重載情況:構造器。由于構造器的名字由類名決定,所以只能有一個構造器名稱。但假若我們想用多種方式創建一個對象呢?例如,假設我們想創建一個類,令其用標準方式進行初始化,另外從文件里讀取信息來初始化。此時,我們需要兩個構造器,一個沒有參數(默認構造器),另一個將字符串作為參數——用于初始化對象的那個文件的名字。由于都是構造器,所以它們必須有相同的名字,亦即類名。所以為了讓相同的方法名伴隨不同的參數類型使用,“方法重載”是非常關鍵的一項措施。同時,盡管方法重載是構造器必需的,但它亦可應用于其他任何方法,且用法非常方便。
在下面這個例子里,我們向大家同時展示了重載構造器和重載的原始方法:
```
//: Overloading.java
// Demonstration of both constructor
// and ordinary method overloading.
import java.util.*;
class Tree {
int height;
Tree() {
prt("Planting a seedling");
height = 0;
}
Tree(int i) {
prt("Creating new Tree that is "
+ i + " feet tall");
height = i;
}
void info() {
prt("Tree is " + height
+ " feet tall");
}
void info(String s) {
prt(s + ": Tree is "
+ height + " feet tall");
}
static void prt(String s) {
System.out.println(s);
}
}
public class Overloading {
public static void main(String[] args) {
for(int i = 0; i < 5; i++) {
Tree t = new Tree(i);
t.info();
t.info("overloaded method");
}
// Overloaded constructor:
new Tree();
}
} ///:~
```
`Tree`既可創建成一顆種子,不含任何參數;亦可創建成生長在苗圃中的植物。為支持這種創建,共使用了兩個構造器,一個沒有參數(我們把沒有參數的構造器稱作“默認構造器”,注釋①),另一個采用現成的高度。
①:在Sun公司出版的一些Java資料中,用簡陋但很說明問題的詞語稱呼這類構造器——“無參數構造器”(no-arg constructors)。但“默認構造器”這個稱呼已使用了許多年,所以我選擇了它。
我們也有可能希望通過多種途徑調用`info()`方法。例如,假設我們有一條額外的消息想顯示出來,就使用`String`參數;而假設沒有其他話可說,就不使用。由于為顯然相同的概念賦予了兩個獨立的名字,所以看起來可能有些古怪。幸運的是,方法重載允許我們為兩者使用相同的名字。
## 4.2.1 區分重載方法
若方法有同樣的名字,Java怎樣知道我們指的哪一個方法呢?這里有一個簡單的規則:每個重載的方法都必須采取獨一無二的參數類型列表。
若稍微思考幾秒鐘,就會想到這樣一個問題:除根據參數的類型,程序員如何區分兩個同名方法的差異呢?
即使參數的順序也足夠我們區分兩個方法(盡管我們通常不愿意采用這種方法,因為它會產生難以維護的代碼):
```
//: OverloadingOrder.java
// Overloading based on the order of
// the arguments.
public class OverloadingOrder {
static void print(String s, int i) {
System.out.println(
"String: " + s +
", int: " + i);
}
static void print(int i, String s) {
System.out.println(
"int: " + i +
", String: " + s);
}
public static void main(String[] args) {
print("String first", 11);
print(99, "Int first");
}
} ///:~
```
兩個`print()`方法有完全一致的參數,但順序不同,可據此區分它們。
## 4.2.2 基本類型的重載
主(數據)類型能從一個“較小”的類型自動轉變成一個“較大”的類型。涉及重載問題時,這會稍微造成一些混亂。下面這個例子揭示了將基本類型傳遞給重載的方法時發生的情況:
```
//: PrimitiveOverloading.java
// Promotion of primitives and overloading
public class PrimitiveOverloading {
// boolean can't be automatically converted
static void prt(String s) {
System.out.println(s);
}
void f1(char x) { prt("f1(char)"); }
void f1(byte x) { prt("f1(byte)"); }
void f1(short x) { prt("f1(short)"); }
void f1(int x) { prt("f1(int)"); }
void f1(long x) { prt("f1(long)"); }
void f1(float x) { prt("f1(float)"); }
void f1(double x) { prt("f1(double)"); }
void f2(byte x) { prt("f2(byte)"); }
void f2(short x) { prt("f2(short)"); }
void f2(int x) { prt("f2(int)"); }
void f2(long x) { prt("f2(long)"); }
void f2(float x) { prt("f2(float)"); }
void f2(double x) { prt("f2(double)"); }
void f3(short x) { prt("f3(short)"); }
void f3(int x) { prt("f3(int)"); }
void f3(long x) { prt("f3(long)"); }
void f3(float x) { prt("f3(float)"); }
void f3(double x) { prt("f3(double)"); }
void f4(int x) { prt("f4(int)"); }
void f4(long x) { prt("f4(long)"); }
void f4(float x) { prt("f4(float)"); }
void f4(double x) { prt("f4(double)"); }
void f5(long x) { prt("f5(long)"); }
void f5(float x) { prt("f5(float)"); }
void f5(double x) { prt("f5(double)"); }
void f6(float x) { prt("f6(float)"); }
void f6(double x) { prt("f6(double)"); }
void f7(double x) { prt("f7(double)"); }
void testConstVal() {
prt("Testing with 5");
f1(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5);
}
void testChar() {
char x = 'x';
prt("char argument:");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
}
void testByte() {
byte x = 0;
prt("byte argument:");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
}
void testShort() {
short x = 0;
prt("short argument:");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
}
void testInt() {
int x = 0;
prt("int argument:");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
}
void testLong() {
long x = 0;
prt("long argument:");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
}
void testFloat() {
float x = 0;
prt("float argument:");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
}
void testDouble() {
double x = 0;
prt("double argument:");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
}
public static void main(String[] args) {
PrimitiveOverloading p =
new PrimitiveOverloading();
p.testConstVal();
p.testChar();
p.testByte();
p.testShort();
p.testInt();
p.testLong();
p.testFloat();
p.testDouble();
}
} ///:~
```
若觀察這個程序的輸出,就會發現常數值5被當作一個`int`值處理。所以假若可以使用一個重載的方法,就能獲取它使用的`int`值。在其他所有情況下,若我們的數據類型“小于”方法中使用的參數,就會對那種數據類型進行“轉型”處理。`char`獲得的效果稍有些不同,這是由于假期它沒有發現一個準確的`char`匹配,就會轉型為`int`。
若我們的參數“大于”重載方法期望的參數,這時又會出現什么情況呢?對前述程序的一個修改揭示出了答案:
```
//: Demotion.java
// Demotion of primitives and overloading
public class Demotion {
static void prt(String s) {
System.out.println(s);
}
void f1(char x) { prt("f1(char)"); }
void f1(byte x) { prt("f1(byte)"); }
void f1(short x) { prt("f1(short)"); }
void f1(int x) { prt("f1(int)"); }
void f1(long x) { prt("f1(long)"); }
void f1(float x) { prt("f1(float)"); }
void f1(double x) { prt("f1(double)"); }
void f2(char x) { prt("f2(char)"); }
void f2(byte x) { prt("f2(byte)"); }
void f2(short x) { prt("f2(short)"); }
void f2(int x) { prt("f2(int)"); }
void f2(long x) { prt("f2(long)"); }
void f2(float x) { prt("f2(float)"); }
void f3(char x) { prt("f3(char)"); }
void f3(byte x) { prt("f3(byte)"); }
void f3(short x) { prt("f3(short)"); }
void f3(int x) { prt("f3(int)"); }
void f3(long x) { prt("f3(long)"); }
void f4(char x) { prt("f4(char)"); }
void f4(byte x) { prt("f4(byte)"); }
void f4(short x) { prt("f4(short)"); }
void f4(int x) { prt("f4(int)"); }
void f5(char x) { prt("f5(char)"); }
void f5(byte x) { prt("f5(byte)"); }
void f5(short x) { prt("f5(short)"); }
void f6(char x) { prt("f6(char)"); }
void f6(byte x) { prt("f6(byte)"); }
void f7(char x) { prt("f7(char)"); }
void testDouble() {
double x = 0;
prt("double argument:");
f1(x);f2((float)x);f3((long)x);f4((int)x);
f5((short)x);f6((byte)x);f7((char)x);
}
public static void main(String[] args) {
Demotion p = new Demotion();
p.testDouble();
}
} ///:~
```
在這里,方法采用了容量更小、范圍更窄的基本類型值。若我們的參數范圍比它寬,就必須用括號中的類型名將其轉為適當的類型。如果不這樣做,編譯器會報告出錯。
大家可注意到這是一種“縮小轉換”。也就是說,在轉換或轉型過程中可能丟失一些信息。這正是編譯器強迫我們明確定義的原因——我們需明確表達想要轉型的愿望。
## 4.2.3 返回值重載
我們很易對下面這些問題感到迷惑:為什么只有類名和方法參數列出?為什么不根據返回值對方法加以區分?比如對下面這兩個方法來說,雖然它們有同樣的名字和參數,但其實是很容易區分的:
```
void f() {}
int f() {}
```
若編譯器可根據上下文(語境)明確判斷出含義,比如在`int x=f()`中,那么這樣做完全沒有問題。然而,我們也可能調用一個方法,同時忽略返回值;我們通常把這稱為“為它的副作用去調用一個方法”,因為我們關心的不是返回值,而是方法調用的其他效果。所以假如我們象下面這樣調用方法:
```
f();
```
Java怎樣判斷`f()`的具體調用方式呢?而且別人如何識別并理解代碼呢?由于存在這一類的問題,所以不能根據返回值類型來區分重載的方法。
## 4.2.4 默認構造器
正如早先指出的那樣,默認構造器是沒有參數的。它們的作用是創建一個“空對象”。若創建一個沒有構造器的類,則編譯程序會幫我們自動創建一個默認構造器。例如:
```
//: DefaultConstructor.java
class Bird {
int i;
}
public class DefaultConstructor {
public static void main(String[] args) {
Bird nc = new Bird(); // default!
}
} ///:~
```
對于下面這一行:
```
new Bird();
```
它的作用是新建一個對象,并調用默認構造器——即使尚未明確定義一個象這樣的構造器。若沒有它,就沒有方法可以調用,無法構建我們的對象。然而,如果已經定義了一個構造器(無論是否有參數),編譯程序都不會幫我們自動生成一個:
```
class Bush {
Bush(int i) {}
Bush(double d) {}
}
```
現在,假若使用下述代碼:
```
new Bush();
```
編譯程序就會報告自己找不到一個相符的構造器。就好象我們沒有設置任何構造器,編譯程序會說:“你看來似乎需要一個構造器,所以讓我們給你制造一個吧。”但假如我們寫了一個構造器,編譯程序就會說:“啊,你已寫了一個構造器,所以我知道你想干什么;如果你不放置一個默認的,是由于你打算省略它。”
## 4.2.5 `this`關鍵字
如果有兩個同類型的對象,分別叫作`a`和`b`,那么您也許不知道如何為這兩個對象同時調用一個`f()`方法:
```
class Banana { void f(int i) { /* ... */ } }
Banana a = new Banana(), b = new Banana();
a.f(1);
b.f(2);
```
若只有一個名叫`f()`的方法,它怎樣才能知道自己是為`a`還是為`b`調用的呢?
為了能用簡便的、面向對象的語法來書寫代碼——亦即“將消息發給對象”,編譯器為我們完成了一些幕后工作。其中的秘密就是第一個參數傳遞給方法`f()`,而且那個參數是準備操作的那個對象的引用。所以前述的兩個方法調用就變成了下面這樣的形式:
```
Banana.f(a,1);
Banana.f(b,2);
```
這是內部的表達形式,我們并不能這樣書寫表達式,并試圖讓編譯器接受它。但是,通過它可理解幕后到底發生了什么事情。
假定我們在一個方法的內部,并希望獲得當前對象的引用。由于那個引用是由編譯器“秘密”傳遞的,所以沒有標識符可用。然而,針對這一目的有個專用的關鍵字:`this`。`this`關鍵字(注意只能在方法內部使用)可為已調用了其方法的那個對象生成相應的引用。可象對待其他任何對象引用一樣對待這個引用。但要注意,假若準備從自己某個類的另一個方法內部調用一個類方法,就不必使用`this`。只需簡單地調用那個方法即可。當前的`this`引用會自動應用于其他方法。所以我們能使用下面這樣的代碼:
```
class Apricot {
void pick() { /* ... */ }
void pit() { pick(); /* ... */ }
}
```
在`pit()`內部,我們可以說`this.pick()`,但事實上無此必要。編譯器能幫我們自動完成。`this`關鍵字只能用于那些特殊的類——需明確使用當前對象的引用。例如,假若您希望將引用返回給當前對象,那么它經常在`return`語句中使用。
```
//: Leaf.java
// Simple use of the "this" keyword
public class Leaf {
private int i = 0;
Leaf increment() {
i++;
return this;
}
void print() {
System.out.println("i = " + i);
}
public static void main(String[] args) {
Leaf x = new Leaf();
x.increment().increment().increment().print();
}
} ///:~
```
由于`increment()`通過`this`關鍵字返回當前對象的引用,所以可以方便地對同一個對象執行多項操作。
(1) 在構造器里調用構造器
若為一個類寫了多個構造器,那么經常都需要在一個構造器里調用另一個構造器,以避免寫重復的代碼。可用`this`關鍵字做到這一點。
通常,當我們說`this`的時候,都是指“這個對象”或者“當前對象”。而且它本身會產生當前對象的一個引用。在一個構造器中,若為其賦予一個參數列表,那么`this`關鍵字會具有不同的含義:它會對與那個參數列表相符的構造器進行明確的調用。這樣一來,我們就可通過一條直接的途徑來調用其他構造器。如下所示:
```
//: Flower.java
// Calling constructors with "this"
public class Flower {
private int petalCount = 0;
private String s = new String("null");
Flower(int petals) {
petalCount = petals;
System.out.println(
"Constructor w/ int arg only, petalCount= "
+ petalCount);
}
Flower(String ss) {
System.out.println(
"Constructor w/ String arg only, s=" + ss);
s = ss;
}
Flower(String s, int petals) {
this(petals);
//! this(s); // Can't call two!
this.s = s; // Another use of "this"
System.out.println("String & int args");
}
Flower() {
this("hi", 47);
System.out.println(
"default constructor (no args)");
}
void print() {
//! this(11); // Not inside non-constructor!
System.out.println(
"petalCount = " + petalCount + " s = "+ s);
}
public static void main(String[] args) {
Flower x = new Flower();
x.print();
}
} ///:~
```
其中,構造器`Flower(String s,int petals)`向我們揭示出這樣一個問題:盡管可用`this`調用一個構造器,但不可調用兩個。除此以外,構造器調用必須是我們做的第一件事情,否則會收到編譯程序的報錯信息。
這個例子也向大家展示了`this`的另一項用途。由于參數`s`的名字以及成員數據`s`的名字是相同的,所以會出現混淆。為解決這個問題,可用`this.s`來引用成員數據。經常都會在Java代碼里看到這種形式的應用,本書的大量地方也采用了這種做法。
在`print()`中,我們發現編譯器不讓我們從除了一個構造器之外的其他任何方法內部調用一個構造器。
(2) `static`的含義
理解了`this`關鍵字后,我們可更完整地理解`static`(靜態)方法的含義。它意味著一個特定的方法沒有`this`。我們不可從一個`static`方法內部發出對非`static`方法的調用(注釋②),盡管反過來說是可以的。而且在沒有任何對象的前提下,我們可針對類本身發出對一個`static`方法的調用。事實上,那正是`static`方法最基本的意義。它就好象我們創建一個全局函數的等價物(在C語言中)。除了全局函數不允許在Java中使用以外,若將一個`static`方法置入一個類的內部,它就可以訪問其他`static`方法以及`static`字段。
②:有可能發出這類調用的一種情況是我們將一個對象引用傳到`static`方法內部。隨后,通過引用(此時實際是`this`),我們可調用非`static`方法,并訪問非`static`字段。但一般地,如果真的想要這樣做,只要制作一個普通的、非`static`方法即可。
有些人抱怨`static`方法并不是“面向對象”的,因為它們具有全局函數的某些特點;利用`static`方法,我們不必向對象發送一條消息,因為不存在`this`。這可能是一個清楚的參數,若您發現自己使用了大量靜態方法,就應重新思考自己的策略。然而,`static`的概念是非常實用的,許多時候都需要用到它。所以至于它們是否真的“面向對象”,應該留給理論家去討論。事實上,即使Smalltalk在自己的“類方法”里也有類似于`static`的東西。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物