Java中繼承、多態(tài)、重載和重寫介紹
什么是多態(tài)?它的實(shí)現(xiàn)機(jī)制是什么呢?重載和重寫的區(qū)別在那里?這就是這一次我們要回顧的四個(gè)十分重要的概念:繼承、多態(tài)、重載和重寫。
繼承(inheritance)
簡(jiǎn)單的說(shuō),繼承就是在一個(gè)現(xiàn)有類型的基礎(chǔ)上,通過(guò)增加新的方法或者重定義已有方法(下面會(huì)講到,這種方式叫重寫)的方式,產(chǎn)生一個(gè)新的類型。繼承是面向?qū)ο蟮娜齻€(gè)基本特征--封裝、繼承、多態(tài)的其中之一,我們?cè)谑褂肑AVA時(shí)編寫的每一個(gè)類都是在繼承,因?yàn)樵贘AVA語(yǔ)言中,java.lang.Object類是所有類最根本的基類(或者叫父類、超類),如果我們新定義的一個(gè)類沒有明確地指定繼承自哪個(gè)基類,那么JAVA就會(huì)默認(rèn)為它是繼承自O(shè)bject類的。
我們可以把JAVA中的類分為以下三種:
類:使用class定義且不含有抽象方法的類。
抽象類:使用abstract class定義的類,它可以含有,也可以不含有抽象方法。
接口:使用interface定義的類。
在這三種類型之間存在下面的繼承規(guī)律:
類可以繼承(extends)類,可以繼承(extends)抽象類,可以繼承(implements)接口。
抽象類可以繼承(extends)類,可以繼承(extends)抽象類,可以繼承(implements)接口。
接口只能繼承(extends)接口。
請(qǐng)注意上面三條規(guī)律中每種繼承情況下使用的不同的關(guān)鍵字extends和implements,它們是不可以隨意替換的。大家知道,一個(gè)普通類繼承一個(gè)接口后,必須實(shí)現(xiàn)這個(gè)接口中定義的所有方法,否則就只能被定義為抽象類。我在這里之所以沒有對(duì)implements關(guān)鍵字使用“實(shí)現(xiàn)”這種說(shuō)法是因?yàn)閺母拍钌蟻?lái)說(shuō)它也是表示一種繼承關(guān)系,而且對(duì)于抽象類implements接口的情況下,它并不是一定要實(shí)現(xiàn)這個(gè)接口定義的任何方法,因此使用繼承的說(shuō)法更為合理一些。
以上三條規(guī)律同時(shí)遵守下面這些約束:
類和抽象類都只能最多繼承一個(gè)類,或者最多繼承一個(gè)抽象類,并且這兩種情況是互斥的,也就是說(shuō)它們要么繼承一個(gè)類,要么繼承一個(gè)抽象類。
類、抽象類和接口在繼承接口時(shí),不受數(shù)量的約束,理論上可以繼承無(wú)限多個(gè)接口。當(dāng)然,對(duì)于類來(lái)說(shuō),它必須實(shí)現(xiàn)它所繼承的所有接口中定義的全部方法。
抽象類繼承抽象類,或者實(shí)現(xiàn)接口時(shí),可以部分、全部或者完全不實(shí)現(xiàn)父類抽象類的抽象(abstract)方法,或者父類接口中定義的接口。
類繼承抽象類,或者實(shí)現(xiàn)接口時(shí),必須全部實(shí)現(xiàn)父類抽象類的全部抽象(abstract)方法,或者父類接口中定義的全部接口。
繼承給我們的編程帶來(lái)的好處就是對(duì)原有類的復(fù)用(重用)。就像模塊的復(fù)用一樣,類的復(fù)用可以提高我們的開發(fā)效率,實(shí)際上,模塊的復(fù)用是大量類的復(fù)用疊加后的效果。除了繼承之外,我們還可以使用組合的方式來(lái)復(fù)用類。所謂組合就是把原有類定義為新類的一個(gè)屬性,通過(guò)在新類中調(diào)用原有類的方法來(lái)實(shí)現(xiàn)復(fù)用。如果新定義的類型與原有類型之間不存在被包含的關(guān)系,也就是說(shuō),從抽象概念上來(lái)講,新定義類型所代表的事物并不是原有類型所代表事物的一種,比如黃種人是人類的一種,它們之間存在包含與被包含的關(guān)系,那么這時(shí)組合就是實(shí)現(xiàn)復(fù)用更好的選擇。下面這個(gè)例子就是組合方式的一個(gè)簡(jiǎn)單示例:
public class Sub { private Parent p = new Parent(); public void doSomething() { // 復(fù)用Parent類的方法 p.method(); // other code } } class Parent { public void method() { // do something here } }
當(dāng)然,為了使代碼更加有效,我們也可以在需要使用到原有類型(比如Parent p)時(shí),才對(duì)它進(jìn)行初始化。
使用繼承和組合復(fù)用原有的類,都是一種增量式的開發(fā)模式,這種方式帶來(lái)的好處是不需要修改原有的代碼,因此不會(huì)給原有代碼帶來(lái)新的BUG,也不用因?yàn)閷?duì)原有代碼的修改而重新進(jìn)行測(cè)試,這對(duì)我們的開發(fā)顯然是有益的。因此,如果我們是在維護(hù)或者改造一個(gè)原有的系統(tǒng)或模塊,尤其是對(duì)它們的了解不是很透徹的時(shí)候,就可以選擇增量開發(fā)的模式,這不僅可以大大提高我們的開發(fā)效率,也可以規(guī)避由于對(duì)原有代碼的修改而帶來(lái)的風(fēng)險(xiǎn)。
多態(tài)(Polymorphism)
多態(tài)是又一個(gè)重要的基本概念,上面說(shuō)到了,它是面向?qū)ο蟮娜齻€(gè)基本特征之一。究竟什么是多態(tài)呢?我們先看看下面的例子,來(lái)幫助理解:
//汽車接口 interface Car { // 汽車名稱 String getName(); // 獲得汽車售價(jià) 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() + " 單價(jià):" + car.getPrice()); // 增加賣出車售價(jià)的收入 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()); } }
運(yùn)行結(jié)果:
車型:BMW 單價(jià):300000
車型:CheryQQ 單價(jià):20000
總收入:320000
繼承是多態(tài)得以實(shí)現(xiàn)的基礎(chǔ)。從字面上理解,多態(tài)就是一種類型(都是Car類型)表現(xiàn)出多種狀態(tài)(寶馬汽車的名稱是BMW,售價(jià)是300000;奇瑞汽車的名稱是CheryQQ,售價(jià)是2000)。將一個(gè)方法調(diào)用同這個(gè)方法所屬的主體(也就是對(duì)象或類)關(guān)聯(lián)起來(lái)叫做綁定,分前期綁定和后期綁定兩種。下面解釋一下它們的定義:
前期綁定:在程序運(yùn)行之前進(jìn)行綁定,由編譯器和連接程序?qū)崿F(xiàn),又叫做靜態(tài)綁定。比如static方法和final方法,注意,這里也包括private方法,因?yàn)樗请[式final的。
后期綁定:在運(yùn)行時(shí)根據(jù)對(duì)象的類型進(jìn)行綁定,由方法調(diào)用機(jī)制實(shí)現(xiàn),因此又叫做動(dòng)態(tài)綁定,或者運(yùn)行時(shí)綁定。除了前期綁定外的所有方法都屬于后期綁定。
多態(tài)就是在后期綁定這種機(jī)制上實(shí)現(xiàn)的。多態(tài)給我們帶來(lái)的好處是消除了類之間的耦合關(guān)系,使程序更容易擴(kuò)展。比如在上例中,新增加一種類型汽車的銷售,只需要讓新定義的類繼承Car類并實(shí)現(xiàn)它的所有方法,而無(wú)需對(duì)原有代碼做任何修改,CarShop類的sellCar(Car car)方法就可以處理新的車型了。新增代碼如下:
// 桑塔納汽車 class Santana implements Car { public String getName() { return "Santana"; } public int getPrice() { return 80000; } }
重載(overloading)和重寫(overriding)
重載和重寫都是針對(duì)方法的概念,在弄清楚這兩個(gè)概念之前,我們先來(lái)了解一下什么叫方法的型構(gòu)(英文名是signature,有的譯作“簽名”,雖然它被使用的較為廣泛,但是這個(gè)翻譯不準(zhǔn)確的)。型構(gòu)就是指方法的組成結(jié)構(gòu),具體包括方法的名稱和參數(shù),涵蓋參數(shù)的數(shù)量、類型以及出現(xiàn)的順序,但是不包括方法的返回值類型,訪問(wèn)權(quán)限修飾符,以及abstract、static、final等修飾符。比如下面兩個(gè)就是具有相同型構(gòu)的方法:
public void method(int i, String s) { // do something } public String method(int i, String s) { // do something }
而這兩個(gè)就是具有不同型構(gòu)的方法:
public void method(int i, String s) { // do something } public void method(String s, int i) { // do something }
了解完型構(gòu)的概念后我們?cè)賮?lái)看看重載和重寫,請(qǐng)看它們的定義:
重寫,英文名是overriding,是指在繼承情況下,子類中定義了與其基類中方法具有相同型構(gòu)的新方法,就叫做子類把基類的方法重寫了。這是實(shí)現(xiàn)多態(tài)必須的步驟。
重載,英文名是overloading,是指在同一個(gè)類中定義了一個(gè)以上具有相同名稱,但是型構(gòu)不同的方法。在同一個(gè)類中,是不允許定義多于一個(gè)的具有相同型構(gòu)的方法的。
我們來(lái)考慮一個(gè)有趣的問(wèn)題:構(gòu)造器可以被重載嗎?答案當(dāng)然是可以的,我們?cè)趯?shí)際的編程中也經(jīng)常這么做。實(shí)際上構(gòu)造器也是一個(gè)方法,構(gòu)造器名就是方法名,構(gòu)造器參數(shù)就是方法參數(shù),而它的返回值就是新創(chuàng)建的類的實(shí)例。但是構(gòu)造器卻不可以被子類重寫,因?yàn)樽宇悷o(wú)法定義與基類具有相同型構(gòu)的構(gòu)造器。
重載、覆蓋、多態(tài)與函數(shù)隱藏
經(jīng)常看到C++的一些初學(xué)者對(duì)于重載、覆蓋、多態(tài)與函數(shù)隱藏的模糊理解。在這里寫一點(diǎn)自己的見解,希望能夠C++初學(xué)者解惑。
要弄清楚重載、覆蓋、多態(tài)與函數(shù)隱藏之間的復(fù)雜且微妙關(guān)系之前,我們首先要來(lái)回顧一下重載覆蓋等基本概念。
首先,我們來(lái)看一個(gè)非常簡(jiǎn)單的例子,理解一下什么叫函數(shù)隱藏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; //下面一句錯(cuò)誤,故屏蔽掉 //d.fun();error C2660: 'fun' : function does not take 0 parameters d.fun(1); Derive *pd =new Derive(); //下面一句錯(cuò)誤,故屏蔽掉 //pd->fun();error C2660: 'fun' : function does not take 0 parameters pd->fun(1); delete pd; return 0; }
/*在不同的非命名空間作用域里的函數(shù)不構(gòu)成重載,子類和父類是不同的兩個(gè)作用域。
在本例中,兩個(gè)函數(shù)在不同作用域中,故不夠成重載,除非這個(gè)作用域是命名空間作用域。*/
在這個(gè)例子中,函數(shù)不是重載overload,也不是覆蓋override,而是隱藏hide。
接下來(lái)的5個(gè)例子具體說(shuō)明一下什么叫隱藏
例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();//正確,派生類沒有與基類同名函數(shù)聲明,則基類中的所有同名重載函數(shù)都會(huì)作為候選函數(shù)。 d.fun(1);//正確,派生類沒有與基類同名函數(shù)聲明,則基類中的所有同名重載函數(shù)都會(huì)作為候選函數(shù)。 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: //新的函數(shù)版本,基類所有的重載版本都被屏蔽,在這里,我們稱之為函數(shù)隱藏hide //派生類中有基類的同名函數(shù)的聲明,則基類中的同名函數(shù)不會(huì)作為候選函數(shù),即使基類有不同的參數(shù)表的多個(gè)版本的重載函數(shù)。 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); //下面一句錯(cuò)誤,故屏蔽掉 //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基類的其中一個(gè)函數(shù)版本,同樣基類所有的重載版本都被隱藏hide //派生類中有基類的同名函數(shù)的聲明,則基類中的同名函數(shù)不會(huì)作為候選函數(shù),即使基類有不同的參數(shù)表的多個(gè)版本的重載函數(shù)。 void fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;} }; int main() { Derive d; d.fun(); //下面一句錯(cuò)誤,故屏蔽掉 //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; } /* 輸出結(jié)果 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; } /* 輸出結(jié)果 Base::fun() Base::fun(int i) Derive::fun(int i,int j) Press any key to continue */
好了,我們先來(lái)一個(gè)小小的總結(jié)重載與覆蓋兩者之間的特征
重載overload的特征:
n 相同的范圍(在同一個(gè)類中);
n 函數(shù)名相同參數(shù)不同;
n virtual 關(guān)鍵字可有可無(wú)。
覆蓋override是指派生類函數(shù)覆蓋基類函數(shù),覆蓋的特征是:
n 不同的范圍(分別位于派生類與基類);
n 函數(shù)名和參數(shù)都相同;
n 基類函數(shù)必須有virtual 關(guān)鍵字。(若沒有virtual 關(guān)鍵字則稱之為隱藏hide)
如果基類有某個(gè)函數(shù)的多個(gè)重載(overload)版本,而你在派生類中重寫(override)了基類中的一個(gè)或多個(gè)函數(shù)版本,或是在派生類中重新添加了新的函數(shù)版本(函數(shù)名相同,參數(shù)不同),則所有基類的重載版本都被屏蔽,在這里我們稱之為隱藏hide。所以,在一般情況下,你想在派生類中使用新的函數(shù)版本又想使用基類的函數(shù)版本時(shí),你應(yīng)該在派生類中重寫基類中的所有重載版本。你若是不想重寫基類的重載的函數(shù)版本,則你應(yīng)該使用例4或例5方式,顯式聲明基類名字空間作用域。
事實(shí)上,C++編譯器認(rèn)為,相同函數(shù)名不同參數(shù)的函數(shù)之間根本沒有什么關(guān)系,它們根本就是兩個(gè)毫不相關(guān)的函數(shù)。只是C++語(yǔ)言為了模擬現(xiàn)實(shí)世界,為了讓程序員更直觀的思維處理現(xiàn)實(shí)世界中的問(wèn)題,才引入了重載和覆蓋的概念。重載是在相同名字空間作用域下,而覆蓋則是在不同的名字空間作用域下,比如基類和派生類即為兩個(gè)不同的名字空間作用域。在繼承過(guò)程中,若發(fā)生派生類與基類函數(shù)同名問(wèn)題時(shí),便會(huì)發(fā)生基類函數(shù)的隱藏。當(dāng)然,這里討論的情況是基類函數(shù)前面沒有virtual 關(guān)鍵字。在有virtual 關(guān)鍵字關(guān)鍵字時(shí)的情形我們另做討論。
繼承類重寫了基類的某一函數(shù)版本,以產(chǎn)生自己功能的接口。此時(shí)C++編繹器認(rèn)為,你現(xiàn)在既然要使用派生類的自己重新改寫的接口,那我基類的接口就不提供給你了(當(dāng)然你可以用顯式聲明名字空間作用域的方法,見[C++基礎(chǔ)]重載、覆蓋、多態(tài)與函數(shù)隱藏(1))。而不會(huì)理會(huì)你基類的接口是有重載特性的。若是你要在派生類里繼續(xù)保持重載的特性,那你就自己再給出接口重載的特性吧。所以在派生類里,只要函數(shù)名一樣,基類的函數(shù)版本就會(huì)被無(wú)情地屏蔽。在編繹器中,屏蔽是通過(guò)名字空間作用域?qū)崿F(xiàn)的。
所以,在派生類中要保持基類的函數(shù)重載版本,就應(yīng)該重寫所有基類的重載版本。重載只在當(dāng)前類中有效,繼承會(huì)失去函數(shù)重載的特性。也就是說(shuō),要把基類的重載函數(shù)放在繼承的派生類里,就必須重寫。
這里“隱藏”是指派生類的函數(shù)屏蔽了與其同名的基類函數(shù),具體規(guī)則我們也來(lái)做一小結(jié):
n 如果派生類的函數(shù)與基類的函數(shù)同名,但是參數(shù)不同。此時(shí),若基類無(wú)virtual關(guān)鍵字,基類的函數(shù)將被隱藏。(注意別與重載混淆,雖然函數(shù)名相同參數(shù)不同應(yīng)稱之為重載,但這里不能理解為重載,因?yàn)榕缮惡突惒辉谕幻挚臻g作用域內(nèi)。這里理解為隱藏)
n 如果派生類的函數(shù)與基類的函數(shù)同名,但是參數(shù)不同。此時(shí),若基類有virtual關(guān)鍵字,基類的函數(shù)將被隱式繼承到派生類的vtable中。此時(shí)派生類vtable中的函數(shù)指向基類版本的函數(shù)地址。同時(shí)這個(gè)新的函數(shù)版本添加到派生類中,作為派生類的重載版本。但在基類指針實(shí)現(xiàn)多態(tài)調(diào)用函數(shù)方法時(shí),這個(gè)新的派生類函數(shù)版本將會(huì)被隱藏。
n 如果派生類的函數(shù)與基類的函數(shù)同名,并且參數(shù)也相同,但是基類函數(shù)沒有virtual關(guān)鍵字。此時(shí),基類的函數(shù)被隱藏。(注意別與覆蓋混淆,這里理解為隱藏)。
n 如果派生類的函數(shù)與基類的函數(shù)同名,并且參數(shù)也相同,但是基類函數(shù)有virtual關(guān)鍵字。此時(shí),基類的函數(shù)不會(huì)被“隱藏”。(在這里,你要理解為覆蓋哦^_^)。
插曲:基類函數(shù)前沒有virtual關(guān)鍵字時(shí),我們要重寫更為順口些,在有virtual關(guān)鍵字時(shí),我們叫覆蓋更為合理些,戒此,我也希望大家能夠更好的理解C++中一些微妙的東西。費(fèi)話少說(shuō),我們舉例說(shuō)明吧。
例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); //下面一句錯(cuò)誤,故屏蔽掉 //pb->fun(1,2);virtual函數(shù)不能進(jìn)行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; } /*
輸出結(jié)果
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很好理解,我把這兩個(gè)例子放在這里,是讓大家作一個(gè)比較擺了,也是為了幫助大家更好的理解:
n 例7-1中,派生類沒有覆蓋基類的虛函數(shù),此時(shí)派生類的vtable中的函數(shù)指針指向的地址就是基類的虛函數(shù)地址。
n 例8-1中,派生類覆蓋了基類的虛函數(shù),此時(shí)派生類的vtable中的函數(shù)指針指向的地址就是派生類自己的重寫的虛函數(shù)地址。
在例7-2和8-2看起來(lái)有點(diǎn)怪怪,其實(shí),你按照上面的原則對(duì)比一下,答案也是明朗的:
n 例7-2中,我們?yōu)榕缮愔剌d了一個(gè)函數(shù)版本:void fun(double d) 其實(shí),這只是一個(gè)障眼法。我們具體來(lái)分析一下,基類共有幾個(gè)函數(shù),派生類共有幾個(gè)函數(shù):
類型
基類
派生類
Vtable部分
void fun(int i)
指向基類版的虛函數(shù)void fun(int i)
靜態(tài)部分
void fun(double d)
我們?cè)賮?lái)分析一下以下三句代碼
Base *pb = new Derive();
pb->fun(1);//Base::fun(int i)
pb->fun((double)0.01);//Base::fun(int i)
這第一句是關(guān)鍵,基類指針指向派生類的對(duì)象,我們知道這是多態(tài)調(diào)用;接下來(lái)第二句,運(yùn)行時(shí)基類指針根據(jù)運(yùn)行時(shí)對(duì)象的類型,發(fā)現(xiàn)是派生類對(duì)象,所以首先到派生類的vtable中去查找派生類的虛函數(shù)版本,發(fā)現(xiàn)派生類沒有覆蓋基類的虛函數(shù),派生類的vtable只是作了一個(gè)指向基類虛函數(shù)地址的一個(gè)指向,所以理所當(dāng)然地去調(diào)用基類版本的虛函數(shù)。最后一句,程序運(yùn)行仍然埋頭去找派生類的vtable,發(fā)現(xiàn)根本沒有這個(gè)版本的虛函數(shù),只好回頭調(diào)用自己的僅有一個(gè)虛函數(shù)。
這里還值得一提的是:如果此時(shí)基類有多個(gè)虛函數(shù),此時(shí)程序編繹時(shí)會(huì)提示”調(diào)用不明確”。示例如下
#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; }
好了,我們?cè)賮?lái)分析一下例8-2。
n 例8-2中,我們也為派生類重載了一個(gè)函數(shù)版本:void fun(double d) ,同時(shí)覆蓋了基類的虛函數(shù),我們?cè)賮?lái)具體來(lái)分析一下,基類共有幾個(gè)函數(shù),派生類共有幾個(gè)函數(shù):
類型
基類
派生類
Vtable部分
void fun(int i)
void fun(int i)
靜態(tài)部分
void fun(double d)
從表中我們可以看到,派生類的vtable中函數(shù)指針指向的是自己的重寫的虛函數(shù)地址。
我們?cè)賮?lái)分析一下以下三句代碼
Base *pb = new Derive();
pb->fun(1);//Derive::fun(int i)
pb->fun((double)0.01);//Derive::fun(int i)
第一句不必多說(shuō)了,第二句,理所當(dāng)然調(diào)用派生類的虛函數(shù)版本,第三句,嘿,感覺又怪怪的,其實(shí)呀,C++程序很笨的了,在運(yùn)行時(shí),埋頭闖進(jìn)派生類的vtable表中,只眼一看,靠,競(jìng)?cè)粵]有想要的版本,真是想不通,基類指針為什么不四處轉(zhuǎn)轉(zhuǎn)再找找呢?呵呵,原來(lái)是眼力有限,基類年紀(jì)這么老了,想必肯定是老花了,它那雙眼睛看得到的僅是自己的非Vtable部分(即靜態(tài)部分)和自己要管理的Vtable部分,派生類的void fun(double d)那么遠(yuǎn),看不到呀!再說(shuō)了,派生類什么都要管,難道派生類沒有自己的一點(diǎn)權(quán)力嗎?哎,不吵了,各自管自己的吧^_^
唉!你是不是要嘆氣了,基類指針能進(jìn)行多態(tài)調(diào)用,但是始終不能進(jìn)行派生類的重載調(diào)用啊(參考例6)~~~
再來(lái)看看例9,
本例的效果同例6,異曲同工。想必你理解了上面的這些例子后,這個(gè)也是小Kiss了。
小結(jié):
重載overload是根據(jù)函數(shù)的參數(shù)列表來(lái)選擇要調(diào)用的函數(shù)版本,而多態(tài)是根據(jù)運(yùn)行時(shí)對(duì)象的實(shí)際類型來(lái)選擇要調(diào)用的虛virtual函數(shù)版本,多態(tài)的實(shí)現(xiàn)是通過(guò)派生類對(duì)基類的虛virtual函數(shù)進(jìn)行覆蓋override來(lái)實(shí)現(xiàn)的,若派生類沒有對(duì)基類的虛virtual函數(shù)進(jìn)行覆蓋override的話,則派生類會(huì)自動(dòng)繼承基類的虛virtual函數(shù)版本,此時(shí)無(wú)論基類指針指向的對(duì)象是基類型還是派生類型,都會(huì)調(diào)用基類版本的虛virtual函數(shù);如果派生類對(duì)基類的虛virtual函數(shù)進(jìn)行覆蓋override的話,則會(huì)在運(yùn)行時(shí)根據(jù)對(duì)象的實(shí)際類型來(lái)選擇要調(diào)用的虛virtual函數(shù)版本,例如基類指針指向的對(duì)象類型為派生類型,則會(huì)調(diào)用派生類的虛virtual函數(shù)版本,從而實(shí)現(xiàn)多態(tài)。
使用多態(tài)的本意是要我們?cè)诨愔新暶骱瘮?shù)為virtual,并且是要在派生類中覆蓋override基類的虛virtual函數(shù)版本,注意,此時(shí)的函數(shù)原型與基類保持一致,即同名同參數(shù)類型;如果你在派生類中新添加函數(shù)版本,你不能通過(guò)基類指針動(dòng)態(tài)調(diào)用派生類的新的函數(shù)版本,這個(gè)新的函數(shù)版本只作為派生類的一個(gè)重載版本。還是同一句話,重載只有在當(dāng)前類中有效,不管你是在基類重載的,還是在派生類中重載的,兩者互不牽連。如果明白這一點(diǎn)的話,在例6、例9中,我們也會(huì)對(duì)其的輸出結(jié)果順利地理解。
重載是靜態(tài)聯(lián)編的,多態(tài)是動(dòng)態(tài)聯(lián)編的。進(jìn)一步解釋,重載與指針實(shí)際指向的對(duì)象類型無(wú)關(guān),多態(tài)與指針實(shí)際指向的對(duì)象類型相關(guān)。若基類的指針調(diào)用派生類的重載版本,C++編繹認(rèn)為是非法的,C++編繹器只認(rèn)為基類指針只能調(diào)用基類的重載版本,重載只在當(dāng)前類的名字空間作用域內(nèi)有效,繼承會(huì)失去重載的特性,當(dāng)然,若此時(shí)的基類指針調(diào)用的是一個(gè)虛virtual函數(shù),那么它還會(huì)進(jìn)行動(dòng)態(tài)選擇基類的虛virtual函數(shù)版本還是派生類的虛virtual函數(shù)版本來(lái)進(jìn)行具體的操作,這是通過(guò)基類指針實(shí)際指向的對(duì)象類型來(lái)做決定的,所以說(shuō)重載與指針實(shí)際指向的對(duì)象類型無(wú)關(guān),多態(tài)與指針實(shí)際指向的對(duì)象類型相關(guān)。
最后闡明一點(diǎn),虛virtual函數(shù)同樣可以進(jìn)行重載,但是重載只能是在當(dāng)前自己名字空間作用域內(nèi)有效
到底創(chuàng)建了幾個(gè)String對(duì)象?
我們首先來(lái)看一段代碼:
Java代碼
String str=new String("abc");
緊接著這段代碼之后的往往是這個(gè)問(wèn)題,那就是這行代碼究竟創(chuàng)建了幾個(gè)String對(duì)象呢?相信大家對(duì)這道題并不陌生,答案也是眾所周知的,2個(gè)。接下來(lái)我們就從這道題展開,一起回顧一下與創(chuàng)建String對(duì)象相關(guān)的一些JAVA知識(shí)。
我們可以把上面這行代碼分成String str、=、"abc"和new String()四部分來(lái)看待。String str只是定義了一個(gè)名為str的String類型的變量,因此它并沒有創(chuàng)建對(duì)象;=是對(duì)變量str進(jìn)行初始化,將某個(gè)對(duì)象的引用(或者叫句柄)賦值給它,顯然也沒有創(chuàng)建對(duì)象;現(xiàn)在只剩下new String("abc")了。那么,new String("abc")為什么又能被看成"abc"和new String()呢?我們來(lái)看一下被我們調(diào)用了的String的構(gòu)造器:
Java代碼
public String(String original) {
//other code ...
}
大家都知道,我們常用的創(chuàng)建一個(gè)類的實(shí)例(對(duì)象)的方法有以下兩種:
使用new創(chuàng)建對(duì)象。
調(diào)用Class類的newInstance方法,利用反射機(jī)制創(chuàng)建對(duì)象。
我們正是使用new調(diào)用了String類的上面那個(gè)構(gòu)造器方法創(chuàng)建了一個(gè)對(duì)象,并將它的引用賦值給了str變量。同時(shí)我們注意到,被調(diào)用的構(gòu)造器方法接受的參數(shù)也是一個(gè)String對(duì)象,這個(gè)對(duì)象正是"abc"。由此我們又要引入另外一種創(chuàng)建String對(duì)象的方式的討論——引號(hào)內(nèi)包含文本。
這種方式是String特有的,并且它與new的方式存在很大區(qū)別。
Java代碼
String str="abc";
毫無(wú)疑問(wèn),這行代碼創(chuàng)建了一個(gè)String對(duì)象。
Java代碼
String a="abc";
String b="abc";
那這里呢?答案還是一個(gè)。
Java代碼
String a="ab"+"cd";
再看看這里呢?答案仍是一個(gè)。有點(diǎn)奇怪嗎?說(shuō)到這里,我們就需要引入對(duì)字符串池相關(guān)知識(shí)的回顧了。
在JAVA虛擬機(jī)(JVM)中存在著一個(gè)字符串池,其中保存著很多String對(duì)象,并且可以被共享使用,因此它提高了效率。由于String類是final的,它的值一經(jīng)創(chuàng)建就不可改變,因此我們不用擔(dān)心String對(duì)象共享而帶來(lái)程序的混亂。字符串池由String類維護(hù),我們可以調(diào)用intern()方法來(lái)訪問(wèn)字符串池。
我們?cè)倩仡^看看String a="abc";,這行代碼被執(zhí)行的時(shí)候,JAVA虛擬機(jī)首先在字符串池中查找是否已經(jīng)存在了值為"abc"的這么一個(gè)對(duì)象,它的判斷依據(jù)是String類equals(Object obj)方法的返回值。如果有,則不再創(chuàng)建新的對(duì)象,直接返回已存在對(duì)象的引用;如果沒有,則先創(chuàng)建這個(gè)對(duì)象,然后把它加入到字符串池中,再將它的引用返回。因此,我們不難理解前面三個(gè)例子中頭兩個(gè)例子為什么是這個(gè)答案了。
對(duì)于第三個(gè)例子:
Java代碼
String a="ab"+"cd";
由于常量的值在編譯的時(shí)候就被確定了。在這里,"ab"和"cd"都是常量,因此變量a的值在編譯時(shí)就可以確定。這行代碼編譯后的效果等同于:
Java代碼
String a="abcd";
因此這里只創(chuàng)建了一個(gè)對(duì)象"abcd",并且它被保存在字符串池里了。
現(xiàn)在問(wèn)題又來(lái)了,是不是所有經(jīng)過(guò)“+”連接后得到的字符串都會(huì)被添加到字符串池中呢?我們都知道“==”可以用來(lái)比較兩個(gè)變量,它有以下兩種情況:
如果比較的是兩個(gè)基本類型(char,byte,short,int,long,float,double,boolean),則是判斷它們的值是否相等。
如果表較的是兩個(gè)對(duì)象變量,則是判斷它們的引用是否指向同一個(gè)對(duì)象。
下面我們就用“==”來(lái)做幾個(gè)測(cè)試。為了便于說(shuō)明,我們把指向字符串池中已經(jīng)存在的對(duì)象也視為該對(duì)象被加入了字符串池:
Java代碼
public class StringTest { public static void main(String[] args) { String a = "ab";// 創(chuàng)建了一個(gè)對(duì)象,并加入字符串池中 System.out.println("String a = \"ab\";"); String b = "cd";// 創(chuàng)建了一個(gè)對(duì)象,并加入字符串池中 System.out.println("String b = \"cd\";"); String c = "abcd";// 創(chuàng)建了一個(gè)對(duì)象,并加入字符串池中 String d = "ab" + "cd"; // 如果d和c指向了同一個(gè)對(duì)象,則說(shuō)明d也被加入了字符串池 if (d == c) { System.out.println("\"ab\"+\"cd\" 創(chuàng)建的對(duì)象 \"加入了\" 字符串池中"); } // 如果d和c沒有指向了同一個(gè)對(duì)象,則說(shuō)明d沒有被加入字符串池 else { System.out.println("\"ab\"+\"cd\" 創(chuàng)建的對(duì)象 \"沒加入\" 字符串池中"); } String e = a + "cd"; // 如果e和c指向了同一個(gè)對(duì)象,則說(shuō)明e也被加入了字符串池 if (e == c) { System.out.println(" a +\"cd\" 創(chuàng)建的對(duì)象 \"加入了\" 字符串池中"); } // 如果e和c沒有指向了同一個(gè)對(duì)象,則說(shuō)明e沒有被加入字符串池 else { System.out.println(" a +\"cd\" 創(chuàng)建的對(duì)象 \"沒加入\" 字符串池中"); } String f = "ab" + b; // 如果f和c指向了同一個(gè)對(duì)象,則說(shuō)明f也被加入了字符串池 if (f == c) { System.out.println("\"ab\"+ b 創(chuàng)建的對(duì)象 \"加入了\" 字符串池中"); } // 如果f和c沒有指向了同一個(gè)對(duì)象,則說(shuō)明f沒有被加入字符串池 else { System.out.println("\"ab\"+ b 創(chuàng)建的對(duì)象 \"沒加入\" 字符串池中"); } String g = a + b; // 如果g和c指向了同一個(gè)對(duì)象,則說(shuō)明g也被加入了字符串池 if (g == c) { System.out.println(" a + b 創(chuàng)建的對(duì)象 \"加入了\" 字符串池中"); } // 如果g和c沒有指向了同一個(gè)對(duì)象,則說(shuō)明g沒有被加入字符串池 else { System.out.println(" a + b 創(chuàng)建的對(duì)象 \"沒加入\" 字符串池中"); } } }
運(yùn)行結(jié)果如下:
String a = "ab";
String b = "cd";
"ab"+"cd" 創(chuàng)建的對(duì)象 "加入了" 字符串池中
a +"cd" 創(chuàng)建的對(duì)象 "沒加入" 字符串池中
"ab"+ b 創(chuàng)建的對(duì)象 "沒加入" 字符串池中
a + b 創(chuàng)建的對(duì)象 "沒加入" 字符串池中
從上面的結(jié)果中我們不難看出,只有使用引號(hào)包含文本的方式創(chuàng)建的String對(duì)象之間使用“+”連接產(chǎn)生的新對(duì)象才會(huì)被加入字符串池中。對(duì)于所有包含new方式新建對(duì)象(包括null)的“+”連接表達(dá)式,它所產(chǎn)生的新對(duì)象都不會(huì)被加入字符串池中,對(duì)此我們不再贅述。
但是有一種情況需要引起我們的注意。請(qǐng)看下面的代碼:
Java代碼
public class StringStaticTest { // 常量A public static final String A = "ab"; // 常量B public static final String B = "cd"; public static void main(String[] args) { // 將兩個(gè)常量用+連接對(duì)s進(jìn)行初始化 String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s等于t,它們是同一個(gè)對(duì)象"); } else { System.out.println("s不等于t,它們不是同一個(gè)對(duì)象"); } } }
這段代碼的運(yùn)行結(jié)果如下:
s等于t,它們是同一個(gè)對(duì)象
這又是為什么呢?原因是這樣的,對(duì)于常量來(lái)講,它的值是固定的,因此在編譯期就能被確定了,而變量的值只有到運(yùn)行時(shí)才能被確定,因?yàn)檫@個(gè)變量可以被不同的方法調(diào)用,從而可能引起值的改變。在上面的例子中,A和B都是常量,值是固定的,因此s的值也是固定的,它在類被編譯時(shí)就已經(jīng)確定了。也就是說(shuō):
Java代碼
String s=A+B;
等同于:
Java代碼
String s="ab"+"cd";
我對(duì)上面的例子稍加改變看看會(huì)出現(xiàn)什么情況:
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) { // 將兩個(gè)常量用+連接對(duì)s進(jìn)行初始化 String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s等于t,它們是同一個(gè)對(duì)象"); } else { System.out.println("s不等于t,它們不是同一個(gè)對(duì)象"); } } }
它的運(yùn)行結(jié)果是這樣:
s不等于t,它們不是同一個(gè)對(duì)象
只是做了一點(diǎn)改動(dòng),結(jié)果就和剛剛的例子恰好相反。我們?cè)賮?lái)分析一下。A和B雖然被定義為常量(只能被賦值一次),但是它們都沒有馬上被賦值。在運(yùn)算出s的值之前,他們何時(shí)被賦值,以及被賦予什么樣的值,都是個(gè)變數(shù)。因此A和B在被賦值之前,性質(zhì)類似于一個(gè)變量。那么s就不能在編譯期被確定,而只能在運(yùn)行時(shí)被創(chuàng)建了。
由于字符串池中對(duì)象的共享能夠帶來(lái)效率的提高,因此我們提倡大家用引號(hào)包含文本的方式來(lái)創(chuàng)建String對(duì)象,實(shí)際上這也是我們?cè)诰幊讨谐2捎玫摹?
接下來(lái)我們?cè)賮?lái)看看intern()方法,它的定義如下:
Java代碼
public native String intern();
這是一個(gè)本地方法。在調(diào)用這個(gè)方法時(shí),JAVA虛擬機(jī)首先檢查字符串池中是否已經(jīng)存在與該對(duì)象值相等對(duì)象存在,如果有則返回字符串池中對(duì)象的引用;如果沒有,則先在字符串池中創(chuàng)建一個(gè)相同值的String對(duì)象,然后再將它的引用返回。
我們來(lái)看這段代碼:
Java代碼
public class StringInternTest { public static void main(String[] args) { // 使用char數(shù)組來(lái)初始化a,避免在a被創(chuàng)建之前字符串池中已經(jīng)存在了值為"abcd"的對(duì)象 String a = new String(new char[] { 'a', 'b', 'c', 'd' }); String b = a.intern(); if (b == a) { System.out.println("b被加入了字符串池中,沒有新建對(duì)象"); } else { System.out.println("b沒被加入字符串池中,新建了對(duì)象"); } } }
運(yùn)行結(jié)果:
b沒被加入字符串池中,新建了對(duì)象
如果String類的intern()方法在沒有找到相同值的對(duì)象時(shí),是把當(dāng)前對(duì)象加入字符串池中,然后返回它的引用的話,那么b和a指向的就是同一個(gè)對(duì)象;否則b指向的對(duì)象就是JAVA虛擬機(jī)在字符串池中新建的,只是它的值與a相同罷了。上面這段代碼的運(yùn)行結(jié)果恰恰印證了這一點(diǎn)。
最后我們?cè)賮?lái)說(shuō)說(shuō)String對(duì)象在JAVA虛擬機(jī)(JVM)中的存儲(chǔ),以及字符串池與堆(heap)和棧(stack)的關(guān)系。我們首先回顧一下堆和棧的區(qū)別:
棧(stack):主要保存基本類型(或者叫內(nèi)置類型)(char、byte、short、int、long、float、double、boolean)和對(duì)象的引用,數(shù)據(jù)可以共享,速度僅次于寄存器(register),快于堆。
堆(heap):用于存儲(chǔ)對(duì)象。
我們查看String類的源碼就會(huì)發(fā)現(xiàn),它有一個(gè)value屬性,保存著String對(duì)象的值,類型是char[],這也正說(shuō)明了字符串就是字符的序列。
當(dāng)執(zhí)行String a="abc";時(shí),JAVA虛擬機(jī)會(huì)在棧中創(chuàng)建三個(gè)char型的值'a'、'b'和'c',然后在堆中創(chuàng)建一個(gè)String對(duì)象,它的值(value)是剛才在棧中創(chuàng)建的三個(gè)char型值組成的數(shù)組{'a','b','c'},最后這個(gè)新創(chuàng)建的String對(duì)象會(huì)被添加到字符串池中。如果我們接著執(zhí)行String b=new String("abc");代碼,由于"abc"已經(jīng)被創(chuàng)建并保存于字符串池中,因此JAVA虛擬機(jī)只會(huì)在堆中新創(chuàng)建一個(gè)String對(duì)象,但是它的值(value)是共享前一行代碼執(zhí)行時(shí)在棧中創(chuàng)建的三個(gè)char型值值'a'、'b'和'c'。
說(shuō)到這里,我們對(duì)于篇首提出的String str=new String("abc")為什么是創(chuàng)建了兩個(gè)對(duì)象這個(gè)問(wèn)題就已經(jīng)相當(dāng)明了了。
相關(guān)文章
Java Metrics系統(tǒng)性能監(jiān)控工具的使用詳解
Metrics是一個(gè)Java庫(kù),可以對(duì)系統(tǒng)進(jìn)行監(jiān)控,統(tǒng)計(jì)一些系統(tǒng)的性能指標(biāo)。本文就來(lái)和大家詳細(xì)聊聊這個(gè)工具的具體使用,希望對(duì)大家有所幫助2022-11-11Java class文件格式之方法_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了Java class文件格式之方法的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06百度翻譯API使用詳細(xì)教程(前端vue+后端springboot)
這篇文章主要給大家介紹了關(guān)于百度翻譯API使用的相關(guān)資料,百度翻譯API是百度面向開發(fā)者推出的免費(fèi)翻譯服務(wù)開放接口,任何第三方應(yīng)用或網(wǎng)站都可以通過(guò)使用百度翻譯API為用戶提供實(shí)時(shí)優(yōu)質(zhì)的多語(yǔ)言翻譯服務(wù),需要的朋友可以參考下2024-02-02關(guān)于IDEA關(guān)聯(lián)數(shù)據(jù)庫(kù)的問(wèn)題
這篇文章主要介紹了IDEA關(guān)聯(lián)數(shù)據(jù)庫(kù)的相關(guān)知識(shí),本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03java實(shí)戰(zhàn)小技巧之字符串與容器互轉(zhuǎn)詳解
Java.lang.String是Java的字符串類. Srting是一個(gè)不可變對(duì)象,下面這篇文章主要給大家介紹了關(guān)于java實(shí)戰(zhàn)小技巧之字符串與容器互轉(zhuǎn)的相關(guān)資料,需要的朋友可以參考下2021-08-08Java使用HttpClient實(shí)現(xiàn)Post請(qǐng)求實(shí)例
本篇文章主要介紹了Java使用HttpClient實(shí)現(xiàn)Post請(qǐng)求實(shí)例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02