Bruce Eckel 前輩寫的《Java編程思想》把問題探討得很是深刻,很是建議同行都學習一下。學習 Java 語言時,老師告訴你 What,本身練習知道 How ,Bruce Eckel 告訴你Why 。java
以前已經把後面的幾章更新完了,之因此如今才寫第一章,徹底是爲了更加容易描述。這章須要不少其餘章節說到的概念。編程
第3章 操做符安全
第5章 初始化與清理編程語言
第7章 複用類函數
第8章 多態學習
在學習編程語言中,除非是最底層的 0101001 這些東西,必定離不開類型(type)。什麼是類型?類型就是「抽象的是什麼」。spa
「抽象的是什麼」這句話,其實很容易理解,咱們學習基礎數據類型時必會提到每種數據類型都佔據多少多少個字節,並且他們都是有範圍限制的,好比 byte 數據類型是8位、有符號的,以二進制補碼錶示的整數——這其實就是經過給二進制數字人爲的賦予定義來得到一個固定的表示內容來達到的,這個過程就是抽象:原本只是毫無心義的 0101001 這種東西,經過抽象,它可以變成任意咱們但願的東西。.net
Java 中八大基本數據類型就是創建在多層抽象上的,而面向對象,你們都說「萬物皆對象」,這句話的意思應該是「萬物皆可被抽象後描述,而不只僅限於一些基本的數據類型」,因此在 Java 中定義一個 class 時,咱們給 class 賦予類型屬性和行爲方法,經過這二者抽象後造成的類(class)就是類型(type)的意思,他們幾乎能夠等同看待。type 所抽象的東西還比較基本,因此說是基本數據類型。而 class 則所有都是對於外界事物的描述,對象則是某個類中的個體,每一個個體都擁有相同的特徵,因此他們屬於同一類(class)。因此類只是個概念,而對象纔是一個個活蹦亂跳的可使用操做的對象。設計
建立一個類,就是建立新的數據類型。對象
舉個例子暢想一下。int 是 Java 自己自帶的基本數據類型,它的描述是:32位、有符號的以二進制補碼錶示的整數,咱們能夠得到一堆的整數,它的類型是肯定的,行爲(運算)也被限定,帶有正負等。Java 中也定義了一個 Integer 的 class,這個 Integer 跟 int 有什麼關聯嗎?固然,它被設計來對應於基本數據類型 int,而且定義了不少操做方法。因此,若是忽略 int 類型的存在的話,Integer 類徹底就是一種數據類型,並且仍是一種「升級版」的。
封裝性很容易理解,就是把一堆東西用大括號包起來。封裝性做爲面向對象的三大特性之一,深層意義毫不僅限於這個淺顯的概念。
上面說到類與類型,類型就是「抽象的是什麼」。類呢?就是「對萬物的抽象後封裝其描述」。因此我在上面總結說:建立一個類,就是建立新的數據類型。
封裝了什麼描述呢?每一個類中的成員變量都是對這個類抽象後的描述。
比方說,我要建立一個類:人,那麼我以爲人應該要有名字、性別。若是實際狀況不須要知道或用到其餘的屬性,我就不會建立其餘的成員變量來描述「人」這個類,而只有兩個成員變量。抽象就是:我只描述我想要的。
抽象了一個類(型)以後,在 Java 裏就是用花括號包起來,就封裝成了一個真正的類(型概念)。
封裝性是面向對象的最基本的特性。
同上面同樣,繼承的概念我也不想多說,只是說一些我認爲起到點睛效果的點。
在 Java 裏的繼承不是真正意義上的或者純概念上的繼承,它是經過讓派生類得到基類的一個子對象來得到看起來是繼承的樣子(效果)的。這個子對象等同於咱們手動 new 一個對象,這就是爲何咱們在寫每個派生類的任意構造函數時都須要確保可以調用到基類的任一個構造函數且只能調用一個構造函數的緣由。
——這句話有點拗口,可是不難理解,這也是考察Java基礎時常考到的知識點——給出幾個類你,他們之間有繼承關係,問你編譯運行後的輸出結果是什麼,一般結果都是發生編譯時異常,由於代碼中每每會經過很隱祕的方法讓派生類最終並不能調用到基類的構造函數,這樣的結果就是派生類沒辦法生成並獲取基類的子對象,那麼繼承所必需的代碼就不完整,天然就在編譯的時候就發生異常了。
理解這一點以後發現,所謂的繼承其實能夠經過手動的方式完成幾乎相同(並不徹底相同)的效果,你確定猜到了,那就是直接 new 另外一個類的對象,讓它成爲本身的類的成員變量——你必定常常這樣作。這樣的結果就是,你得到那個類的所有訪問權限容許的屬性和方法。
經常有人出這樣的題目來嚇唬人,就是有繼承關係的兩個類,基類有個私有變量a(String型),派生類能不能使用基類的a變量?答案確定是不能的,系統提示沒有訪問權限。若是是真的概念上的繼承的話,派生類應該得到基類的全部元素纔對啊,爲何說沒有權限呢?緣由也是上述的:這僅僅是經過new一個類的對象來訪問而已,天然是不能直接操做對象的聲明爲私有的任何東西的。
真正的繼承,固然就是說全部東西都是屬於派生類纔對的。
假設這時候子類也有一個私有變量a(String型)。能不能訪問到呢?這兩個變量是什麼關係呢?這時候變成能夠訪問了,由於他們都是分別屬於兩個類的成員變量,互相獨立,他們之間沒有任何的關係,這時候其實就是訪問本身的私有變量,固然沒有問題了。
因此說手動的、直接 new 一個對象,也是一種「繼承」,這種方式反而更加靈活方便,它有個專門的名詞叫作「組合」。組合與繼承之間糾纏着的愛恨情仇大抵如上。同時,在寫代碼的時候,一般是優先使用組合而不是繼承。
那麼何時使用繼承呢?繼承雖然相比組合來講比較笨重不靈活(好比不能多繼承能夠多組合等),可是繼承的魅力仍是不小的,好比多態等。因此當你以爲派生類有向上(基類)轉型的必要時,使用繼承。
多態依賴於繼承,組合是沒辦法完成多態的,這就是優缺點——有得必有失,看你的取捨。
多態的概念也很少說,由於這文章並非知識普及用的,是思想提高用的,若是連相關知識點都沒有掌握的話,估計你也不會看到這裏了。
多態有個類型轉換的問題:向上轉型和向下轉型。向上轉型是永遠不會出錯的,同時意味着派生類丟失基類並無的部分信息。而向下轉型原則上是不容許的或者是不建議的,隨意的、直接的向下轉型,在編譯器就會報錯。若是確實有須要,則須要強制轉型:因此在 Java 裏全部的向下轉型都必須顯式聲明,當顯式聲明時,就意味着你已經瞭解這種風險並願意承擔其帶來的問題等。
這裏有個問題:爲何向上轉型是安全的,向下轉型則是危險的?若是你把它看成一個知識點去學習並記住了,那麼你爲何不會好奇其背後的原因呢?這裏就想回答這個背後的緣由。
上面說繼承的時候說過,Java 中的繼承並非咱們概念上所理解的真正的繼承,若是子類繼承了父類,那麼當你 new 了一個子類對象時,其實在 Java 的底層會同時幫你 new 一個父類的對象做爲子類的子對象,這時候在Java裏想要向上轉型時,經過這個父類的子對象很容易就知道了這個繼承關係,轉型天然是安全的。
而若是 new 了的是父對象,再向下轉型成子對象時,這樣在編譯期就會發生異常——Java 裏是不容許這樣的隨意向下轉型的行爲的。因此你須要在父對象的前面顯式聲明說:我要強制轉型。編譯器才能經過編譯。
可是,在上面這種狀況,即便你在轉換的時候已經在前面的小括號裏聲明瞭類型來強制轉型,讓本身的代碼在編譯時可以經過了,運行時仍是會出現異常的:ClassCastException,由於這時候JVM發現這個父對象根本沒有任何強轉類型(子類)的信息。爲了不這種運行時異常一般須要去確認一下是否是同一個類型:instanceof 判斷經過後再進行轉換,就能確保代碼不會出現異常。
有一種向下轉型不會出現運行時異常 ClassCastException 的狀況,那就是一開始 new 的是子類的對象,而後賦值給一個父類的引用,而後再將這個父類引用的對象強制轉型爲子類對象,這時候的強轉是成功的——這背後的緣由也很容易理解:new 建立的全部對象都是存放在堆內存區的,而引用則存放在棧內存區,它保存的只是堆內存中那個對象的開始地址而已。由於一開始 new 的就是子類的對象,因此這個對象是不只擁有父類的子對象,並且擁有自身的對象的。這時候它無論是向上轉型仍是向下強制轉型,都是不會有問題的。
下面是一個類似的例子:
第25行代碼會在運行時拋出ClassCastException,java.lang.Integer cannot be cast to java.lang.Double。這兩個類都繼承自Number抽象類,因此他們的向上轉型都不會有任何問題而且不須要強轉,可是向上轉型後再向下轉型時,拋出的異常依然能識別到建立對象時的類,強轉是失敗的。
這就是爲何非要作類型判斷後才能進行強轉,因此註釋掉的28到33行代碼纔是正確的作法。
同時,在Java裏有個 Class 類,JVM 每加載一個類,Class 類都會新建一個對象,用於記錄它的類型信息,Java 的反射機制也是基於它。
本文講述的主要是如下幾點:
- 語言創建在抽象之上,基本數據類型是語言根據須要抽象出來的經常使用類型(type),
- 對象則是更高層次的抽象,任何人均可以根據須要,本身抽象外界後封裝成一種新的數據類型(class)。
- 在 Java 裏,最終仍是經過子類所擁有的祖先子對象來得到繼承、類型轉換、多態等能力的。
- 向上轉型安全和向下轉型不安全的背後緣由,都是基於Java內部的繼承機制才存在的問題。
若是理解了上述幾個問題,就會對 Java 中的不少看成死記硬背的「知識點」恍然大悟了,之後這些在你眼裏就不再是須要記憶的、知其然不知其因此然的、知識點了,而是一個個邏輯清晰的、一切皆有原因的語言準則了。
BTW:有興趣的朋友可加羣(302659873)交流技術。