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