Java 面向對象一覽
本文轉載自微信公眾號「蝸牛互聯網」,作者白色蝸牛。轉載本文請聯系蝸牛互聯網公眾號。
本文大綱:
前言
學 Java 的朋友都知道,Java 是一門典型的面向對象的高級程序設計語言,但有些朋友可能不清楚面向對象在 Java 中是怎么體現的。這篇文章就向大家分享下 Java 在面向對象方面的一些知識。
Java 語言簡介
Java 語言特點
首先我們看下 Java 的語言特點,如圖所示。
Java 是純粹的面向對象語言,它因統一的字節碼文件和差異化的 JDK 而具有平臺無關的特性。
Java 內置豐富的類庫,使開發者效率大為提升。它支持 web,廣泛應用于各大互聯網企業的網站后臺,像阿里美團都在使用。
Java 的安全性也很出眾,通過沙箱安全模型保證其安全性,能夠有效防止代碼攻擊。
Java 也具備很強的健壯性,比如它是強類型的,支持自動化的垃圾回收器,有完善的異常處理機制和安全檢查機制。
與 C++ 比較
比較點 | C++ | Java |
---|---|---|
語言類型 | 編譯型語言 | 解釋編譯混合型語言 |
執行速度 | 快 | 慢 |
是否跨平臺 | 否 | 是 |
面向對象 | 面向對象和面向過程混合 | 純面向對象 |
指針 | 有 | 無 |
多繼承 | 支持 | 不支持 |
內存管理 | 手動 | 自動 |
從語言類型上看,C++ 的代碼編譯好,就能被計算機直接執行,它是編譯型語言,而 Java 經過 javac 把 java 文件編譯成 class 文件后,還需要 JVM 從 class 文件讀一行解釋執行一行,它是解釋編譯混合型語言。也就是中間多了 JVM 這一道,Java 也具備了跨平臺特性,而 C++ 就沒有這個優勢。
從面向對象的角度上看,C++ 是在 C 的基礎上的新的探索和延伸,因此它是面向對象和面向過程混合的,而 Java 就是純粹的面向對象。
此外,C++ 有指針的概念,Java 沒有。C++ 支持多繼承,Java 不支持。C++ 需要手動進行內存管理,Java 通過垃圾回收機制實現了內存的自動管理。
面向對象思想
我們總在提面向對象,那面向對象究竟是個什么東西呢?在面向對象出現之前的面向過程又是怎么回事呢?
其實無論是面向對象還是面向過程,都是我們在編程時解決問題的一種思維方式。
只是在最初,人們分析解決問題的時候,會把所需要的步驟都列出來,然后通過計算機中的函數把這些步驟挨個實現,這種過程化的敘事思維,就是面向過程思想。
你比如,把一頭大象放進冰箱,通常會怎么做呢?
我們的習慣性思維是會分為三步,第一步,把冰箱門打開,第二步,把大象推進去,第三步,把冰箱門關閉(假設大象很乖,冰箱很大,門能關住)。
這種方式固然可行,但當場景發生變化時,比如大象變成豬,冰箱變成衣柜,類似的步驟用面向過程編碼的話就要再寫一遍。這樣就導致代碼開發變成了記流水賬,久而久之就成為面條代碼。
我們仔細分析面向過程的這些步驟,會發現都是命令式的動賓結構:開冰箱門,推大象,場景切換下就是開衣柜門,推豬。你會發現從這兩種場景下是可以找到共性的,就是冰箱門和衣柜門都有打開和關閉的特點,大象和豬都能走路,所以能被人推進去。
當我們的視角不再是流程,而是操作對象的時候,冰箱門和衣柜門都可以抽象成門,有打開和關閉的特點,大象和豬都可以抽象成動物,有走路的特點。按這個思路,我們可以把這件事簡化成主謂結構:門打開,動物走進去,門關閉。
這種把事情分解成各個對象,描述對象在整個事情中的行為,就是面向對象思想。
你會發現,面向過程更講事情的步驟,面向對象更講對象的行為。
面向對象可以基于對象的共性做抽象,為軟件工程的復用和擴展打好了堅實的基礎。這也是為什么在很多大型軟件開發選型上,大多會使用面向對象語言編程。
面向對象基礎
Java 作為純面向對象語言,我們有必要了解下面向對象的基礎知識。
面向對象有四大特征,是抽象,封裝,繼承和多態。也有很多人認為是三大特征,不包括抽象,但我覺得抽象才是面向對象思想最為核心的特征,其他三個特征無非是抽象這個特征的實現或擴展。
我總結了下這四大特征在面向對象領域分別解決了什么問題,再逐一介紹:
- 抽象:解決了模型的定義問題。
- 封裝:解決了數據的安全問題。
- 繼承:解決了代碼的重用問題。
- 多態:解決了程序的擴展問題。
抽象
抽象是面向對象的核心特征,良好的業務抽象和建模分析能力是后續封裝、繼承和多態的基礎。
面向對象思維中的抽象分為歸納和演繹兩種。
歸納是從具體到本質,從個性到共性,將一類對象的共同特征進行歸一化的邏輯思維過程。比如我們把見到的像大象,老虎,豬這些能動的有生命的對象,歸納成動物。
演繹是從本質到具體,從共性到個性,將對象逐步形象化的過程。比如從生物到動物,從動物到鳥類。演繹的結果不一定是具體的對象,也可以是像鳥類這種抽象結果,因此演繹仍然是抽象思維,而非具象思維。
Java 中的 Object 類是任何類的默認父類,是對萬物的抽象。這就是我們常說的:萬物皆對象。
看一看 java.lang.Object 類的源碼,我們基本能看到 Java 世界里對象的共同特征。
getClass() 說明了對象是誰,toString() 是對象的名片,clone() 是繁殖對象的方式, finalize() 是銷毀對象的方式,hashCode() 和 equals() 是判斷當前對象與其他對象是否相等的方式,wait() 和 notify() 是對象間通信與協作的方式。
類的定義
除了 JDK 中提供的類之外,我們也可以基于自己業務場景的抽象定義類。
我們看下 Java 語法中的 class(類)是怎么構成的。
以下是概覽圖,我們按圖介紹。
我們先關注圖中的黃色區塊,在 Java 里就叫 class(類)。
好比一個事物有屬性和能力一樣,比如人有名字,人能吃飯。對應到 Java class 里就是變量和方法,即紅色區塊和紫色區塊。
變量分為成員變量,靜態變量和局部變量三種,方法分為構造方法、實例方法和靜態方法三種。
我們舉個例子來說明下,假設全世界的面包數量就 100 個,并且生產已經停滯,而且只有蝸牛和小白兩個人能吃到,我們就可以按以下的代碼來描述這兩個人吃面包的過程以及面包的情況。
- package cn.java4u.oo;
- /**
- * @author 蝸牛
- * @from 公眾號:蝸牛互聯網
- */
- public class Person {
- /**
- * [成員變量]需要被實例化后使用,每個實例都有獨立空間,通過 對象.成員變量名 訪問
- * 名字
- */
- String name;
- /**
- * [靜態變量]用 static 修飾,無需實例化即可使用,每個實例共享同一個空間,通過 類名.靜態變量名 訪問
- * 面包數量
- */
- static int breadNum;
- /**
- * [方法]
- * 吃一個面包
- *
- * @param num 方法入參,要吃面包的個數
- */
- void eatBread(int num) {
- // num 是[局部變量]
- breadNum = breadNum - num;
- System.out.println(name + "吃了 " + num + " 個面包,全世界的面包還剩 " + breadNum + " 個!");
- }
- /**
- * [構造方法]
- * 參數為空
- */
- public Person() {
- }
- /**
- * [構造方法]
- *
- * @param name 此為構造方法的輸入參數,和成員變量有關
- */
- public Person(String name) {
- this.name = name;
- }
- /**
- * [靜態方法]
- */
- static void testStaticMethod() {
- // 通過構造方法,初始化名字叫蝸牛的人
- Person woniu = new Person("蝸牛");
- // 通過構造方法,初始化名字叫小白的人
- Person xiaobai = new Person("小白");
- // 假設全世界的面包數量就 100 個,并且生產已經停滯
- Person.breadNum = 100;
- // 蝸牛吃五個面包
- woniu.eatBread(5);
- // 小白吃六個面包
- xiaobai.eatBread(6);
- // 打印成員變量和靜態變量的值
- System.out.println(woniu.name + "和" + xiaobai.name + "吃飽后,世界只剩 " + Person.breadNum + " 個面包了!");
- }
- }
變量
首先定義了一個名字叫 Person 的類,表示人,然后定義了一個成員變量 name ,表示人的名字。成員變量也叫實例變量,實例變量的特點就是,每個實例都有獨立的變量,各個實例之間的同名變量互不影響。
其次定義了一個靜態變量 breadNum ,表示面包的數量,靜態變量用 static 修飾。靜態變量相對于成員變量就不一樣了,它是共享的,所有實例會共享這個變量。
方法
再接著定義了一個返回值為空,只有一個入參的方法 eatBread(int num) ,方法入參 num 作為局部變量參與了內部的運算,通過和它的運算,靜態變量breadNum 的值得到了更新,并打印了一行操作信息。方法的語法結構如下:
- 修飾符 返回類型 方法名(方法參數列表) {
- 方法語句;
- return 方法返回值;
- }
另外定義了 Person 的構造方法,你會發現構造方法和實例方法的區別就在于它是沒有返回值的,因為它的目的很純粹,就是用來初始化對象實例的,和 new 搭配使用,所以它的方法名就是類名,它的入參也都和成員變量有關。
到這里,你會發現 Java 方法的返回值并不是那么重要,甚至沒有都可以!是的,Java 方法簽名只包括名稱和參數列表,它們是 JVM 標識方法的唯一索引,是不包含返回值的,更不包括各種修飾符或者異常類型。
請注意,任何 class 都是有構造方法的,即便你代碼里不寫,Java 也會在編譯 class 文件的時候,默認生成一個無參構造方法。但是只要你手動定義了構造方法,編譯器就不會再生成。也就是說如果你僅定義了一個有參的構造方法,那么編譯后的 class 是不會有無參構造方法的。
最后就是靜態方法了,名字叫testStaticMethod ,方法內部我們先用 new 的語法調用構造方法,初始化了蝸牛和小白的Person 對象。這兩個對象就是 Person 這個類的實例,這兩個實例都有獨立空間,name 這個成員變量也只能在被實例化后使用,可以通過 對象.成員變量名 訪問。
接著我們通過 Person.breadNum 也就是 類名.靜態變量名 的方式,更新了面包數量這個值。你會發現 breadNum 這個靜態變量無需實例化就能使用,因為就這個變量而言,Person 的每個實例都會共享同一個空間。這意味著,每個實例的修改,都會影響到這個變量值的變化。
然后我們通過調用方法 eatBread 并傳參的方式,影響到了面包數的值。
- package cn.java4u.oo;
- /**
- * @author 蝸牛
- * @from 公眾號:蝸牛互聯網
- */
- public class MainTest {
- public static void main(String[] args) {
- // 靜態方法,通過 類名.靜態方法名 訪問
- Person.testStaticMethod();
- }
- }
最后我們新定義一個觸發調用的入口函數,通過 Person.testStaticMethod() 這樣 類名.靜態方法名 的方式就能訪問到靜態方法了。
抽象類與接口
抽象類顧名思義,就是會對同類事物做抽象,通常包括抽象方法、實例方法和成員變量。被抽象類和抽象類之間是 is-a 關系,這種關系要符合里氏替換原則,即抽象類的所有行為都適用于被抽象類,比如大象是一種動物,動物能做的事,大象都能做。代碼定義也很簡單,就是在 class 和抽象方法上加 abstract 修飾符。
- package cn.java4u.oo;
- /**
- * 抽象類
- *
- * @author 蝸牛
- * @from 公眾號:蝸牛互聯網
- */
- public abstract class AbstractClass {
- String name;
- /**
- * 實例方法
- *
- * @return name
- */
- public String getName() {
- return name;
- }
- /**
- * 抽象方法-操作
- *
- * @return 結果
- */
- public abstract String operate();
- }
如果一個抽象類只有一個抽象方法,那它就等于一個接口。接口是要求被普通類實現的,接口在被實現時體現的是 can-do 關系,它表達了對象具備的能力。鳥有飛的能力,宇宙飛船也有飛的能力,那么可以把飛的能力抽出來,有單獨的一個抽象方法。代碼定義也比較簡單,class 的關鍵字用 interface 來替換。
- package cn.java4u.oo;
- /**
- * 可飛翔
- *
- * @author 蝸牛
- * @from 公眾號:蝸牛互聯網
- */
- public interface Flyable {
- /**
- * 飛
- */
- void fly();
- }
內部類
在 Java 源代碼文件中,只能定義一個類目與文件名完全一致的公開類。如果想在一個文件里定義另外一個類,在面向對象里也是支持的,那就是內部類。
內部類分為以下四種:
- 靜態內部類:static class StaticInnerClass {}
- 成員內部類:private class InstanceInnerClass {}
- 局部內部類:class MethodClass {} ,定義在方法或者表達式內部
- 匿名內部類:(new Thread() {}).start();
示例代碼如下:
- package cn.java4u.oo.innerclass;
- /**
- * 內部類演示
- *
- * @author 蝸牛
- * @from 公眾號:蝸牛互聯網
- */
- public class InnerClassDemo {
- /**
- * 成員內部類
- */
- private class InstanceInnerClass {}
- /**
- * 靜態內部類
- */
- static class StaticInnerClass {}
- public static void main(String[] args) {
- // 兩個匿名內部類
- (new Thread() {}).start();
- (new Thread() {}).start();
- // 方法內部類
- class MethodClass {}
- }
- }
編譯后得到的 class 文件如下:
我們會發現,無論什么類型的內部類,都會編譯生成一個獨立的 .class 文件,只是內部類文件的命名會通過 $ 連接在外部類后面,如果是匿名內部類,會使用編號來標識。
類關系
關系是指事物之間有沒有單向或者相互作用或者影響的狀態。
類和類之間的關系分為 6 種:
- 繼承:extends(is-a)
- 實現:implements(can-do)
- 組合:類是成員變量(contains-a)
- 聚合:類是成員變量(has-a)
- 依賴:單向弱關系(使用類屬性,類方法、作為方法入參、作為方法出參)
- 關聯:互相平等的依賴關系(links-a)
序列化
內存中的數據對象只有轉換為二進制流才可以進行數據持久化和網絡傳輸。
將數據對象轉換成二進制流的過程稱為對象的序列化(Serialization)。
將二進制流恢復為數據對象的過程稱為反序列化(Deserialization)。
常見的序列化使用場景是 RPC 框架的數據傳輸。
常見的序列化方式有三種:
- Java 原生序列化。特點是兼容性好,不支持跨語言,性能一般。
- Hessian 序列化。特點是支持跨語言,性能高效。
- JSON 序列化。特點是可讀性好,但有安全風險。
封裝
封裝是在抽象基礎上決定信息是否公開,以及公開等級,核心問題是以什么樣的方式暴露哪些信息。
抽象是要找到成員和行為的共性,成員是行為的基本生產資料,具有一定的敏感性,不能直接對外暴露。封裝的主要任務是對成員、數據、部分內部敏感行為實現隱藏。
對成員的訪問與修改必須通過定義公共的接口來進行,另外某些敏感方法或者外部不需要感知的復雜邏輯處理,一般也會進行封裝。
像智能音箱,與用戶交互的唯一接口就是語音輸入,封裝了內部的實現細節和相關數據。
設計模式七大原則之一的迪米特法則也說明了封裝的要求,A 接口使用 B 接口,對 B 知道的要盡可能少。
包
包(package)這個名稱就很明顯體現了封裝的含義,它能起到把一個模塊封裝到一起,并由幾個接口開放給使用方。使用方只能看到接口信息,而看不到接口實現。另外包解決重名問題,相同類名在相同路徑下是不允許的,切換包路徑就可以起相同的類名。
訪問權限控制
我們編寫的程序要想讓使用方,能看到一些信息,又不能看到另外一些信息,這就涉及到信息隱藏了。
信息隱藏是面向對象程序設計的重要特點之一,它可以防止類的使用者意外損壞數據,對任何實現細節所作的修改不會影響到使用該類的其它代碼,也使類更易于使用。
那在 Java 里,實現信息隱藏的就是訪問權限控制機制了。Java 的訪問權限控制有 4 個訪問修飾符:public 、protected 、private 和缺省。可以使用這四個訪問修飾符修飾類的成員,它們在不同位置的可訪問性如下表所示。
位置\訪問修飾符 | public | protected | 缺省 | private |
---|---|---|---|---|
本類 | 可以 | 可以 | 可以 | 可以 |
本包 | 可以 | 可以 | 可以 | 不可以 |
子類 | 可以 | 可以 | 不可以 | 不可以 |
所有 | 可以 | 不可以 | 不可以 | 不可以 |
你會發現 public 不受任何限制,本類和非本類都可以隨意訪問(全局友好)。protected 本類及其子類可以訪問(父子友好),同一個包中的其它類也可以訪問(包內友好)。而缺省的時候,只有相同包中的類可以訪問(包內友好)。private 只有本類可以訪問,其余都不可以(類內友好)。
除了為類成員添加訪問權限控制外,也可以在定義類的時候,為類添加訪問修飾符,對類進行訪問權限控制。不過對類使用的訪問修飾符只有 public 和缺省兩種,訪問范圍也分別是全局友好和包內友好。
getter 與 setter
為了讓類成員不對外直接暴露,我們經常把成員變量的訪問權限設置成 private,而成員值的訪問與修改使用相應的 getter/setter 方法。而不是對 public 的成員進行讀取和修改。
- package cn.java4u.oo.packagedemo;
- /**
- * getter 和 setter 演示
- * @author 蝸牛
- * @from 公眾號:蝸牛互聯網
- */
- public class GetterSetterDemo {
- /**
- * 成員變量私有化
- */
- private String name;
- /**
- * 公開方法獲取成員變量值
- *
- * @return 名稱
- */
- public String getName() {
- return name;
- }
- /**
- * 公開方法設置成員變量值
- *
- * @param name 名稱
- */
- public void setName(String name) {
- this.name = name;
- }
- }
繼承
類繼承
class 了解之后,我們考慮一個問題。如果兩個 class,它們的變量和方法基本相同,僅僅是其中一個 class 會有一些自己特有的變量和方法,那么相同的那些變量和方法真的需要在兩個 class 里都寫一遍么?
比如一個表示學生的 class Student ,它相對于 class Person 只是多了一個分數 score 的成員變量,那還需要像下面這樣,把 name 字段也定義一下么?
- /**
- * 學生
- *
- * @author 蝸牛
- * @from 公眾號:蝸牛互聯網
- */
- public class Student {
- /**
- * 名字
- */
- String name;
- /**
- * 分數
- */
- int score;
- }
這很明顯帶來了代碼重復使用的問題!那能不能在 Student 中不寫重復代碼?
Java 里的繼承這時候就派上用場了,繼承是面向對象編程的一種強大機制,能夠讓子類繼承父類的特征和行為,使得子類對象能夠具有父類的實例變量和方法。
子類繼承父類,父類派生子類。父類也叫基類,子類也叫派生類。
通常來講,類的層次劃分總是下一層比上一層更具體,并且包含上一層的特征,這樣下層的類就能自動享有上層類的特點和性質。繼承就是派生類自動地共享基類中成員變量和成員方法的機制。
在 Java 中,通過 extends 關鍵字實現繼承,并且所有的類都是繼承于 java.lang.Object ,所以這就是萬物皆對象在 Java 里的真實寫照。你可能會疑惑,自定義的類并沒有 extends 關鍵字為什么還能繼承 Object 呢?這是因為這個類在 java.lang 包里,Java 已經默認支持了。
- package cn.java4u.oo;
- /**
- * 學生
- *
- * @author 蝸牛
- * @from 公眾號:蝸牛互聯網
- */
- public class Student extends Person {
- /**
- * 分數
- */
- int score;
- }
知道了繼承的基礎概念后,我們看下繼承有啥作用?
首先,繼承是能夠自動傳播代碼和重用代碼的有力工具。它能在已有類上擴充新類,減少代碼的重復冗余,也因為冗余度降低,一致性就得到了增強,從而提升了程序的可維護性。
其次,繼承可以清晰體現出類與類之間的層次結構關系,提升了代碼的可讀性。
另外,繼承是單方向的,即派生類可以繼承和訪問基類成員,但反過來就不行。而且 Java 只允許單一繼承,也就是一個派生類不能同時繼承多個基類,這和 C++ 是不同的。
在使用繼承的時候,還要考慮到基類成員的訪問控制權限。可以參考封裝那塊內容的訪問權限控制介紹。
子類實例化過程
特別要說明的是,父類的構造方法是不能被子類繼承的,即便它是 public 的。父類的構造方法負責初始化屬于它的成員變量,而子類的構造方法只需考慮自己特有的成員變量即可,不必關注父類狀況。
- package cn.java4u.oo.inherit;
- /**
- * 定義父類
- *
- * @author 蝸牛
- * @from 公眾號:蝸牛互聯網
- */
- public class Parent {
- /**
- * 構造方法
- */
- public Parent() {
- System.out.println("這是父類 Parent 的構造方法");
- }
- }
- package cn.java4u.oo.inherit;
- /**
- * 定義子類
- *
- * @author 蝸牛
- * @from 公眾號:蝸牛互聯網
- */
- public class Child extends Parent {
- /**
- * 構造方法
- */
- public Child() {
- System.out.println("這是子類 Child 的構造方法");
- }
- }
- package cn.java4u.test;
- import cn.java4u.oo.inherit.Child;
- /**
- * @author 蝸牛
- * @from 公眾號:蝸牛互聯網
- */
- public class InheritTest {
- public static void main(String[] args) {
- Child child = new Child();
- }
- }
因此,在實例化子類的對象時,Java 先是執行父類的構造方法,然后執行子類的構造方法。如果父類還有更上級的父類,就會先調用更高父類的構造方法,再逐個依次地將所有繼承關系的父類構造方法全部執行。如果父類的構造方法執行失敗,則子類的對象也將無法實例化。
上邊的代碼運行后,會輸出:
- 這是父類 Parent 的構造方法
- 這是子類 Child 的構造方法
this 與 super
如果調用父類構造方法涉及到有參構造方法,可以使用 super 關鍵字來調用父類構造方法并傳遞參數。
說的 super,它還有一個能力,就是父類和子類的成員如果同名了,子類中默認只能訪問自己的那個成員,想要訪問父類成員,就可以通過 super.成員名 的語法實現。但這有個前提,就是父類的這個成員不能是 private 的。
與 super 相對的關鍵字是 this ,super 是指向當前對象的父類,而 this 是指向當前對象自己。this 常用來區別成員變量和局部變量,比如下面這段代碼,我加了個有參構造方法。
- public class Parent {
- int a;
- /**
- * 構造方法
- */
- public Parent() {
- System.out.println("這是父類 Parent 的構造方法");
- }
- public Parent(int a) {
- this.a = a;
- }
- }
多態
說完繼承,我們再來聊聊多態!
多態字面上解釋,就是程序可以有多個運行狀態。
既然是運行狀態,那其實更多的是強調方法的使用。
重載與覆寫
方法在兩種情況下使用會比較特別,一種是 overload(重載),overload 方法是本類內的新方法,方法名一樣,但是參數的類型或數量不同。這種方法沒有特殊的標識,通過類內方法是否重名判定。
另外一種就是 override(覆寫),override 方法是繼承關系下子類的新方法,方法簽名和父類完全相同。這種方法都會有 @Override 注解的標識。
- package cn.java4u.oo.polymorphism;
- /**
- * 動物
- *
- * @author 蝸牛
- * @from 公眾號:蝸牛互聯網
- */
- public class Animal {
- /**
- * 與 eat(String food) 重載
- */
- public void eat() {
- System.out.println("Animal.eat");
- }
- /**
- * 與 eat() 重載
- *
- * @param food 食物
- */
- public void eat(String food) {
- System.out.println("Animal.eat: " + food);
- }
- /**
- * 覆寫
- *
- * @return 字符串
- * @see java.lang.Object#toString
- */
- @Override
- public String toString() {
- return "Animal " + super.toString();
- }
- }
舉個例子,Animal 類里兩個 eat 方法就互為重載方法,toString 方法就是相對于父類方法 java.lang.Object#toString 的覆寫方法。
多態就發生在覆寫這種場景下。針對某個類型的方法調用,它真正執行的方法取決于運行時期實際類型的方法。比如下面這段代碼,當聲明類型為 Object ,初始化類型為 Animal 時,你覺得輸出的是 Animal 的 toString 方法,還是 Object 的 toString 方法?
- package cn.java4u.oo.polymorphism;
- /**
- * @author 蝸牛
- * @from 公眾號:蝸牛互聯網
- */
- public class PolymorphismTest {
- /**
- * 打印對象
- *
- * @param scene 打印場景
- * @param obj obj
- */
- public static void printObjectString(String scene, Object obj) {
- System.out.println(scene + ": " + obj.toString());
- }
- public static void main(String[] args) {
- // 父類引用初始化父類對象并打印
- Object rootObj = new Object();
- printObjectString("父類引用初始化父類對象", rootObj);
- // 子類引用初始化子類對象并打印
- Animal animal = new Animal();
- printObjectString("子類引用初始化子類對象", animal);
- // 父類引用初始化子類對象并打印
- Object animalWhenParentRef = new Animal();
- printObjectString("父類引用初始化子類對象", animal);
- }
- }
答案是子類 Animal 的 toString 方法!
- 父類引用初始化父類對象: java.lang.Object@60e53b93
- 子類引用初始化子類對象: Animal cn.java4u.oo.polymorphism.Animal@5e2de80c
- 父類引用初始化子類對象: Animal cn.java4u.oo.polymorphism.Animal@5e2de80c
實際類型為 Animal 引用類型為 Object ,調用 toString 方法時,實際上是子類的。因此我們可以得出結論:Java 的實例方法調用是基于運行時的實際類型的動態調用,而非變量的聲明類型。這種特性就是多態!
你會發現 printObjectString 方法的第二個參數,即便聲明的是 Object ,實際運行的時候,卻可以是它的子類覆寫方法。
至此,我們也理出了 Java 實現多態三要素,那就是 繼承、覆寫和向上轉型。即兩個類之間有繼承關系,某個類覆寫了父類的某個方法,方法的引用會指向子類的實現處。
總結
本文從 Java 的視角出發,分析了 Java 的語言特點,并和 C++ 進行了比較。針對這門典型的面向對象語言,我們又分析了面向對象的概念和思想。接著基于面向對象的特征:抽象、封裝、繼承和多態,我們又詳細的分析了在 Java 中的體現方式,并伴有很多樣例代碼輔助學習。看完這篇文章,想必你對面向對象這個東西會有更全面的了解。
好啦,本期的分享就到這里,如果各位喜歡我的分享,請務必三連,點贊,在看,收藏,關注我,這會對我有非常大的幫助。
我們下期再見。