面試題: 過載(Overload)和重寫(Override)的區別。過載的方法能否根據返回型別進行區分
面試官考察點猜想
這道題純粹只是考查基礎理論知識,對實際開發工作中沒有太多的指導意義,畢竟編輯器都有語法提示功能,如果沒寫正確,會有錯誤提示。
背景知識詳解
關於過載(Overload)和重寫(Override),在實際開發中使用非常頻繁,涉及到的背景知識並不難。
重寫
重寫是子類對父類的允許訪問的方法的實現過程進行重新編寫, 返回值和形參都不能改變。即外殼不變,核心重寫!
重寫是發生在類的繼承關係,或者類的實現關係中的,重寫後的方法和原方法需要保持完全相同的返回值型別、方法名、引數個數以及引數型別,簡單來說,就是子類重寫的方法必須和父類保持完全一致
類的繼承關係
我們來看下面這個基於繼承關係的例子。
class Animal{
public void move(){
System.out.println("動物可以移動");
}
}
class Bird extends Animal{
public void move(){
System.out.println("鳥可以飛");
}
}
class Dog extends Animal{
public void move(){
System.out.println("狗可以跑")
}
}
public class TestMain{
public static void main(String args[]){
Animal a = new Animal(); // Animal 物件
Animal b = new Bird(); //Bird物件
Animal c = new Dog(); // Dog 物件
a.move();// 執行 Animal 類的方法
b.move(); //執行Bird類的方法
c.move();//執行 Dog 類的方法
}
}
上述程式執行的結果
動物可以移動
鳥可以飛
狗可以跑
在這個案例中,Animal
是一個屬於動物的抽象類,它定義了一個方法move()
。表示動物的具有的行為。
而動物只是一個泛類別,具體到某種動物時,行為方式是不同的,因此定義了Bird
和Doc
,分別繼承了Animal
這個類,並且重寫了move()
方法,分別實現這兩種動物的行為方式。
重寫的好處在於子類可以根據需要,定義特定於自己的行為。 也就是說子類能夠根據需要實現父類的方法。
在類繼承關係中,父類的非抽象方法,子類是不強制要求重寫的。在實際應用中,如果重寫了父類的方法,並且例項物件的引用指向的是子類時,JVM會自動呼叫子類重寫的方法,此時,父類的方法完全被遮蔽了。就像前面測試的程式碼。
父類引用指向子類實現Dog()
,此時呼叫c.move()
方法,只會呼叫到Dog
類中的move()
方法。如果Dog
子類沒有重寫move()
方法,則會呼叫父類Animal
的move()
方法。
Animal c = new Dog(); // Dog 物件
c.move();//執行 Dog 類的方法
在有些情況下,子類重寫了父類的方法,我們希望在呼叫子類重寫方法的同時,仍然能夠呼叫到父類被重寫的方法,怎麼實現?
Super關鍵字
當需要在子類中呼叫父類的被重寫方法時,要使用 super 關鍵字。
class Animal{
public void move(){
System.out.println("動物可以移動");
}
}
class Bird extends Animal{
public void move(){
super.move(); //增加super呼叫
System.out.println("鳥可以飛");
}
}
}
public class TestMain{
public static void main(String args[]){
Animal b = new Bird(); //Bird物件
b.move();//執行 Bird 類的方法
}
}
執行結果如下:
動物可以移動
鳥可以飛
方法的重寫規則
總結一下,在Java中,方法重寫的規則。
- 引數列表與被重寫方法的引數列表必須完全相同。
- 返回型別與被重寫方法的返回型別可以不相同,但是必須是父類返回值的派生類(java5 及更早版本返回型別要一樣,java7 及更高版本可以不同)。
- 訪問許可權不能比父類中被重寫的方法的訪問許可權更低。例如:如果父類的一個方法被宣告為 public,那麼在子類中重寫該方法就不能宣告為 protected。
- 父類的成員方法只能被它的子類重寫。
- 宣告為 final 的方法不能被重寫。
- 宣告為 static 的方法不能被重寫,但是能夠被再次宣告。
- 子類和父類在同一個包中,那麼子類可以重寫父類所有方法,除了宣告為 private 和 final 的方法。
- 子類和父類不在同一個包中,那麼子類只能夠重寫父類的宣告為 public 和 protected 的非 final 方法。
- 重寫的方法能夠丟擲任何非強制異常,無論被重寫的方法是否丟擲異常。但是,重寫的方法不能丟擲新的強制性異常,或者比被重寫方法宣告的更廣泛的強制性異常,反之則可以。
- 構造方法不能被重寫。
- 如果不能繼承一個類,則不能重寫該類的方法。
基於介面實現的重寫
基於介面實現的重寫,在實際應用中,使用非常頻繁,以執行緒實現為例,如圖所示,表示Thread和Runnable的類關係圖。
Runnable是一個介面,它定義了執行緒的執行方法,程式碼如下:
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
在實際應用中,我們可以直接繼承這個介面來宣告一個執行緒。
Thread,是一個普通的執行緒類,它實現了Runnable介面,並且重寫了Runnable這個介面的run
方法,這裡這麼設計的目的是: 避免Java中一個類只能實現一個介面這一規則
導致,如果一個類已經繼承了其他的介面,但是又想要去實現執行緒時的問題。
public
class Thread implements Runnable {
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
由於介面只是用來做規範設計,用來描述某個物件具有什麼行為,但是它並沒有具體的實現,因此如果需要宣告一個執行緒,就需要實現該介面並且重寫裡面的抽象方法(介面中未實現的方法都是抽象的,子類必須要重寫)。
Thread類中重寫了Runnable中的run
方法,該方法呼叫了target.run()
。這個target
是真正的執行緒業務實現,Thread只是一個委派設計模式。
因此,如果我們想通過繼承Thread
來實現執行緒,則需要按照如下程式碼的寫法來實現,其中target
就是代表著子類的App
這個物件例項。
public class App extends Thread{
@Override
public void run() {
//doSomething
}
}
由於介面只是一種行為規範,本身不提供實現,因此實現介面的子類,都“必須”要重寫父類的方法,這個和類繼承是有區別的。
過載
過載(overloading) 是在一個類裡面,方法名字相同,而引數不同。返回型別可以相同也可以不同。
每個過載的方法(或者建構函式)都必須有一個獨一無二的引數型別列表。
最常用的地方就是構造器的過載,比如在ThreadPoolExecutor
執行緒池的實現類中,可看到如下的過載方法。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
方法過載的好處就是讓類以統一的方式處理不同型別的一種手段,呼叫方法時通過傳遞給他們的不同個數和型別的引數來決定具體使用哪個方法,這就是多型性。
它的特點是:過載發生在本類,方法名相同,引數列表不同,與返回值無關,只和方法名,引數的型別相關。
方法過載時,方法之間需要存在一定的聯絡,因為這樣可以提高程式的可讀性,並且我們一般只過載功能相似的方法。
過載規則
- 被過載的方法必須改變引數列表(引數個數或型別不一樣);
- 被過載的方法可以改變返回型別;
- 被過載的方法可以改變訪問修飾符;
- 被過載的方法可以宣告新的或更廣的檢查異常;
- 方法能夠在同一個類中或者在一個子類中被過載。
- 無法以返回值型別作為過載函式的區分標準。
問題解答
理解了上述知識點以後,再來看這道面試題。
面試題: 過載(Overload)和重寫(Override)的區別。過載的方法能否根據返回型別進行區分
區別:
- 方法過載是一個類中定義了多個方法名相同,而他們的引數的數量不同或數量相同而型別和次序不同,則稱為方法的過載(Overloading)。
- 方法重寫是在子類存在方法與父類的方法的名字相同,而且引數的個數與型別一樣,返回值也一樣的方法,就稱為重寫(Overriding)。
- 方法過載是一個類的多型性表現,而方法重寫是子類與父類的一種多型性表現。
過載方法是否能夠根據返回型別進行區分
過載方法無法根據型別來區分, 它只能通過引數型別、引數個數來區分,但是對於過載的方法,是允許修改返回值型別、異常型別、訪問等級,但是不能只根據這些型別類做過載。
為什們不能僅根據返回型別來區分過載呢?
原因是,在呼叫目標方法時,是無法指定返回值型別資訊的,這個時候編譯器並不知道你要呼叫哪個函式。
比如在下面這段程式碼中,當呼叫max(1,2);時無法確定呼叫的是哪個,單從這一點上來說,僅返回值型別不同的過載是不應該允許的。
float max(int a, int b);
int max(int a, int b);
可能有同學會問,如果讓編譯器能夠根據上下文語境來判斷呢?比如像下面這段程式碼。
float x=max(1,2);
int y=max(2,3);
在實際開發中,很多時候會存在這樣一種方法呼叫max(1,2)
,並不會去宣告返回值,由於這種情況的存在,所以這個理論也不能實現。
函式的返回值只是作為函式執行之後的一個“狀態”他是保持方法的呼叫者與被呼叫者進行通訊的關鍵。並不能作為某個方法的“標識”
問題總結
這個問題,其實是屬於那種,你不問我,我一定會認為自己知道,而且在工作開發中也能使用不會出問題,但是你一問我,我一定會懵逼,不是因為真的不懂,而是不知道怎麼去組織語言來描述這兩個概念。
建議大家參考“費曼學習法”,就是把這篇文章學到的理論,通過演講的方式表達出來,可以和同事,或者自己自問自答。
關注[跟著Mic學架構]公眾號,獲取更多精品原創