Java中繼續、多態、重載和重寫引見。本站提示廣大學習愛好者:(Java中繼續、多態、重載和重寫引見)文章只能為提供參考,不一定能成為您想要的結果。以下是Java中繼續、多態、重載和重寫引見正文
甚麼是多態?它的完成機制是甚麼呢?重載和重寫的差別在那邊?這就是這一次我們要回想的四個非常主要的概念:繼續、多態、重載和重寫。
繼續(inheritance)
簡略的說,繼續就是在一個現有類型的基本上,經由過程增長新的辦法或許重界說已無方法(上面會講到,這類方法叫重寫)的方法,發生一個新的類型。繼續是面向對象的三個根本特點--封裝、繼續、多態的個中之一,我們在應用JAVA時編寫的每個類都是在繼續,由於在JAVA說話中,java.lang.Object類是一切類最基本的基類(或許叫父類、超類),假如我們新界說的一個類沒有明白地指定繼續自哪一個基類,那末JAVA就會默許為它是繼續自Object類的。
我們可以把JAVA中的類分為以下三種:
類:應用class界說且不含有籠統辦法的類。
籠統類:應用abstract class界說的類,它可以含有,也能夠不含有籠統辦法。
接口:應用interface界說的類。
在這三品種型之間存鄙人面的繼續紀律:
類可以繼續(extends)類,可以繼續(extends)籠統類,可以繼續(implements)接口。
籠統類可以繼續(extends)類,可以繼續(extends)籠統類,可以繼續(implements)接口。
接口只能繼續(extends)接口。
請留意下面三條紀律中每種繼續情形下應用的分歧的症結字extends和implements,它們是弗成以隨便調換的。年夜家曉得,一個通俗類繼續一個接口後,必需完成這個接口中界說的一切辦法,不然就只能被界說為籠統類。我在這裡之所以沒有對implements症結字應用“完成”這類說法是由於從概念下去說它也是表現一種繼續關系,並且關於籠統類implements接口的情形下,它其實不是必定要完成這個接口界說的任何辦法,是以應用繼續的說法更加公道一些。
以上三條紀律同時遵照上面這些束縛:
類和籠統類都只能最多繼續一個類,或許最多繼續一個籠統類,而且這兩種情形是互斥的,也就是說它們要末繼續一個類,要末繼續一個籠統類。
類、籠統類和接口在繼續接口時,不受數目的束縛,實際上可以繼續無窮多個接口。固然,關於類來講,它必需完成它所繼續的一切接口中界說的全體辦法。
籠統類繼續籠統類,或許完成接口時,可以部門、全體或許完整不完成父類籠統類的籠統(abstract)辦法,或許父類接口中界說的接口。
類繼續籠統類,或許完成接口時,必需全體完成父類籠統類的全體籠統(abstract)辦法,或許父類接口中界說的全體接口。
繼續給我們的編程帶來的利益就是對原有類的復用(重用)。就像模塊的復用一樣,類的復用可以進步我們的開辟效力,現實上,模塊的復用是年夜量類的復用疊加後的後果。除繼續以外,我們還可使用組合的方法來復用類。所謂組合就是把原有類界說為新類的一個屬性,經由過程在新類中挪用原有類的辦法來完成復用。假如新界說的類型與原有類型之間不存在被包括的關系,也就是說,從籠統概念下去講,新界說類型所代表的事物其實不是原有類型所代表事物的一種,好比黃種人是人類的一種,它們之間存在包括與被包括的關系,那末這時候組合就是完成復用更好的選擇。上面這個例子就是組合方法的一個簡略示例:
public class Sub { private Parent p = new Parent(); public void doSomething() { // 復用Parent類的辦法 p.method(); // other code } } class Parent { public void method() { // do something here } }
固然,為了使代碼加倍有用,我們也能夠在須要應用到原有類型(好比Parent p)時,才對它停止初始化。
應用繼續和組合復用原本的類,都是一種增量式的開辟形式,這類方法帶來的利益是不須要修正原本的代碼,是以不會給原有代碼帶來新的BUG,也不消由於對原有代碼的修正而從新停止測試,這對我們的開辟明顯是無益的。是以,假如我們是在保護或許改革一個原本的體系或模塊,特別是對它們的懂得不是很透辟的時刻,便可以選擇增量開辟的形式,這不只可以年夜年夜進步我們的開辟效力,也能夠躲避因為對原有代碼的修正而帶來的風險。
多態(Polymorphism)
多態是又一個主要的根本概念,下面說到了,它是面向對象的三個根本特點之一。畢竟甚麼是多態呢?我們先看看上面的例子,來贊助懂得:
//汽車接口 interface Car { // 汽車稱號 String getName(); // 取得汽車售價 int getPrice(); } // 寶馬 class BMW implements Car { public String getName() { return "BMW"; } public int getPrice() { return 300000; } } // 奇瑞QQ class CheryQQ implements Car { public String getName() { return "CheryQQ"; } public int getPrice() { return 20000; } } // 汽車出售店 public class CarShop { // 售車支出 private int money = 0; // 賣出一部車 public void sellCar(Car car) { System.out.println("車型:" + car.getName() + " 單價:" + car.getPrice()); // 增長賣出車售價的支出 money += car.getPrice(); } // 售車總支出 public int getMoney() { return money; } public static void main(String[] args) { CarShop aShop = new CarShop(); // 賣出一輛寶馬 aShop.sellCar(new BMW()); // 賣出一輛奇瑞QQ aShop.sellCar(new CheryQQ()); System.out.println("總支出:" + aShop.getMoney()); } }
運轉成果:
車型:BMW 單價:300000
車型:CheryQQ 單價:20000
總支出:320000
繼續是多態得以完成的基本。從字面上懂得,多態就是一品種型(都是Car類型)表示出多種狀況(寶馬汽車的稱號是BMW,售價是300000;奇瑞汽車的稱號是CheryQQ,售價是2000)。將一個辦法挪用同這個辦法所屬的主體(也就是對象或類)聯系關系起來叫做綁定,分後期綁定和前期綁定兩種。上面說明一下它們的界說:
後期綁定:在法式運轉之進步行綁定,由編譯器和銜接法式完成,又叫做靜態綁定。好比static辦法和final辦法,留意,這裡也包含private辦法,由於它是隱式final的。
前期綁定:在運轉時依據對象的類型停止綁定,由辦法挪用機制完成,是以又叫做靜態綁定,或許運轉時綁定。除後期綁定外的一切辦法都屬於前期綁定。
多態就是在前期綁定這類機制上完成的。多態給我們帶來的利益是清除了類之間的耦合關系,使法式更輕易擴大。好比在上例中,新增長一品種型汽車的發賣,只須要讓新界說的類繼續Car類並完成它的一切辦法,而無需對原有代碼做任何修正,CarShop類的sellCar(Car car)辦法便可以處置新的車型了。新增代碼以下:
// 桑塔納汽車 class Santana implements Car { public String getName() { return "Santana"; } public int getPrice() { return 80000; } }
重載(overloading)和重寫(overriding)
重載和重寫都是針對辦法的概念,在弄清晰這兩個概念之前,我們先來懂得一下甚麼叫辦法的型構(英文名是signature,有的譯作“簽名”,固然它被應用的較為普遍,然則這個翻譯禁絕確的)。型構就是指辦法的構成構造,詳細包含辦法的稱號和參數,涵蓋參數的數目、類型和湧現的次序,然則不包含辦法的前往值類型,拜訪權限潤飾符,和abstract、static、final等潤飾符。好比上面兩個就是具有雷同型構的辦法:
public void method(int i, String s) { // do something } public String method(int i, String s) { // do something }
而這兩個就是具有分歧型構的辦法:
public void method(int i, String s) { // do something } public void method(String s, int i) { // do something }
懂得完型構的概念後我們再來看重視載和重寫,請看它們的界說:
重寫,英文名是overriding,是指在繼續情形下,子類中界說了與其基類中辦法具有雷同型構的新辦法,就叫做子類把基類的辦法重寫了。這是完成多態必需的步調。
重載,英文名是overloading,是指在統一個類中界說了一個以上具有雷同稱號,然則型構分歧的辦法。在統一個類中,是不許可界說多於一個的具有雷同型構的辦法的。
我們來斟酌一個風趣的成績:結構器可以被重載嗎?謎底固然是可以的,我們在現實的編程中也常常這麼做。現實上結構器也是一個辦法,結構器名就是辦法名,結構器參數就是辦法參數,而它的前往值就是新創立的類的實例。然則結構器卻弗成以被子類重寫,由於子類沒法界說與基類具有雷同型構的結構器。
重載、籠罩、多態與函數隱蔽
常常看到C++的一些初學者關於重載、籠罩、多態與函數隱蔽的隱約懂得。在這裡寫一點本身的看法,願望可以或許C++初學者解惑。
要弄清晰重載、籠罩、多態與函數隱蔽之間的龐雜且奧妙關系之前,我們起首要往返顧一下重載籠罩等根本概念。
起首,我們來看一個異常簡略的例子,懂得一下甚麼叫函數隱蔽hide。
#include <iostream> using namespace std; class Base{ public: void fun() { cout << "Base::fun()" << endl; } }; class Derive : public Base{ public: void fun(int i) { cout << "Derive::fun()" << endl; } }; int main() { Derive d; //上面一句毛病,故屏障失落 //d.fun();error C2660: 'fun' : function does not take 0 parameters d.fun(1); Derive *pd =new Derive(); //上面一句毛病,故屏障失落 //pd->fun();error C2660: 'fun' : function does not take 0 parameters pd->fun(1); delete pd; return 0; }
/*在分歧的橫死名空間感化域裡的函數不組成重載,子類和父類是分歧的兩個感化域。
在本例中,兩個函數在分歧感化域中,故不敷成重載,除非這個感化域是定名空間感化域。*/
在這個例子中,函數不是重載overload,也不是籠罩override,而是隱蔽hide。
接上去的5個例子詳細解釋一下甚麼叫隱蔽
例1
#include <iostream> using namespace std; class Basic{ public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base::fun(int i)" << endl;}//overload }; class Derive :public Basic{ public: void fun2(){cout << "Derive::fun2()" << endl;} }; int main() { Derive d; d.fun();//准確,派生類沒有與基類同名函數聲明,則基類中的一切同名重載函數都邑作為候選函數。 d.fun(1);//准確,派生類沒有與基類同名函數聲明,則基類中的一切同名重載函數都邑作為候選函數。 return 0; }
例2
#include <iostream> using namespace std; class Basic{ public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base::fun(int i)" << endl;}//overload }; class Derive :public Basic{ public: //新的函數版本,基類一切的重載版本都被屏障,在這裡,我們稱之為函數隱蔽hide //派生類中有基類的同名函數的聲明,則基類中的同名函數不會作為候選函數,即便基類有分歧的參數表的多個版本的重載函數。 void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;} void fun2(){cout << "Derive::fun2()" << endl;} }; int main() { Derive d; d.fun(1,2); //上面一句毛病,故屏障失落 //d.fun();error C2660: 'fun' : function does not take 0 parameters return 0; }
例3
#include <iostream> using namespace std; class Basic{ public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base::fun(int i)" << endl;}//overload }; class Derive :public Basic{ public: //籠罩override基類的個中一個函數版本,異樣基類一切的重載版本都被隱蔽hide //派生類中有基類的同名函數的聲明,則基類中的同名函數不會作為候選函數,即便基類有分歧的參數表的多個版本的重載函數。 void fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;} }; int main() { Derive d; d.fun(); //上面一句毛病,故屏障失落 //d.fun(1);error C2660: 'fun' : function does not take 1 parameters return 0; }
例4
#include <iostream> using namespace std; class Basic{ public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base::fun(int i)" << endl;}//overload }; class Derive :public Basic{ public: using Basic::fun; void fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;} }; int main() { Derive d; d.fun();//准確 d.fun(1);//准確 return 0; } /* 輸入成果 Derive::fun() Base::fun(int i) Press any key to continue */
例5
#include <iostream> using namespace std; class Basic{ public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base::fun(int i)" << endl;}//overload }; class Derive :public Basic{ public: using Basic::fun; void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;} void fun2(){cout << "Derive::fun2()" << endl;} }; int main() { Derive d; d.fun();//准確 d.fun(1);//准確 d.fun(1,2);//准確 return 0; } /* 輸入成果 Base::fun() Base::fun(int i) Derive::fun(int i,int j) Press any key to continue */
好了,我們先來一個小小的總結重載與籠罩二者之間的特點
重載overload的特點:
n 雷同的規模(在統一個類中);
n 函數名雷同參數分歧;
n virtual 症結字無關緊要。
籠罩override是指派生類函數籠罩基類函數,籠罩的特點是:
n 分歧的規模(分離位於派生類與基類);
n 函數名和參數都雷同;
n 基類函數必需有virtual 症結字。(若沒有virtual 症結字則稱之為隱蔽hide)
假如基類有某個函數的多個重載(overload)版本,而你在派生類中重寫(override)了基類中的一個或多個函數版本,或是在派生類中從新添加了新的函數版本(函數名雷同,參數分歧),則一切基類的重載版本都被屏障,在這裡我們稱之為隱蔽hide。所以,在普通情形下,你想在派生類中應用新的函數版本又想應用基類的函數版本時,你應當在派生類中重寫基類中的一切重載版本。你若是不想重寫基類的重載的函數版本,則你應當應用例4或例5方法,顯式聲明基類名字空間感化域。
現實上,C++編譯器以為,雷同函數名分歧參數的函數之間基本沒有甚麼關系,它們基本就是兩個絕不相干的函數。只是C++說話為了模仿實際世界,為了讓法式員更直不雅的思想處置實際世界中的成績,才引入了重載和籠罩的概念。重載是在雷同名字空間感化域下,而籠罩則是在分歧的名字空間感化域下,好比基類和派生類即為兩個分歧的名字空間感化域。在繼續進程中,若產生派生類與基類函數同名成績時,便會產生基類函數的隱蔽。固然,這裡評論辯論的情形是基類函數後面沒有virtual 症結字。在有virtual 症結字症結字時的情況我們另做評論辯論。
繼續類重寫了基類的某一函數版本,以發生本身功效的接口。此時C++編繹器以為,你如今既然要應用派生類的本身從新改寫的接口,那我基類的接口就不供給給你了(固然你可以用顯式聲明名字空間感化域的辦法,見[C++基本]重載、籠罩、多態與函數隱蔽(1))。而不會理睬你基類的接口是有重載特征的。若是你要在派生類裡持續堅持重載的特征,那你就本身再給出接口重載的特征吧。所以在派生類裡,只需函數名一樣,基類的函數版本就會被無情地屏障。在編繹器中,屏障是經由過程名字空間感化域完成的。
所以,在派生類中要堅持基類的函數重載版本,就應當重寫一切基類的重載版本。重載只在以後類中有用,繼續會掉去函數重載的特征。也就是說,要把基類的重載函數放在繼續的派生類裡,就必需重寫。
這裡“隱蔽”是指派生類的函數屏障了與其同名的基類函數,詳細規矩我們也來做一小結:
n 假如派生類的函數與基類的函數同名,然則參數分歧。此時,若基類無virtual症結字,基類的函數將被隱蔽。(留意別與重載混雜,固然函數名雷同參數分歧應稱之為重載,但這裡不克不及懂得為重載,由於派生類和基類不在統一名字空間感化域內。這裡懂得為隱蔽)
n 假如派生類的函數與基類的函數同名,然則參數分歧。此時,若基類有virtual症結字,基類的函數將被隱式繼續到派生類的vtable中。此時派生類vtable中的函數指向基類版本的函數地址。同時這個新的函數版本添加到派生類中,作為派生類的重載版本。但在基類指針完成多態挪用函數辦法時,這個新的派生類函數版本將會被隱蔽。
n 假如派生類的函數與基類的函數同名,而且參數也雷同,然則基類函數沒有virtual症結字。此時,基類的函數被隱蔽。(留意別與籠罩混雜,這裡懂得為隱蔽)。
n 假如派生類的函數與基類的函數同名,而且參數也雷同,然則基類函數有virtual症結字。此時,基類的函數不會被“隱蔽”。(在這裡,你要懂得為籠罩哦^_^)。
插曲:基類函數前沒有virtual症結字時,我們要重寫更加順口些,在有virtual症結字時,我們叫籠罩更加公道些,戒此,我也願望年夜家可以或許更好的懂得C++中一些奧妙的器械。費話少說,我們舉例解釋吧。
例6
#include <iostream> using namespace std; class Base{ public: virtual void fun() { cout << "Base::fun()" << endl; }//overload virtual void fun(int i) { cout << "Base::fun(int i)" << endl; }//overload }; class Derive : public Base{ public: void fun() { cout << "Derive::fun()" << endl; }//override void fun(int i) { cout << "Derive::fun(int i)" << endl; }//override void fun(int i,int j){ cout<< "Derive::fun(int i,int j)" <<endl;}//overload }; int main() { Base *pb = new Derive(); pb->fun(); pb->fun(1); //上面一句毛病,故屏障失落 //pb->fun(1,2);virtual函數不克不及停止overload,error C2661: 'fun' : no overloaded function takes 2 parameters cout << endl; Derive *pd = new Derive(); pd->fun(); pd->fun(1); pd->fun(1,2);//overload delete pb; delete pd; return 0; } /*
輸入成果
Derive::fun()
Derive::fun(int i)
Derive::fun()
Derive::fun(int i)
Derive::fun(int i,int j)
Press any key to continue
*/
例7-1
#include <iostream> using namespace std; class Base{ public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; } }; class Derive : public Base{}; int main() { Base *pb = new Derive(); pb->fun(1);//Base::fun(int i) delete pb; return 0; }
例7-2
#include <iostream> using namespace std; class Base{ public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; } }; class Derive : public Base{ public: void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } }; int main() { Base *pb = new Derive(); pb->fun(1);//Base::fun(int i) pb->fun((double)0.01);//Base::fun(int i) delete pb; return 0; }
例8-1
#include <iostream> using namespace std; class Base{ public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; } }; class Derive : public Base{ public: void fun(int i){ cout <<"Derive::fun(int i)"<< endl; } }; int main() { Base *pb = new Derive(); pb->fun(1);//Derive::fun(int i) delete pb; return 0; }
例8-2
#include <iostream> using namespace std; class Base{ public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; } }; class Derive : public Base{ public: void fun(int i){ cout <<"Derive::fun(int i)"<< endl; } void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } }; int main() { Base *pb = new Derive(); pb->fun(1);//Derive::fun(int i) pb->fun((double)0.01);//Derive::fun(int i) delete pb; return 0; }
例9
#include <iostream> using namespace std; class Base{ public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; } }; class Derive : public Base{ public: void fun(int i){ cout <<"Derive::fun(int i)"<< endl; } void fun(char c){ cout <<"Derive::fun(char c)"<< endl; } void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } }; int main() { Base *pb = new Derive(); pb->fun(1);//Derive::fun(int i) pb->fun('a');//Derive::fun(int i) pb->fun((double)0.01);//Derive::fun(int i) Derive *pd =new Derive(); pd->fun(1);//Derive::fun(int i) //overload pd->fun('a');//Derive::fun(char c) //overload pd->fun(0.01);//Derive::fun(double d) delete pb; delete pd; return 0; }
例7-1和例8-1很好懂得,我把這兩個例子放在這裡,是讓年夜家作一個比擬擺了,也是為了贊助年夜家更好的懂得:
n 例7-1中,派生類沒有籠罩基類的虛函數,此時派生類的vtable中的函數指針指向的地址就是基類的虛函數地址。
n 例8-1中,派生類籠罩了基類的虛函數,此時派生類的vtable中的函數指針指向的地址就是派生類本身的重寫的虛函數地址。
在例7-2和8-2看起來有點怪怪,其實,你依照下面的准繩比較一下,謎底也是晴明的:
n 例7-2中,我們為派生類重載了一個函數版本:void fun(double d) 其實,這只是一個障眼法。我們詳細來剖析一下,基類共有幾個函數,派生類共有幾個函數:
類型
基類
派生類
Vtable部門
void fun(int i)
指向基類版的虛函數void fun(int i)
靜態部門
void fun(double d)
我們再來剖析一下以下三句代碼
Base *pb = new Derive();
pb->fun(1);//Base::fun(int i)
pb->fun((double)0.01);//Base::fun(int i)
這第一句是症結,基類指針指向派生類的對象,我們曉得這是多態挪用;接上去第二句,運轉時基類指針依據運轉時對象的類型,發明是派生類對象,所以起首到派生類的vtable中去查找派生類的虛函數版本,發明派生類沒有籠罩基類的虛函數,派生類的vtable只是作了一個指向基類虛函數地址的一個指向,所以天經地義地去挪用基類版本的虛函數。最初一句,法式運轉依然專一去找派生類的vtable,發明基本沒有這個版本的虛函數,只好回頭挪用本身的唯一一個虛函數。
這裡還值得一提的是:假如此時基類有多個虛函數,此時法式編繹時會提醒”挪用不明白”。示例以下
#include <iostream> using namespace std; class Base{ public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; } virtual void fun(char c){ cout <<"Base::fun(char c)"<< endl; } }; class Derive : public Base{ public: void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } }; int main() { Base *pb = new Derive(); pb->fun(0.01);//error C2668: 'fun' : ambiguous call to overloaded function delete pb; return 0; }
好了,我們再來剖析一下例8-2。
n 例8-2中,我們也為派生類重載了一個函數版本:void fun(double d) ,同時籠罩了基類的虛函數,我們再來詳細來剖析一下,基類共有幾個函數,派生類共有幾個函數:
類型
基類
派生類
Vtable部門
void fun(int i)
void fun(int i)
靜態部門
void fun(double d)
從表中我們可以看到,派生類的vtable中函數指針指向的是本身的重寫的虛函數地址。
我們再來剖析一下以下三句代碼
Base *pb = new Derive();
pb->fun(1);//Derive::fun(int i)
pb->fun((double)0.01);//Derive::fun(int i)
第一句不用多說了,第二句,天經地義挪用派生類的虛函數版本,第三句,嘿,感到又怪怪的,其實呀,C++法式很笨的了,在運轉時,專一闖進派生類的vtable表中,只眼一看,靠,競然沒有想要的版本,真是想欠亨,基類指針為何不四周轉轉再找找呢?呵呵,本來是眼光無限,基類年事這麼老了,想必確定是老花了,它那雙眼睛看獲得的僅是本身的非Vtable部門(即靜態部門)和本身要治理的Vtable部門,派生類的void fun(double d)那末遠,看不到呀!再說了,派生類甚麼都要管,豈非派生類沒有本身的一點權利嗎?哎,不吵了,各自管本身的吧^_^
唉!你是否是要歎息了,基類指針能停止多態挪用,然則一直不克不及停止派生類的重載挪用啊(參考例6)~~~
再來看看例9,
本例的後果同例6,異曲同工。想必你懂得了下面的這些例子後,這個也是小Kiss了。
小結:
重載overload是依據函數的參數列表來選摘要挪用的函數版本,而多態是依據運轉時對象的現實類型來選摘要挪用的虛virtual函數版本,多態的完成是經由過程派生類對基類的虛virtual函數停止籠罩override來完成的,若派生類沒有對基類的虛virtual函數停止籠罩override的話,則派生類會主動繼續基類的虛virtual函數版本,此時不管基類指針指向的對象是基類型照樣派生類型,都邑挪用基類版本的虛virtual函數;假如派生類對基類的虛virtual函數停止籠罩override的話,則會在運轉時依據對象的現實類型來選摘要挪用的虛virtual函數版本,例如基類指針指向的對象類型為派生類型,則會挪用派生類的虛virtual函數版本,從而完成多態。
應用多態的本意是要我們在基類中聲明函數為virtual,而且是要在派生類中籠罩override基類的虛virtual函數版本,留意,此時的函數原型與基類堅持分歧,即同名同參數類型;假如你在派生類中新添加函數版本,你不克不及經由過程基類指針靜態挪用派生類的新的函數版本,這個新的函數版本只作為派生類的一個重載版本。照樣統一句話,重載只要在以後類中有用,不論你是在基類重載的,照樣在派生類中重載的,二者互不連累。假如明確這一點的話,在例6、例9中,我們也會對其的輸入成果順遂地輿解。
重載是靜態聯編的,多態是靜態聯編的。進一步說明,重載與指針現實指向的對象類型有關,多態與指針現實指向的對象類型相干。若基類的指針挪用派生類的重載版本,C++編繹以為長短法的,C++編繹器只以為基類指針只能挪用基類的重載版本,重載只在以後類的名字空間感化域內有用,繼續會掉去重載的特征,固然,若此時的基類指針挪用的是一個虛virtual函數,那末它還會停止靜態選擇基類的虛virtual函數版本照樣派生類的虛virtual函數版原來停止詳細的操作,這是經由過程基類指針現實指向的對象類型來做決議的,所以說重載與指針現實指向的對象類型有關,多態與指針現實指向的對象類型相干。
最初說明一點,虛virtual函數異樣可以停止重載,然則重載只能是在以後本身名字空間感化域內有用
究竟創立了幾個String對象?
我們起首來看一段代碼:
Java代碼
String str=new String("abc");
緊接著這段代碼以後的常常是這個成績,那就是這行代碼畢竟創立了幾個String對象呢?信任年夜家對這道題其實不生疏,謎底也是盡人皆知的,2個。接上去我們就從這道題睜開,一路回想一下與創立String對象相干的一些JAVA常識。
我們可以把下面這行代碼分紅String str、=、"abc"和new String()四部門來對待。String str只是界說了一個名為str的String類型的變量,是以它並沒有創立對象;=是對變量str停止初始化,將某個對象的援用(或許叫句柄)賦值給它,明顯也沒有創立對象;如今只剩下new String("abc")了。那末,new String("abc")為何又能被算作"abc"和new String()呢?我們來看一下被我們挪用了的String的結構器:
Java代碼
public String(String original) {
//other code ...
}
年夜家都曉得,我們經常使用的創立一個類的實例(對象)的辦法有以下兩種:
應用new創立對象。
挪用Class類的newInstance辦法,應用反射機制創立對象。
我們恰是應用new挪用了String類的下面誰人結構器辦法創立了一個對象,並將它的援用賦值給了str變量。同時我們留意到,被挪用的結構器辦法接收的參數也是一個String對象,這個對象恰是"abc"。由此我們又要引入別的一種創立String對象的方法的評論辯論——引號內包括文本。
這類方法是String獨有的,而且它與new的方法存在很年夜差別。
Java代碼
String str="abc";
毫無疑問,這行代碼創立了一個String對象。
Java代碼
String a="abc";
String b="abc";
那這裡呢?謎底照樣一個。
Java代碼
String a="ab"+"cd";
再看看這裡呢?謎底還是一個。有點奇異嗎?說到這裡,我們就須要引入對字符串池相干常識的回想了。
在JAVA虛擬機(JVM)中存在著一個字符串池,個中保留著許多String對象,而且可以被同享應用,是以它進步了效力。因為String類是final的,它的值一經創立就弗成轉變,是以我們不消擔憂String對象同享而帶來法式的凌亂。字符串池由String類保護,我們可以挪用intern()辦法來拜訪字符串池。
我們再回頭看看String a="abc";,這行代碼被履行的時刻,JAVA虛擬機起首在字符串池中查找能否曾經存在了值為"abc"的這麼一個對象,它的斷定根據是String類equals(Object obj)辦法的前往值。假如有,則不再創立新的對象,直接前往已存在對象的援用;假如沒有,則先創立這個對象,然後把它參加到字符串池中,再將它的援用前往。是以,我們不難懂得後面三個例子中頭兩個例子為何是這個謎底了。
關於第三個例子:
Java代碼
String a="ab"+"cd";
因為常量的值在編譯的時刻就被肯定了。在這裡,"ab"和"cd"都是常量,是以變量a的值在編譯時便可以肯定。這行代碼編譯後的後果同等於:
Java代碼
String a="abcd";
是以這裡只創立了一個對象"abcd",而且它被保留在字符串池裡了。
如今成績又來了,是否是一切經由“+”銜接後獲得的字符串都邑被添加到字符串池中呢?我們都曉得“==”可以用來比擬兩個變量,它有以下兩種情形:
假如比擬的是兩個根本類型(char,byte,short,int,long,float,double,boolean),則是斷定它們的值能否相等。
假如表較的是兩個對象變量,則是斷定它們的援用能否指向統一個對象。
上面我們就用“==”來做幾個測試。為了便於解釋,我們把指向字符串池中曾經存在的對象也視為該對象被參加了字符串池:
Java代碼
public class StringTest { public static void main(String[] args) { String a = "ab";// 創立了一個對象,並參加字符串池中 System.out.println("String a = \"ab\";"); String b = "cd";// 創立了一個對象,並參加字符串池中 System.out.println("String b = \"cd\";"); String c = "abcd";// 創立了一個對象,並參加字符串池中 String d = "ab" + "cd"; // 假如d和c指向了統一個對象,則解釋d也被參加了字符串池 if (d == c) { System.out.println("\"ab\"+\"cd\" 創立的對象 \"參加了\" 字符串池中"); } // 假如d和c沒有指向了統一個對象,則解釋d沒有被參加字符串池 else { System.out.println("\"ab\"+\"cd\" 創立的對象 \"沒參加\" 字符串池中"); } String e = a + "cd"; // 假如e和c指向了統一個對象,則解釋e也被參加了字符串池 if (e == c) { System.out.println(" a +\"cd\" 創立的對象 \"參加了\" 字符串池中"); } // 假如e和c沒有指向了統一個對象,則解釋e沒有被參加字符串池 else { System.out.println(" a +\"cd\" 創立的對象 \"沒參加\" 字符串池中"); } String f = "ab" + b; // 假如f和c指向了統一個對象,則解釋f也被參加了字符串池 if (f == c) { System.out.println("\"ab\"+ b 創立的對象 \"參加了\" 字符串池中"); } // 假如f和c沒有指向了統一個對象,則解釋f沒有被參加字符串池 else { System.out.println("\"ab\"+ b 創立的對象 \"沒參加\" 字符串池中"); } String g = a + b; // 假如g和c指向了統一個對象,則解釋g也被參加了字符串池 if (g == c) { System.out.println(" a + b 創立的對象 \"參加了\" 字符串池中"); } // 假如g和c沒有指向了統一個對象,則解釋g沒有被參加字符串池 else { System.out.println(" a + b 創立的對象 \"沒參加\" 字符串池中"); } } }
運轉成果以下:
String a = "ab";
String b = "cd";
"ab"+"cd" 創立的對象 "參加了" 字符串池中
a +"cd" 創立的對象 "沒參加" 字符串池中
"ab"+ b 創立的對象 "沒參加" 字符串池中
a + b 創立的對象 "沒參加" 字符串池中
從下面的成果中我們不好看出,只要應用引號包括文本的方法創立的String對象之間應用“+”銜接發生的新對象才會被參加字符串池中。關於一切包括new方法新建對象(包含null)的“+”銜接表達式,它所發生的新對象都不會被參加字符串池中,對此我們不再贅述。
然則有一種情形須要惹起我們的留意。請看上面的代碼:
Java代碼
public class StringStaticTest { // 常量A public static final String A = "ab"; // 常量B public static final String B = "cd"; public static void main(String[] args) { // 將兩個常量用+銜接對s停止初始化 String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s等於t,它們是統一個對象"); } else { System.out.println("s不等於t,它們不是統一個對象"); } } }
這段代碼的運轉成果以下:
s等於t,它們是統一個對象
這又是為何呢?緣由是如許的,關於常量來說,它的值是固定的,是以在編譯期就可以被肯定了,而變量的值只要到運轉時能力被肯定,由於這個變量可以被分歧的辦法挪用,從而能夠惹起值的轉變。在下面的例子中,A和B都是常量,值是固定的,是以s的值也是固定的,它在類被編譯時就曾經肯定了。也就是說:
Java代碼
String s=A+B;
同等於:
Java代碼
String s="ab"+"cd";
我對下面的例子略加轉變看看會湧現甚麼情形:
Java代碼
public class StringStaticTest { // 常量A public static final String A; // 常量B public static final String B; static { A = "ab"; B = "cd"; } public static void main(String[] args) { // 將兩個常量用+銜接對s停止初始化 String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s等於t,它們是統一個對象"); } else { System.out.println("s不等於t,它們不是統一個對象"); } } }
它的運轉成果是如許:
s不等於t,它們不是統一個對象
只是做了一點修改,成果就和方才的例子正好相反。我們再來剖析一下。A和B固然被界說為常量(只能被賦值一次),然則它們都沒有立時被賦值。在運算出s的值之前,他們什麼時候被賦值,和被付與甚麼樣的值,都是個變數。是以A和B在被賦值之前,性質相似於一個變量。那末s就不克不及在編譯期被肯定,而只能在運轉時被創立了。
因為字符串池中對象的同享可以或許帶來效力的進步,是以我們倡導年夜家用引號包括文本的方法來創立String對象,現實上這也是我們在編程中常采取的。
接上去我們再來看看intern()辦法,它的界說以下:
Java代碼
public native String intern();
這是一個當地辦法。在挪用這個辦法時,JAVA虛擬機起首檢討字符串池中能否曾經存在與該對象值相等對象存在,假如有則前往字符串池中對象的援用;假如沒有,則先在字符串池中創立一個雷同值的String對象,然後再將它的援用前往。
我們來看這段代碼:
Java代碼
public class StringInternTest { public static void main(String[] args) { // 應用char數組來初始化a,防止在a被創立之前字符串池中曾經存在了值為"abcd"的對象 String a = new String(new char[] { 'a', 'b', 'c', 'd' }); String b = a.intern(); if (b == a) { System.out.println("b被參加了字符串池中,沒有新建對象"); } else { System.out.println("b沒被參加字符串池中,新建了對象"); } } }
運轉成果:
b沒被參加字符串池中,新建了對象
假如String類的intern()辦法在沒有找到雷同值的對象時,是把以後對象參加字符串池中,然後前往它的援用的話,那末b和a指向的就是統一個對象;不然b指向的對象就是JAVA虛擬機在字符串池中新建的,只是它的值與a雷同而已。下面這段代碼的運轉成果恰好印證了這一點。
最初我們再來講說String對象在JAVA虛擬機(JVM)中的存儲,和字符串池與堆(heap)和棧(stack)的關系。我們起首回想一下堆和棧的差別:
棧(stack):重要保留根本類型(或許叫內置類型)(char、byte、short、int、long、float、double、boolean)和對象的援用,數據可以同享,速度僅次於存放器(register),快於堆。
堆(heap):用於存儲對象。
我們檢查String類的源碼就會發明,它有一個value屬性,保留著String對象的值,類型是char[],這也正解釋了字符串就是字符的序列。
當履行String a="abc";時,JAVA虛擬機遇在棧中創立三個char型的值'a'、'b'和'c',然後在堆中創立一個String對象,它的值(value)是適才在棧中創立的三個char型值構成的數組{'a','b','c'},最初這個新創立的String對象會被添加到字符串池中。假如我們接著履行String b=new String("abc");代碼,因為"abc"曾經被創立並保留於字符串池中,是以JAVA虛擬機只會在堆中新創立一個String對象,然則它的值(value)是同享前一行代碼履行時在棧中創立的三個char型值值'a'、'b'和'c'。
說到這裡,我們關於篇首提出的String str=new String("abc")為何是創立了兩個對象這個成績就曾經相當清楚明了了。