摘要:Java語言有什麼特點?如何最大效率的學習?深淺拷貝到底有何區別?阿里巴巴高階開發工程師為大家帶來Java系統解讀,帶你掌握Java技術要領,突破重點難點,入門物件導向程式設計,以詳細示例帶領大家Java基礎入門!
演講嘉賓簡介:
邢凱航(花名:弗止),阿里巴巴Java高階開發工程師,香港大學電腦科學碩士,16年加入阿里巴巴,目前就職於研發效能事業部使用者聲音及程式碼智慧化團隊,負責程式碼中心後端開發。
以下內容根據演講嘉賓視訊分享以及PPT整理而成。
本文將圍繞一下幾個方面進行介紹:
1. Java語言特點
2. 如何學習Java
3. JVM概述
4. 物件導向入門
5. 示例演示
6. 擴充套件閱讀
1. Java是一種物件導向的語言,以物件為顆粒度,物件中包含屬性和方法,通過物件間的繼承和組合構建程式世界。在學習面嚮物件語言時,大家不僅僅應該關注過程,還需要對待解決的問題進行抽象和建模,最終生成易於維護和擴充套件的設計方案。這是一個由淺入深、循序漸進的過程。
2. 其次,Java具有良好的跨平臺特性。Java程式可以不受計算機硬體和作業系統的約束,在任何支援Java虛擬機器(JVM)的環境下都可以正常執行。編寫的Java程式經過編譯後生成的位元組碼可以被JVM識別,JVM為程式執行遮蔽了底層作業系統的差異。
3. 第三個特點是Java具有垃圾回收機制,簡稱GC(Garbage Collection)。在Java中不需要關心記憶體空間的回收問題,這一切都會交給JVM進行處理。JVM會識別出哪些物件不需要再次被使用,進而自動回收其記憶體空間,不需要手動回收,大大提高了開發效率。
4. 第四個特性是Java為單根結構。Java中所有的類都繼承成自同一個基礎類object,如此所有類具有同一個通用介面,並且在層次結構上都屬於同一型別,這為程式設計提供了極大的便利。
5. 另外Java在設計之初就非常注重安全性,在多個階段均提供了安全保證。Java中不支援指標,避免了非法記憶體的操作。在編譯執行時,提供了多重語法、型別、邊界和位元組碼的檢查。
6. 最後Java語言是解釋型的語言。Java編譯的結果並不會在作業系統上執行,而是生成一箇中間class檔案,被JVM載入並解釋執行。早期的Java版本因為解釋過程,執行速度相比C++要慢很多,但隨著Java編譯器的優化,某些結果甚至已經表明Java會比C++執行更快,因此如今並沒有統一的定論。
二. 如何學習Java
首先,Java的學習有兩條主線——Java語言和JVM。一方面,大家需要學習Java語言程式設計的語法規則,能夠熟練使用JDK提供的常用的工具類,並通過多執行緒解決問題。此外還需要熟練掌握一至兩個框架,快速上手工程的開發。另一方面,大家需要了解JVM底層,瞭解Java內部的執行機制。其次,關於工具的選擇,這裡推薦大家使用在業界比較流行的IntelliJ IDEA或Eclipse。一個好的程式設計工具會提供很多優秀的能力,提升開發效率。第三點,建議大家使用較新的JDK版本,例如JDK8及以上。JDK在更新過程中會出現一些優秀的類庫以及新的語法規則,及時更新版本有助於跟上業界新步伐。最後尤為重要的是需要多看、多思考、多實踐。多看一些優秀的原始碼和工程,例如JDK原始碼,可以瞭解好的編碼習慣和風格,並且通過熟悉底層的原理,有助於寫出高效能和健壯的程式;再例如Tomcat原始碼,阿里Dubbo原始碼等,從中學習軟體設計思維。最後還需要多練習實踐。
1. Java記憶體區域管理
Java記憶體區域包括兩部分:由所有執行緒共享的資料區和執行緒隔離的資料區,如圖所示:
線上程隔離的資料區中,包括虛擬機器棧、本地方法棧和程式計數器。程式計數器可以指示當前執行緒所執行的位元組碼的行號,位元組碼直譯器會通過更改計數器的值來選取下一條需要執行的位元組碼指令。每個執行緒的程式計數器都是獨立的,確保各執行緒間計數器互不影響。虛擬機器棧也是執行緒私有的,生命週期和執行緒相同,每個方法執行時會建立一個棧針、當前執行方法的區域性變數表、運算元、動態連結、方法出口等資訊都儲存在該區域中。方法的呼叫和返回對應的棧針在虛擬機器棧中的入棧和出棧操作中。本地方法棧的作用與虛擬機器棧類似,不過本地方法棧儲存的是呼叫native方法時使用的資料結構。方法區是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量和靜態變數等資料。堆也是共享記憶體區域,儲存物件例項、JVM的垃圾回收等。
Java的垃圾回收機制中,首先需要確定哪些物件需要進行垃圾回收,這裡通常採用可達性分析來進行判斷。這個演算法的基本思想是設定一系列物件作為起點,稱為GC Roots節點,搜尋建立引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。在進行可達性分析時,需要讓整個系統凍結在某個時間點,對外則表現為所有工作程式都停止,如此才可以準確獲取所有GC Roots,這個過程稱為stop the world。此外,引用計數器演算法也可以判斷物件是否存活,雖然該演算法效率較高,但如果存在物件間的迴圈引用,即使這些物件不可訪問,也存在無法回收的情況。
在回收物件例項時,有多種演算法可供選擇。第一種標記-清除演算法,分為兩個階段,首先標記出所有需要回收的物件,然後統一進行回收。第二種複製演算法,針對方法一中記憶體碎片過多的缺點,複製演算法將記憶體按照容量劃分為大小相同的兩塊,每次只使用其中的一塊,當一塊用完後,將仍存活的物件複製到另一塊中,然後將使用的那塊空間一次性清理,如此反覆使用。第三種標記-整理演算法,針對方法二中記憶體利用率不高的缺點進行改進,過程和方法一類似,首先對物件進行標記,然後將仍存活的物件向一端移動,然後清理邊界以外的記憶體區域。方法四分代收集演算法,將堆劃分為新生代和老年代,根據物件的生命週期,分別放入不同的記憶體區域中,同時針對不同垃圾回收特點的物件採用不同的回收策略。新生代分為一塊較大的Eden區和兩個較小的survival區,因為新生代大部分物件都需要回收,所以採用複製演算法進行回收。而老年代中需要回收的物件較少,因此採用標記-清除或者標記-整理演算法進行回收。基於上述垃圾回收演算法,JVM實現了多個垃圾收集器,可以通過一些引數進行設定(具體內容可參考擴充套件閱讀)。
在Java語言中,所有類都擁有一個共同的父類Object,即便沒有顯式宣告。接下來展示Object的一些方法,如下圖所示:
首先,它包含一個private方法refisterNatives(),該方法與本地註冊有關,這裡不做詳細討論。其次,它包含notify()和notifyAll()方法,這兩種方法比較類似,歸併為一類,另包含三種wait()方法,也歸為一類。因此Object類共包含8種方法,分為以下四類進行講解。
他們分別回答了四個問題:我是誰,我從哪裡來,我到哪裡去,我與外界如何互相作用的。getClass和hashCode分別讓外界知曉當前物件的型別和一個獨一無二的標識。equals告訴外界當前物件是否和另一個物件相等。toString用字串標識當前物件資訊。clone方法可以讓外界獲得當前物件的一個拷貝。finalize方法可以實現物件被回收前最後的清理工作。notify()和notifyAll()方法用於喚醒一個等待在該物件上的一個執行緒或所有執行緒。wait方法是讓當前執行緒進入等待狀態,直至被喚醒或者等待時間結束。注意這裡clone方法和finalize方法使用灰色表示,因為二者都屬於protected方法,如果需使用則需要重寫其實現。另外在重寫clone還要注意深拷貝和淺拷貝的問題,finalize方法使用時具有不確定性,這裡不推薦大家使用。
1. 整體示例
在類Phone中,定義了兩個屬性brand和serialNum,代表品牌和序列號。在建構函式中,為兩個屬性初始化值。同時實現了clone介面,覆蓋了父類實現。
在main函式中,定義了phone1和phone2,phone2是phone1的拷貝,然後列印出兩個變數的基本資訊,包括手機的類名稱、hashCode、表示當前手機的字串和對比兩個手機是否相同。執行結果如下所示:
由執行結果可見,二者屬於同一個類,但hashCode不同,並且字串表示也不相同。接下來仔細介紹每個方法。hashCode方法為native,表明呼叫的本地方法,程式碼由非Java程式碼寫出。hashCode返回一個int數值,代表記憶體空間中的一個地址。
equals方法如下所示,用雙等號來對比兩個物件,注意因為此處兩變數都為引用型別,因此這裡雙等號對比的是二者引用的記憶體地址是否一致。上例中由列印出的hashCode可見二者記憶體地址不一致,因此equals方法返回false。
toString方法中,首先列印類名稱,加上@,還有記憶體地址空間的十六進位制表示。
但某些情況下我們認為如果phone的品牌和序列號一致即為二者一致,此時便需要嘗試修改一些方法的實現。這裡可以使用IDE工具自動生成generate的部分程式碼(具體操作詳見視訊),如下所示:
此時在equals中,首先比較二者的記憶體地址空間,如果一致返回true;若不一致則進一步比較二者的類資訊,若類不一致,則返回false,若一致,則需要繼續比較其屬性值,若屬性值都一致,則可以認為二者是相同的物件。hashCode中也類似,使用其兩個屬性值生成hashCode。
類似的也可以生成toString的實現,使用StringBuffer類來拼接字串,列印出相關屬性值。此時再執行主函式,得出結果如下:
此時兩個變數的hashCode值已經一致,用字串表示時也會列印出一些屬性資訊,對比結果也返回true。
類Screen中,包含屬性值size,表示螢幕大小。並且有getSize方法獲取螢幕大小以及setSize方法設定size。toString方法將size值列印出來。
接著在上例Phone類中加入屬性值Screen,並且更新建構函式,加入獲取和更新螢幕的方法,如下所示:
重新編寫main()函式。首先定義了大小為5的螢幕,phone1和phone2與上例一致,只是phone1新增了引數變數screen。分別列印出兩個phone的螢幕大小,然後更改screen的值,再列印一次,執行結果如下所示:
由結果可見,在更改screen大小之前,兩個phone的螢幕大小都為5,更改完成後都變為6。這表明此處預設的clone方法實現的為淺拷貝。因為screen中儲存的為記憶體地址空間,拷貝後的物件儲存的依然是該地址,因此改變螢幕size時二者同時變化。下面展示深拷貝,程式碼如圖所示:
首先從super.clone中獲得phone變數,為其重新設定螢幕大小,設定的值為呼叫當前screen的clone方法,最後返回變化後的拷貝。執行結果如下所示:
此時可以看到在第二次更改完size之後,只有phone1的螢幕大小變為6,而phone2的螢幕大小仍然為之前的值。
數十款阿里雲產品限時折扣中,趕緊點選這裡,領劵開始雲上實踐吧!