java 多态判断非空_重拾JavaSE基础——多态及其实现方式
今天是比較抽象的多態(tài),希望能給大家?guī)韼椭?/p>
主要內(nèi)容
多態(tài)
為什么使用多態(tài)
多態(tài)的形式
多態(tài)的概念
多態(tài)的劣勢
多態(tài)存在的必然條件
類型轉(zhuǎn)換
多態(tài)的實(shí)現(xiàn)原理
多態(tài)的分類
運(yùn)行時(shí)多態(tài)的形式
實(shí)現(xiàn)原理
常量池
方法調(diào)用方式
動(dòng)態(tài)綁定實(shí)現(xiàn)多態(tài)
寫在最后
多態(tài)
先說好不鉆牛角尖哈,多態(tài)Java的特性之一,先不著急說他的概念,先看看為什么要使用多態(tài),多態(tài)給我們帶來什么好處
為什么使用多態(tài)
舉個(gè)例子吧,老奶奶喜歡養(yǎng)寵物,領(lǐng)養(yǎng)了一只加菲貓,加菲貓是只小動(dòng)物,要吃飯,老奶奶每天負(fù)責(zé)喂它。Java翻譯過來就是下面這樣子的
// 老奶奶
public class Granny {
public static void main(String[] args) {
// 領(lǐng)養(yǎng)一只加菲貓,這里簡單的new出來了
Garfield garfield = new Garfield();
// 抱起加菲貓給它喂食
feed(garfield);
}
public static void feed(Garfield garfield) {
// 加菲貓吃東西
garfield.eat();
}
}
class Garfield extends Animal{
@Override
public void eat() {
System.out.println("加菲貓吃飽了");
}
}
abstract class Animal {
public abstract void eat();
}
一切都很順暢。但是這時(shí)候老奶奶又去領(lǐng)養(yǎng)了一只牧羊犬,牧羊犬也是小動(dòng)物,也要吃飯,老奶奶也要給他喂食,這時(shí)候代碼要添加一個(gè)牧羊犬類,老奶奶要添加一個(gè)給牧羊犬喂食的方法
public class Granny {
public static void main(String[] args) {
// 領(lǐng)養(yǎng)一只加菲貓,這里簡單的new出來了
Garfield garfield = new Garfield();
// 抱起加菲貓給它喂食
feed(garfield);
// 領(lǐng)養(yǎng)一只牧羊犬
Shepherd shepherd = new Shepherd();
// 老奶奶給他喂食
shepherd.eat();
}
public static void feed(Garfield garfield) {
// 加菲貓吃東西
garfield.eat();
}
public static void feed(Shepherd shepherd) {
// 加菲貓吃東西
shepherd.eat();
}
}
class Shepherd extends Animal{
@Override
public void eat() {
Systen.out.println("牧羊犬吃的很開心");
}
}
// 加菲貓
class Garfield extends Animal{
// ...
}
如果老奶奶還想繼續(xù)領(lǐng)養(yǎng)小動(dòng)物,老奶奶又要給這只小動(dòng)物創(chuàng)建一個(gè)新的喂食的方法。聰明的我給老奶奶指了條明路,只要把feed方法的參數(shù)范圍擴(kuò)大一點(diǎn),不要指定是加菲貓還是牧羊犬z,只要是小動(dòng)物都給他喂食,反正小動(dòng)物都有吃的方法。
public class Granny {
public static void main(String[] args) {
// 領(lǐng)養(yǎng)一只加菲貓,這里簡單的new出來了
Garfield garfield = new Garfield();
// 抱起加菲貓給它喂食
feed(garfield);
// 領(lǐng)養(yǎng)一只牧羊犬
Shepherd shepherd = new Shepherd();
// 老奶奶給他喂食
shepherd.eat();
}
// 擴(kuò)大了范圍
public static void feed(Animal animal) {
// 給動(dòng)物喂食
animal.eat();
}
}
這樣老奶奶就舒服了,所以多態(tài)的好處之一就是方便傳參。
后來老奶奶發(fā)現(xiàn)自己家里的動(dòng)物越來越多,受不了了決定只養(yǎng)一只其他的都賣了,于是老奶奶選擇留下加菲貓又回到了最初的日子
public class Granny {
public static void main(String[] args) {
// 領(lǐng)養(yǎng)一只加菲貓,這里簡單的new出來了
Garfield garfield = new Garfield();
// 抱起加菲貓給它喂食
feed(garfield);
}
public static void feed(Garfield garfield) {
// 加菲貓吃東西
garfield.eat();
}
}
但是養(yǎng)了一段時(shí)間老奶奶覺得加菲貓老在家躺著沒什么意思,想念牧羊犬了,于是把加菲貓丟了換回牧羊犬,將原來Garfield garfield = new Garfield();改為
Shepherd shepherd = new Shepherd();
又過了一段時(shí)間老奶奶覺得不行,牧羊犬吃得太多了開銷頂不住,還是加菲貓好,于是他又把代碼改回來了,又將Shepherd shepherd = new Shepherd();改回
Garfield garfield = new Garfield();
我見老奶奶都一把年紀(jì)了,改來改去還挺麻煩的,就跟她說你要不定義一個(gè)Animal類的annimal變量代表你的寵物把,像這樣
Animal animal = new Garfield();
這樣換寵物只要改new后面的就行了,老奶奶一聽覺得很有道理,所以多態(tài)的另一個(gè)好處就是右邊的對象可以組件化切換,業(yè)務(wù)功能也會隨之改變
在我們開發(fā)中也常常使用多態(tài),大家回憶一下一個(gè)Service需要依賴其他Service,是不是這樣寫的
@Resource
private IUserServiceImpl userService;
總結(jié):多態(tài)的優(yōu)勢可以總結(jié)成兩個(gè)點(diǎn):方便入?yún)⒑蛯?shí)現(xiàn)組件化切換:
多態(tài)的形式
子類繼承父類
父類 變量名稱 = new 子類構(gòu)造器
實(shí)現(xiàn)類實(shí)現(xiàn)接口
接口 變量名稱 = new 實(shí)現(xiàn)類構(gòu)造器
多態(tài)的概念
看完上面的內(nèi)容,會有一種感覺,多態(tài)的風(fēng)格其實(shí)是定義變量的時(shí)候把類型范圍擴(kuò)大,如上面的例子,老奶奶以后都會把他的寵物們定義成這樣
Animal garfield = new Garfield();
Animal shepherd = new Shepherd();
定義加菲貓和牧羊犬的時(shí)候聲明的都是Animal類型,但他們的eat方法是不一樣的。同一種類型的對象執(zhí)行同一個(gè)行為(方法)會得到不同的結(jié)果,這個(gè)就是多態(tài)的概念
多態(tài)只是一種編程風(fēng)格,沒有要求一定要遵循,只是使用了多態(tài)會有他的好處,多態(tài)已經(jīng)成為大家公認(rèn)且遵守的Java特性,順著趨勢走就OK
多態(tài)的劣勢
這里有個(gè)小插曲,為什么老奶奶一開始會放棄加菲貓選擇牧羊犬,因?yàn)槟裂蛉梢詭兔醇?#xff0c;這是他的獨(dú)有功能
class Shepherd extends Animal{
private Integer i = 0;
@Override
public void eat() {
Systen.out.println("牧羊犬吃的很開心");
}
public void lookDDoor() {
Systen.out.println("這是牧羊犬的超能力");
}
}
但是他發(fā)現(xiàn)自從用了多態(tài)后,再也無法讓牧羊犬去看門了
public class Granny {
public static void main(String[] args) {
// 領(lǐng)養(yǎng)一只牧羊犬
Animal shepherd = new Shepherd();
// 看門
shepherd.lookDoor(); // 報(bào)錯(cuò)
}
}
大家可以先認(rèn)為Animal shepherd = new Shepherd();進(jìn)行了自動(dòng)轉(zhuǎn)型,shepherd已經(jīng)沒有看家的方法了,所以多態(tài)的劣勢就是子類失去了獨(dú)有的行為,而且連成員變量都不能直接訪問(只能借助重寫的方法去訪問)
public static void main(String[] args) {
// 領(lǐng)養(yǎng)一只加菲貓,這里簡單的new出來了
Garfield garfield = new Garfield();
garfield.i;// 報(bào)錯(cuò)
}
這時(shí)候需要使用強(qiáng)制類型轉(zhuǎn)換來解決問題,至于為什么不能調(diào)用子類的方法相信看完后面你就懂啦
多態(tài)存在的必然條件
必須存在繼承關(guān)系
必須是父類/接口類型變量引用子類/實(shí)現(xiàn)類類型變量
必須存在重寫方法
類型轉(zhuǎn)換
大家可以先記住語法,回頭就能理解轉(zhuǎn)換到底是在干嘛了
自動(dòng)轉(zhuǎn)換
Animal garfield = new Garfield();
子類類型會自動(dòng)轉(zhuǎn)換成父類類型,其實(shí)就是多態(tài)的默認(rèn)寫法
強(qiáng)制類型轉(zhuǎn)換
子類 新變量名稱 = (子類) 需要轉(zhuǎn)換的變量名稱
如
Animal garfield = new Garfield();
// garfield = (Garfield)garfield 必須用新的引用接收
Garfield garfield2 = (Garfield)garfield;
注意:必須使用新的變量去接收
強(qiáng)制類型轉(zhuǎn)換的時(shí)候需要對類型進(jìn)行判斷
在老奶奶養(yǎng)加菲貓和牧羊犬的時(shí)候有一個(gè)小插曲,加菲貓很貪吃,一頓要吃多點(diǎn),老奶奶也沒辦法,只能給他加餐,但是使用了多態(tài),喂貓喂狗的方法都是同一個(gè)`feed`,有沒有辦法可以判斷一下入?yún)⒌降资羌臃曝堖€是牧羊犬呢,那肯定是有的
public static void feed(Animal animal) {
// 判斷是不是加菲貓,是的話給他加餐
if (garfield instanceof Garfield) {
System.out.println("加餐");
animal.eat();
}
}
到底是加菲貓還是牧羊犬只有代碼運(yùn)行的時(shí)候才知道,intanceof可以判斷運(yùn)行引用animal的實(shí)際類型是否為Garfield類
多態(tài)的實(shí)現(xiàn)原理
一個(gè)對象變量可以指向多種實(shí)際類型的現(xiàn)象成為多態(tài),這導(dǎo)致一個(gè)對象變量調(diào)用同一個(gè)方法的時(shí)候得到了不同的結(jié)果。感覺非常抽象,看下面的例子
一只貓有兩個(gè)個(gè)eat方法,一個(gè)無參一個(gè)有參
class Cat {
public void eat() {
System.out.println("貓會吃飯")
}
public void eat(Integer weight) {
System.out.println("貓會吃飯,吃了" + weight)
}
}
當(dāng)主函數(shù)運(yùn)行以下代碼的時(shí)候
Cat cat = new Cat();
cat.eat();
cat.eat(10)
回想剛剛的概念,是不是同一個(gè)變量cat,調(diào)用同一個(gè)方法eat,但結(jié)果是不一樣。這就是編譯時(shí)多態(tài),在編譯成class文件的時(shí)候就可以確定,程序執(zhí)行的eat方法是Cat類中的成員方法,而且根據(jù)形參也可以知道是哪個(gè)eat方法,
方法簽名和返回參數(shù)相同看作同一個(gè)方法。這種形式成為方法重載(Overload)
再看下一種情況,貓類繼承了動(dòng)物類,重寫了動(dòng)物類的eat方法
ublic class Animal {
public void eat() {
System.out.println("動(dòng)物可以走路");
}
}
class Cat {
@Override
public void eat() {
System.out.println("貓會走路");
}
}
現(xiàn)在有一個(gè)feed喂養(yǎng)的方法,需要傳入一個(gè)動(dòng)物類型
public void feed(Animal animal) {
animal.eat();
}
在編譯的時(shí)候不能確定animal到底是什么類型的,可能是加菲貓可能是牧羊犬,準(zhǔn)確點(diǎn)應(yīng)該是計(jì)算機(jī)不知道animal實(shí)際是什么類型的,但程序員知道。這種就是我們最常用的多態(tài),叫運(yùn)行時(shí)多態(tài),由于不確定傳入的參數(shù)是什么類型的,同一個(gè)變量animal調(diào)用同一個(gè)方法eat產(chǎn)生的結(jié)果是不一樣的
多態(tài)的分類
根據(jù)上面的例子,多態(tài)可以分為
編譯時(shí)多態(tài)(靜態(tài)多態(tài))
運(yùn)行時(shí)多態(tài)(動(dòng)態(tài)多態(tài))
后面所提到的多態(tài)都是運(yùn)行時(shí)多態(tài)
運(yùn)行時(shí)多態(tài)的形式
就是上面提到過的那兩種
子類繼承父類
父類 變量名稱 = new 子類構(gòu)造器
實(shí)現(xiàn)類實(shí)現(xiàn)接口
接口 變量名稱 = new 實(shí)現(xiàn)類構(gòu)造器
實(shí)現(xiàn)原理
盡量用通俗的話去解釋,如果理解有誤麻煩評論區(qū)告訴我
常量值
大家肯定聽過,編譯器把源代碼編譯成class文件的時(shí)候,會把一些常量信息統(tǒng)一放在class文件的一塊區(qū)域,大家可以用字節(jié)碼分析工具隨便打開一個(gè)class文件就能看到c常量池了,這種寫在文件里面的常量信息被稱為靜態(tài)常量池,當(dāng)class文件被加載到虛擬機(jī)的時(shí)候,會在方法區(qū)開辟一段空間存放這些常量信息,這個(gè)區(qū)域就叫做運(yùn)行時(shí)常量池
常量池存了哪些信息
可以看下圖,其實(shí)很像我們的數(shù)據(jù)庫,
注意:因?yàn)?class文件還沒被加載,所以現(xiàn)在用分析工具展示的是靜態(tài)常量池,里面包含一些符號引用(就是一個(gè)名字),加載到方法區(qū)后會替換成直接引用(內(nèi)存地址)
CONSTANT_utf8_info
基本信息都存在CONSTANT_utf8_info,里面保存了這個(gè)類里面的成員方法的名字、我們定義的字符串常量(System.out.println(...)里面的字),引用類型類名(如Cat、Animal),變量名(如cat)等等
Length of bytes array; 6
length of String: 6
String: Animal
CONSTANT_Class_info
保存對其他類的符號引用(Class_name)和在CONSTANT_utf8_info的引用
Class_name:cp info #25
CONSTANT_NameAndType_info
保存方法和字段的類型和名稱,還有描述符信息(入?yún)⒑头祷刂?
Name: cp info #15
Descriptor: cp info #18
Name: cp info #28
Descriptor: cp info #10
里面的V表示返回值為空
CONSTANT_Methodref_info
保存方法的方法名稱的索引和該方法所屬的類名的索引,這個(gè)相當(dāng)于中間表
Class_name: cp info #22
Name_and_type: cp info #23
CONSTANT_interfaceMethod_info
和CONSTANT_Methodref_info類似,保存了接口方法的名稱和類型的索引和接口的索引
所有的表最終信息都保存在CONSTANT_utf8_info種,看上去就像我們的數(shù)據(jù)庫表設(shè)計(jì)一樣
方法調(diào)用方式
Java的方法調(diào)用方式有兩種,靜態(tài)調(diào)用和動(dòng)態(tài)調(diào)用
靜態(tài)調(diào)用
顧名思義,就是A類調(diào)用B類的靜態(tài)成員方法,也就是說調(diào)用的時(shí)候很明確,我要調(diào)用方法區(qū)里面那個(gè)叫B類的那個(gè)靜態(tài)方法,最后會把B類的靜態(tài)方法的字節(jié)碼地址替換運(yùn)行時(shí)常量池對應(yīng)的表符號引用,替換的過程稱為靜態(tài)綁定,調(diào)用綁定后的方法稱為靜態(tài)調(diào)用
StringUtils.isBlank();
類調(diào)用(invokestatic)在編譯的時(shí)候計(jì)算機(jī)已經(jīng)很明確要調(diào)那個(gè)方法了,只要類被加載到方法區(qū),一切都順利
注意:Java中只有被private、static、final修飾 的方法屬于靜態(tài)
動(dòng)態(tài)調(diào)用
如果要調(diào)用動(dòng)態(tài)成員變量的方法就比較麻煩了,必須先去堆中找到對應(yīng)的對象,然后根據(jù)對象的信息找到對應(yīng)的方法的字節(jié)碼地址,保存到堆中,對象中為什么會有方法的字節(jié)碼地址呢,這是動(dòng)態(tài)綁定完成的操作,具體后面再說,調(diào)用動(dòng)態(tài)綁定后的方法被稱為動(dòng)態(tài)調(diào)用
cat.eat();
實(shí)例調(diào)用(invokevirtual)就需要等到對象被創(chuàng)建的時(shí)候才能指定調(diào)用哪個(gè)方法
JVM調(diào)用方法的指令:
靜態(tài)調(diào)用:invokestatic、`invokespecial
動(dòng)態(tài)調(diào)用:invokeinterface、invokevirtual
實(shí)例化
這里需要說明的是,類如
Animal cat = new Cat();
這種形式對于cat來說他是Animal類型的,但在堆中開辟的是Cat類的對象空間,并由this指針指向Cat實(shí)例,所以cat的實(shí)際類型其實(shí)是Cat類
動(dòng)態(tài)綁定實(shí)現(xiàn)多態(tài)
子類繼承父類
方法表是在方法區(qū)中有一個(gè)集合,專門存放方法名稱和代碼指針,代碼指針指向存放方法體字節(jié)碼的內(nèi)存地址。這里需要強(qiáng)調(diào)的是,如果是子類重寫了父類的方法或者實(shí)現(xiàn)類實(shí)現(xiàn)了接口的方法,指針是指向重寫的方法的
如下面的代碼
public class Main {
public static void main(String[] args) {
Animal cat = new Cat();
cat.run();
}
}
class Animal {
public void play() {
System.out.println("父類方法");
}
public void run() {
System.out.println("父類方法");
}
public void eat() {
System.out.println("父類方法");
}
}
class Cat extends Animal {
@Override
public void run() {
System.out.println("子類方法");
}
}
對于Animal和Cat類,方法表是這樣的
當(dāng)調(diào)用Cat的run方法的時(shí)候,字節(jié)碼為invokevirtual #15,JVM先在常量池查CONSTANT_Methodref_info -> CONSTANT_NameAndType_info -> CONSTANT_utf8_info,查出來現(xiàn)在需要調(diào)用的是Animal類中run方法,然后去Animal的方法表里面找run方法,記錄以下偏移量offset,再調(diào)用invoke this,offset,這時(shí)候的this指針正指向的是堆中的Cat對象,Cat也有一張方法表,恰好數(shù)下來offset就是子類的run方法,于是找到Cat類的run方法的字節(jié)碼地址,順利調(diào)用。所以動(dòng)態(tài)調(diào)用的核心就在于這個(gè)方法表和this指針的設(shè)計(jì)
實(shí)現(xiàn)類實(shí)現(xiàn)接口
接口可以多繼承的,大家看下面的例子會發(fā)現(xiàn)用偏移量無法實(shí)現(xiàn)動(dòng)態(tài)調(diào)用
interface A {
public void a1();
public void a2();
public void a3();
}
interface B {
public void b1();
}
class TestA implements A{
// 重寫三個(gè)方法
}
class TestAB implements A, B {
// 從寫四個(gè)方法
}
public class Main {
B testAB = new TestAB();
testAB.b1();
}
很明顯接口B的b1方法的偏移量和實(shí)現(xiàn)類TestAB不一樣,所以JVM提供了invokeinterface方法,它不再使用偏移量,而是使用搜索的方式尋找合適的方法,所以調(diào)用接口的方法會比調(diào)用子類的慢
為什么不能調(diào)用子類中非重寫的方法
因?yàn)樵诟割惖姆椒ū韷焊蜎]與那個(gè)方法,例如上面的例子,如果run是Cat獨(dú)有的方法,在父類Animal中就沒有這個(gè)方法,就不能進(jìn)行動(dòng)態(tài)綁定了
那大家可以想一下強(qiáng)制類型轉(zhuǎn)換到底是在干嘛
寫在最后
寫這篇文章之前我是完全不知道多態(tài)是怎么實(shí)現(xiàn)的,我也是一邊查資料一邊研究,希望能幫助大家理解多態(tài)
總結(jié)
以上是生活随笔為你收集整理的java 多态判断非空_重拾JavaSE基础——多态及其实现方式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jmap 几个慎用操作
- 下一篇: 重拾Java基础知识:设计模式