日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

java 多态判断非空_重拾JavaSE基础——多态及其实现方式

發布時間:2024/1/8 java 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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基础——多态及其实现方式的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。