java核心技術36總結筆記(1-9)

前段時間在極客時間上購買了楊曉峯老師的《Java核心技術36講》,趁着這段時間有空,對相關知識點做了一個整體的大綱若想深入學習,可以購買這個專欄,也可以對着該知識點查閱相關資料,源碼等自學

1 Java平臺的理解

JVM :英文名稱(Java Virtual Machine),就是我們耳熟能詳的 Java 虛擬機。它只認識 xxx.class 這種類型的文件,它能夠將 class 文件中的字節碼指令進行識別並調用操作系統向上的 API 完成動作。所以說,jvm 是 Java 能夠跨平臺的核心,具體的下文會詳細說明。

JRE :英文名稱(Java Runtime Environment),我們叫它:Java 運行時環境。它主要包含兩個部分,jvm 的標準實現和 Java 的一些基本類庫。它相對於 jvm 來說,多出來的是一部分的 Java 類庫。

JDK :英文名稱(Java Development Kit),Java 開發工具包。jdk 是整個 Java 開發的核心,它集成了 jre 和一些好用的小工具。例如:javac.exe,java.exe,jar.exe 等。

顯然,這三者的關係是:一層層的嵌套關係。JDK>JRE>JVM

 

今天我要問你的問題是,談談你對 Java 平臺的理解?“Java 是解釋執行”,這句話正確嗎?

典型回答

Java 本身是一種面向對象的語言,最顯著的特性有兩個方面,一是所謂的“書寫一次,到處運行”(Write once, run anywhere),能夠非常容易地獲得跨平臺能力;另外就是垃圾收集(GC, Garbage Collection),Java 通過垃圾收集器(Garbage Collector)回收分配內存,大部分情況下,程序員不需要自己操心內存的分配和回收。

我們日常會接觸到 JRE(Java Runtime Environment)或者 JDK(Java Development Kit)。 JRE,也就是 Java 運行環境,包含了 JVM 和 Java 類庫,以及一些模塊等。而 JDK 可以看作是 JRE 的一個超集,提供了更多工具,比如編譯器、各種診斷工具等。

對於“Java 是解釋執行”這句話,這個說法不太準確。我們開發的 Java 的源代碼,首先通過 Javac 編譯成爲字節碼(bytecode),然後,在運行時,通過 Java 虛擬機(JVM)內嵌的解釋器將字節碼轉換成爲最終的機器碼。但是常見的 JVM,比如我們大多數情況使用的 Oracle JDK 提供的 Hotspot JVM,都提供了 JIT(Just-In-Time)編譯器,也就是通常所說的動態編譯器,JIT 能夠在運行時將熱點代碼編譯成機器碼,這種情況下部分熱點代碼就屬於編譯執行,而不是解釋執行了。

 

熱點評論回答:

Java特性:

面向對象(封裝,繼承,多態)

平臺無關性(JVM運行.class文件)

語言(泛型,Lambda)

類庫(集合,併發,網絡,IO/NIO)

JRE(Java運行環境,JVM,類庫)

JDK(Java開發工具,包括JRE,javac,診斷工具)

 

Java是解析運行嗎?

不正確!

1,Java源代碼經過Javac編譯成.class文件

2,.class文件經JVM解析或編譯運行。

(1)解析:.class文件經過JVM內嵌的解析器解析執行。

(2)編譯:存在JIT編譯器(Just In Time Compile 即時編譯器)把經常運行的代碼作爲"熱點代碼"編譯與本地平臺相關的機器碼,並進行各種層次的優化。

(3)AOT編譯器: Java 9提供的直接將所有代碼編譯成機器碼執行。

 

“一次編譯、到處運行”說的是Java語言跨平臺的特性,Java的跨平臺特性與Java虛擬機的存在密不可分,可在不同的環境中運行。比如說Windows平臺和Linux平臺都有相應的JDK,安裝好JDK後也就有了Java語言的運行環境。其實Java語言本身與其他的編程語言沒有特別大的差異,並不是說Java語言可以跨平臺,而是在不同的平臺都有可以讓Java語言運行的環境而已,所以纔有了Java一次編譯,到處運行這樣的效果。

嚴格的講,跨平臺的語言不止Java一種,但Java是較爲成熟的一種。“一次編譯,到處運行”這種效果跟編譯器有關。編程語言的處理需要編譯器和解釋器。Java虛擬機和DOS類似,相當於一個供程序運行的平臺。

程序從源代碼到運行的三個階段:編碼——編譯——運行——調試。Java在編譯階段則體現了跨平臺的特點。編譯過程大概是這樣的:首先是將Java源代碼轉化成.CLASS文件字節碼,這是第一次編譯。.class文件就是可以到處運行的文件。然後Java字節碼會被轉化爲目標機器代碼,這是是由JVM來執行的,即Java的第二次編譯。

“到處運行”的關鍵和前提就是JVM。因爲在第二次編譯中JVM起着關鍵作用。在可以運行Java虛擬機的地方都內含着一個JVM操作系統。從而使JAVA提供了各種不同平臺上的虛擬機制,因此實現了“到處運行”的效果。需要強調的一點是,java並不是編譯機制,而是解釋機制。Java字節碼的設計充分考慮了JIT這一即時編譯方式,可以將字節碼直接轉化成高性能的本地機器碼,這同樣是虛擬機的一個構成部分。

 

1,JVM的內存模型,堆、棧、方法區;字節碼的跨平臺性;對象在JVM中的強引用,弱引用,軟引用,虛引用,是否可用finalise方法救救它?;雙親委派進行類加載,什麼是雙親呢?雙親就是多親,一份文檔由我加載,然後你也加載,這份文檔在JVM中是一樣的嗎?;多態思想是Java需要最核心的概念,也是面向對象的行爲的一個最好詮釋;理解方法重載與重寫在內存中的執行流程,怎麼定位到這個具體方法的。2,發展流程,JDK5(重寫bug),JDK6(商用最穩定版),JDK7(switch的字符串支持),JDK8(函數式編程),一直在發展進化。3,理解祖先類Object,它的行爲是怎樣與現實生活連接起來的。4,理解23種設計模式,因爲它是道與術的結合體。

 

我理解的java程序執行步驟:

首先javac編譯器將源代碼編譯成字節碼。

然後jvm類加載器加載字節碼文件,然後通過解釋器逐行解釋執行,這種方式的執行速度相對會比較慢。有些方法和代碼塊是高頻率調用的,也就是所謂的熱點代碼,所以引進jit技術,提前將這類字節碼直接編譯成本地機器碼。這樣類似於緩存技術,運行時再遇到這類代碼直接可以執行,而不是先解釋後執行。

解釋執行和編譯執行有何區別: 類比一下,一個是同聲傳譯,一個是放錄音

 

編譯型語言:C/C++、 Pascal(Delphi)

編譯就是把源代碼(高級語言,人類容易讀,容易理解)轉換成機器碼(CPU能理解,能高效的執行)

 

解釋型語言:JavaScript、Perl、Python、Ruby

解釋就簡單多了,解析源代碼,並且直接執行,沒有編譯過程 

 

編譯程序是整體編譯完了,再一次性執行。 而解釋程序是一邊解釋,一邊執行

JAVA語言是一種編譯型-解釋型語言,同時具備編譯特性和解釋特性

其所謂的編譯過程只是將.java文件編程成平臺無關的字節碼.class文件,

並不是向C一樣編譯成可執行的機器語言,在此請讀者注意Java中所謂的“編譯”和傳統的“編譯”的區別。

作爲編譯型語言,JAVA程序要被統一編譯成字節碼文件——文件後綴是class。此種文件在java中又稱爲類文件。

java類文件不能再計算機上直接執行,它需要被java虛擬機翻譯成本地的機器碼後才能執行,而java虛擬機的翻譯過程則是解釋性的。

java字節碼文件首先被加載到計算機內存中,然後讀出一條指令,翻譯一條指令,執行一條指令,該過程被稱爲java語言的解釋執行,是由java虛擬機完成的。

以上說的是Java的解釋執行,但是比如我們大多數情況使用的Hotspot JVM,都提供了動態編譯器編譯器JIT,能夠追蹤熱點代碼, 然後變成機器指令,這種情況下部分熱點代碼就屬於編譯執行,而不是解釋執行了

java堆、棧、堆棧的區別

 

2 Exception 和 Error 有什麼區別

理解Java的異常體系的設計,Throwable ,Exception,Error 的關係

理解ClassNotFoundException NoClassDefFoundError的區別

遵循 Throw early, catch late 原則

 

今天我要問你的問題是,請對比 Exception 和 Error,另外,運行時異常與一般異常有什麼區別?

典型回答

Exception 和 Error 都是繼承了 Throwable 類,在 Java 中只有 Throwable 類型的實例纔可以被拋出(throw)或者捕獲(catch),它是異常處理機制的基本組成類型。

Exception 和 Error 體現了 Java 平臺設計者對不同異常情況的分類。Exception 是程序正常運行中,可以預料的意外情況,可能並且應該被捕獲,進行相應處理。

Error 是指在正常情況下,不大可能出現的情況,絕大部分的 Error 都會導致程序(比如 JVM 自身)處於非正常的、不可恢復狀態。既然是非正常情況,所以不便於也不需要捕獲,常見的比如 OutOfMemoryError 之類,都是 Error 的子類。

Exception 又分爲可檢查(checked)異常和不檢查(unchecked)異常,可檢查異常在源代碼裏必須顯式地進行捕獲處理,這是編譯期檢查的一部分。前面我介紹的不可查的 Error,是 Throwable 不是 Exception , 不檢查異常就是所謂的運行時異常,類似 NullPointerException、ArrayIndexOutOfBoundsException 之類,通常是可以編碼避免的邏輯錯誤,具體根據需要來判斷是否需要捕獲,並不會在編譯期強制要求。

 

其中有些子類型,最好重點理解一下,比如 NoClassDefFoundError 和 ClassNotFoundException 有什麼區別,這也是個經典的入門題目。

正如它們的名字所說明的:NoClassDefFoundError是一個錯誤(Error),而ClassNOtFoundException是一個異常,在Java中錯誤和異常是有區別的,我們可以從異常中恢復程序但卻不應該嘗試從錯誤中恢復程序。

ClassNotFoundException的產生原因:

Java支持使用Class.forName方法來動態地加載類,任意一個類的類名如果被作爲參數傳遞給這個方法都將導致該類被加載到JVM內存中,如果這個類在類路徑中沒有被找到,那麼此時就會在運行時拋出ClassNotFoundException異常。

要解決這個問題很容易,唯一需要做的就是要確保所需的類連同它依賴的包存在於類路徑中。當Class.forName被調用的時候,類加載器會查找

類路徑中的類,如果找到了那麼這個類就會被成功加載,如果沒找到,那麼就會拋出ClassNotFountException,除了Class.forName,ClassLoader.loadClass、ClassLOader.findSystemClass在動態加載類到內存中的時候也可能會拋出這個異常。

另外還有一個導致ClassNotFoundException的原因就是:當一個類已經某個類加載器加載到內存中了,此時另一個類加載器又嘗試着動態地從同一個包中加載這個類。

由於類的動態加載在某種程度上是被開發者所控制的,所以他可以選擇catch這個異常然後採取相應的補救措施。有些程序可能希望忽略這個異常而採取其他方法。還有一些程序則會終止程序然後讓用戶再次嘗試前做點事情。

 

NoClassDefFoundError產生的原因:

如果JVM或者ClassLoader實例嘗試加載(可以通過正常的方法調用,也可能是使用new來創建新的對象)類的時候卻找不到類的定義。要查找的類在編譯的時候是存在的,運行的時候卻找不到了。這個錯誤往往是你使用new操作符來創建一個新的對象但卻找不到該對象對應的類。這個時候就會導致NoClassDefFoundError.

由於NoClassDefFoundError是有JVM引起的,所以不應該嘗試捕捉這個錯誤。

解決這個問題的辦法就是:查找那些在開發期間存在於類路徑下但在運行期間卻不在類路徑下的類。

另:

ClassNotFoundException發生在裝入階段。

當應用程序試圖通過類的字符串名稱,使用常規的三種方法裝入類,但卻找不到指定名稱的類定義時就拋出該異常。

NoClassDefFoundError: 當目前執行的類已經編譯,但是找不到它的定義時

也就是說你如果編譯了一個類B,在類A中調用,編譯完成以後,你又刪除掉B,運行A的時候那麼就會出現這個錯誤

加載時從外存儲器找不到需要的class就出現ClassNotFoundException

連接時從內存找不到需要的class就出現NoClassDefFoundError

 

1.異常的父類。throwable.

2.異常的分類。error錯誤。exception異常。 

3。對異常分類的使用。error是jvm環境運行錯誤,不可進行捕獲。包括,throwable。exception是程序上的錯誤,需要在錯誤時進行捕獲,恢復正常運行的形態。對異常捕獲,最好進行異常類型最匹配的形式,這樣具有日誌堆棧信息便於查詢,排錯。

4。異常的使用是比較消耗性能。消耗性能的方式有try代碼快,與生成exception的堆棧快照。

5。注意信息。在生成exception錯誤信息時,不能使用exception自帶方法進行輸出。這種方式,不清楚會輸出到什麼地方,不好排查。

6。異常實踐。異常分2種進行處理,一種業務異常,一種程序異常。業務異常直接拋出,程序異常,先處理一次,如果處理不了在進行拋出。

 

3 final、finally、finalize 有什麼不同

從安全、性能、垃圾收集方面考慮

談談 final、finally、 finalize 有什麼不同?

典型回答

final 可以用來修飾類、方法、變量,分別有不同的意義,final 修飾的 class 代表不可以繼承擴展,final 的變量是不可以修改的,而 final 的方法也是不可以重寫的(override)。

finally 則是 Java 保證重點代碼一定要被執行的一種機制。我們可以使用 try-finally 或者 try-catch-finally 來進行類似關閉 JDBC 連接、保證 unlock 鎖等動作。

finalize 是基礎類 java.lang.Object 的一個方法,它的設計目的是保證對象在被垃圾收集前完成特定資源的回收。finalize 機制現在已經不推薦使用,並且在 JDK 9 開始被標記爲 deprecated。

 

1。final修飾的類,不可被繼承,修飾的方法不可被重寫,修飾的變量不可多次賦值。通過final能夠得到性能上的優化,但是不明顯,如果大量使用可能會干擾代碼,不能表達出本來具有的含義。故不使用。匿名內部類,訪問局部變量要求傳入的參數,必須是final是要保證數據一致性問題。

2。finally。代碼中總是會執行的代碼段。除了退出虛擬機外。

3。finalize。在虛擬機回收改對象前進行調用。此種方式不可取。因爲java虛擬機不知道在什麼時候纔對對象進行回收。

String爲什麼被設計成final這個問題

設計安全:

  只有當字符串是不可變的,字符串池纔有可能實現,字符串池的實現可以在運行時節約很多heap空間,因爲不同的字符串變量都指向池中的同一個字符串,如果字符串是可變的,當變量改變了它的值,那麼其它指向這個值的變量的值也會一起改變,那麼會引起很嚴重的安全問題。

  另一方面,從平臺安全性上來說,Java自出生那天起就是“爲人民服務”,這也就是爲什麼Java做不了病毒,也不一定非得是病毒,反正總之就是爲了安全,人家Java的開發者目的就是不想讓Java幹這類危險的事兒,Java並不是操作系統本地語言,換句話說Java必須藉助操作系統本身的力量才能做事,JDK中提供的好多核心類比如String,這類的類的內部好多方法的實現都不是Java編程語言本身編寫的,好多方法都是調用的操作系統本地的API,這就是著名的“本地方法調用”,也只有這樣才能做事,這種類是非常底層的,和操作系統交流頻繁的,那麼如果這種類可以被繼承的話,如果我們再把它的方法重寫了,往操作系統內部寫入一段具有惡意攻擊性質的代碼什麼的,這不就成了核心病毒了麼?如果這些個核心的類都能被隨便操作的話,那是很恐怖的,會出現好多好多未知的錯誤,莫名其妙的錯誤….

從效率上講:

  final修飾的類不能被繼承,這意味着不允許任何人定義String的子類。這意味着JVM纔不用對相關方法在虛函數表中查詢,而直接定位到String類的相關方法上,提高了執行效率。

  因爲字符串是不可變的,同一個字符串實例可以被多個線程共享。這樣便不用因爲線程安全問題而使用同步,這也是設計爲final來保證線程安全的一個體現。

  因爲字符串是不可變的,所以在它創建的時候hashcode就被緩存了,不需要重新計算,這就使得字符串很適合作爲Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵往往都使用字符串。

總而言之,就是要保證 java.lang.String 引用的對象一定是 java.lang.String的對象,而不是引用它的子孫類,這樣才能保證它的效率和安全。

 

4 強引用、軟引用、弱引用、幻象引用有什麼區別

 

典型回答

不同的引用類型,主要體現的是對象不同的可達性(reachable)狀態和對垃圾收集的影響

所謂強引用("Strong" Reference),就是我們最常見的普通對象引用,只要還有強引用指向一個對象,就能表明對象還“活着”,垃圾收集器不會碰這種對象。對於一個普通的對象,如果沒有其他的引用關係,只要超過了引用的作用域或者顯式地將相應(強)引用賦值爲 null,就是可以被垃圾收集的了,當然具體回收時機還是要看垃圾收集策略。

軟引用(SoftReference),是一種相對強引用弱化一些的引用,可以讓對象豁免一些垃圾收集,只有當 JVM 認爲內存不足時,纔會去試圖回收軟引用指向的對象。JVM 會確保在拋出 OutOfMemoryError 之前,清理軟引用指向的對象。軟引用通常用來實現內存敏感的緩存,如果還有空閒內存,就可以暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。

弱引用(WeakReference)並不能使對象豁免垃圾收集,僅僅是提供一種訪問在弱引用狀態下對象的途徑。這就可以用來構建一種沒有特定約束的關係,比如,維護一種非強制性的映射關係,如果試圖獲取時對象還在,就使用它,否則重現實例化。它同樣是很多緩存實現的選擇。

對於幻象引用,有時候也翻譯成虛引用,你不能通過它訪問對象。幻象引用僅僅是提供了一種確保對象被 finalize 以後,做某些事情的機制,比如,通常用來做所謂的 Post-Mortem 清理機制,我在專欄上一講中介紹的 Java 平臺自身 Cleaner 機制等,也有人利用幻象引用監控對象的創建和銷燬。

對象的可達性狀態:強引用 -> 軟引用 -> 弱引用 -(finalize)->幻想引用

這是 Java 定義的不同可達性級別(reachability level),具體如下:

  • 強可達(Strongly Reachable),就是當一個對象可以有一個或多個線程可以不通過各種引用訪問到的情況。比如,我們新創建一個對象,那麼創建它的線程對它就是強可達。

  • 軟可達(Softly Reachable),就是當我們只能通過軟引用才能訪問到對象的狀態。

  • 弱可達(Weakly Reachable),類似前面提到的,就是無法通過強引用或者軟引用訪問,只能通過弱引用訪問時的狀態。這是十分臨近 finalize 狀態的時機,當弱引用被清除的時候,就符合 finalize 的條件了。

  • 幻象可達(Phantom Reachable),上面流程圖已經很直觀了,就是沒有強、軟、弱引用關聯,並且 finalize 過了,只有幻象引用指向這個對象的時候。

  • 當然,還有一個最後的狀態,就是不可達(unreachable),意味着對象可以被清除了。

判斷對象可達性,是 JVM 垃圾收集器決定如何處理對象的一部分考慮。

所有引用類型,都是抽象類 java.lang.ref.Reference 的子類,你可能注意到它提供了 get() 方法:

除了幻象引用(因爲 get 永遠返回 null),如果對象還沒有被銷燬,都可以通過 get 方法獲取原有對象。這意味着,利用軟引用和弱引用,我們可以將訪問到的對象,重新指向強引用,也就是人爲的改變了對象的可達性狀態!這也是爲什麼我在上面圖裏有些地方畫了雙向箭頭。

所以,對於軟引用、弱引用之類,垃圾收集器可能會存在二次確認的問題,以保證處於弱引用狀態的對象,沒有改變爲強引用。

但是,你覺得這裏有沒有可能出現什麼問題呢?

不錯,如果我們錯誤的保持了強引用(比如,賦值給了 static 變量),那麼對象可能就沒有機會變回類似弱引用的可達性狀態了,就會產生內存泄漏。所以,檢查弱引用指向對象是否被垃圾收集,也是診斷是否有特定內存泄漏的一個思路,如果我們的框架使用到弱引用又懷疑有內存泄漏,就可以從這個角度檢查。

對象可達性狀態的流轉

引用隊列(ReferenceQueue)的使用

診斷 JVM 引用情況

Reachability Fence 通過底層 API 來達到強引用的效果

 

在Java語言中,除了基本數據類型外,其他的都是指向各類對象的對象引用;Java中根據其生命週期的長短,將引用分爲4類。

1 強引用

特點:我們平常典型編碼Object obj = new Object()中的obj就是強引用。通過關鍵字new創建的對象所關聯的引用就是強引用。 當JVM內存空間不足,JVM寧願拋出OutOfMemoryError運行時錯誤(OOM),使程序異常終止,也不會靠隨意回收具有強引用的“存活”對象來解決內存不足的問題。對於一個普通的對象,如果沒有其他的引用關係,只要超過了引用的作用域或者顯式地將相應(強)引用賦值爲 null,就是可以被垃圾收集的了,具體回收時機還是要看垃圾收集策略。

2 軟引用

特點:軟引用通過SoftReference類實現。 軟引用的生命週期比強引用短一些。只有當 JVM 認爲內存不足時,纔會去試圖回收軟引用指向的對象:即JVM 會確保在拋出 OutOfMemoryError 之前,清理軟引用指向的對象。軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。後續,我們可以調用ReferenceQueue的poll()方法來檢查是否有它所關心的對象被回收。如果隊列爲空,將返回一個null,否則該方法返回隊列中前面的一個Reference對象。

應用場景:軟引用通常用來實現內存敏感的緩存。如果還有空閒內存,就可以暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。

3 弱引用

弱引用通過WeakReference類實現。 弱引用的生命週期比軟引用短。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。由於垃圾回收器是一個優先級很低的線程,因此不一定會很快回收弱引用的對象。弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。

應用場景:弱應用同樣可用於內存敏感的緩存。

4 虛引用

特點:虛引用也叫幻象引用,通過PhantomReference類來實現。無法通過虛引用訪問對象的任何屬性或函數。幻象引用僅僅是提供了一種確保對象被 finalize 以後,做某些事情的機制。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。虛引用必須和引用隊列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。

ReferenceQueue queue = new ReferenceQueue ();

PhantomReference pr = new PhantomReference (object, queue); 

程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。如果程序發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的內存被回收之前採取一些程序行動。

應用場景:可用來跟蹤對象被垃圾回收器回收的活動,當一個虛引用關聯的對象被垃圾收集器回收之前會收到一條系統通知。

 

5 String、StringBuffer、StringBuilder有什麼區別

今天我要問你的問題是,理解 Java 的字符串,String、StringBuffer、StringBuilder 有什麼區別?

典型回答

String 是 Java 語言非常基礎和重要的類,提供了構造和管理字符串的各種基本邏輯。它是典型的 Immutable 類,被聲明成爲 final class,所有屬性也都是 final 的。也由於它的不可變性,類似拼接、裁剪字符串等動作,都會產生新的 String 對象。由於字符串操作的普遍性,所以相關操作的效率往往對應用性能有明顯影響。

StringBuffer 是爲解決上面提到拼接產生太多中間對象的問題而提供的一個類,我們可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本質是一個線程安全的可修改字符序列,它保證了線程安全,也隨之帶來了額外的性能開銷,所以除非有線程安全的需要,不然還是推薦使用它的後繼者,也就是 StringBuilder。

StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 沒有本質區別,但是它去掉了線程安全的部分,有效減小了開銷,是絕大部分情況下進行字符串拼接的首選。

 

(1) String的創建機理

由於String在Java世界中使用過於頻繁,Java爲了避免在一個系統中產生大量的String對象,引入了字符串常量池。其運行機制是:創建一個字符串時,首先檢查池中是否有值相同的字符串對象,如果有則不需要創建直接從池中剛查找到的對象引用;如果沒有則新建字符串對象,返回對象引用,並且將新創建的對象放入池中。但是,通過new方法創建的String對象是不檢查字符串池的,而是直接在堆區或棧區創建一個新的對象,也不會把對象放入池中。上述原則只適用於通過直接量給String對象引用賦值的情況。

舉例:String str1 = "123"; //通過直接量賦值方式,放入字符串常量池

String str2 = new String(“123”);//通過new方式賦值方式,不放入字符串常量池

注意:String提供了inter()方法。調用該方法時,如果常量池中包括了一個等於此String對象的字符串(由equals方法確定),則返回池中的字符串。否則,將此String對象添加到池中,並且返回此池中對象的引用。

 

(2) String的特性

[A] 不可變。是指String對象一旦生成,則不能再對它進行改變。不可變的主要作用在於當一個對象需要被多線程共享,並且訪問頻繁時,可以省略同步和鎖等待的時間,從而大幅度提高系統性能。不可變模式是一個可以提高多線程程序的性能,降低多線程程序複雜度的設計模式。

[B] 針對常量池的優化。當2個String對象擁有相同的值時,他們只引用常量池中的同一個拷貝。當同一個字符串反覆出現時,這個技術可以大幅度節省內存空間。

2 StringBuffer/StringBuilder

StringBuffer和StringBuilder都實現了AbstractStringBuilder抽象類,擁有幾乎一致對外提供的調用接口;其底層在內存中的存儲方式與String相同,都是以一個有序的字符序列(char類型的數組)進行存儲,不同點是StringBuffer/StringBuilder對象的值是可以改變的,並且值改變以後,對象引用不會發生改變;兩者對象在構造過程中,首先按照默認大小申請一個字符數組,由於會不斷加入新數據,當超過默認大小後,會創建一個更大的數組,並將原先的數組內容複製過來,再丟棄舊的數組。因此,對於較大對象的擴容會涉及大量的內存複製操作,如果能夠預先評估大小,可提升性能。

唯一需要注意的是:StringBuffer是線程安全的,但是StringBuilder是線程不安全的。可參看Java標準類庫的源代碼,StringBuffer類中方法定義前面都會有synchronize關鍵字。爲此,StringBuffer的性能要遠低於StringBuilder。

3 應用場景

[A]在字符串內容不經常發生變化的業務場景優先使用String類。例如:常量聲明、少量的字符串拼接操作等。如果有大量的字符串內容拼接,避免使用String與String之間的“+”操作,因爲這樣會產生大量無用的中間對象,耗費空間且執行效率低下(新建對象、回收對象花費大量時間)。

[B]在頻繁進行字符串的運算(如拼接、替換、刪除等),並且運行在多線程環境下,建議使用StringBuffer,例如XML解析、HTTP參數解析與封裝。

[C]在頻繁進行字符串的運算(如拼接、替換、刪除等),並且運行在單線程環境下,建議使用StringBuilder,例如SQL語句拼裝、JSON封裝等。

字符串的設計與實現考量

字符串緩存 intern()方法,由永久代移到堆中。

String 的演化,Java 9 中底層把 char 數組換成了 byte 數組,佔用更少的空間

getBytes()方法指定字符編碼

參考: 幾張圖輕鬆理解String.intern()

 

6 動態代理是基於什麼原理

 

典型回答

反射機制是 Java 語言提供的一種基礎功能,賦予程序在運行時自省(introspect,官方用語)的能力。通過反射我們可以直接操作類或者對象,比如獲取某個對象的類定義,獲取類聲明的屬性和方法,調用方法或者構造對象,甚至可以運行時修改類定義。

動態代理是一種方便運行時動態構建代理、動態處理代理方法調用的機制,很多場景都是利用類似機制做到的,比如用來包裝 RPC 調用、面向切面的編程(AOP)。

實現動態代理的方式很多,比如 JDK 自身提供的動態代理,就是主要利用了上面提到的反射機制。還有其他的實現方式,比如利用傳說中更高性能的字節碼操作機制,類似 ASM、cglib(基於 ASM)、Javassist 等。

1 關於反射

反射最大的作用之一就在於我們可以不在編譯時知道某個對象的類型,而在運行時通過提供完整的”包名+類名.class”得到。注意:不是在編譯時,而是在運行時。

功能:

•在運行時能判斷任意一個對象所屬的類。

•在運行時能構造任意一個類的對象。

•在運行時判斷任意一個類所具有的成員變量和方法。

•在運行時調用任意一個對象的方法。

說大白話就是,利用Java反射機制我們可以加載一個運行時才得知名稱的class,獲悉其構造方法,並生成其對象實體,能對其fields設值並喚起其methods。

應用場景:

反射技術常用在各類通用框架開發中。因爲爲了保證框架的通用性,需要根據配置文件加載不同的對象或類,並調用不同的方法,這個時候就會用到反射——運行時動態加載需要加載的對象。

特點:

由於反射會額外消耗一定的系統資源,因此如果不需要動態地創建一個對象,那麼就不需要用反射。另外,反射調用方法時可以忽略權限檢查,因此可能會破壞封裝性而導致安全問題。

2 動態代理

爲其他對象提供一種代理以控制對這個對象的訪問。在某些情況下,一個對象不適合或者不能直接引用另一個對象,而代理對象可以在兩者之間起到中介的作用(可類比房屋中介,房東委託中介銷售房屋、簽訂合同等)。

所謂動態代理,就是實現階段不用關心代理誰,而是在運行階段才指定代理哪個一個對象(不確定性)。如果是自己寫代理類的方式就是靜態代理(確定性)。

組成要素:

(動態)代理模式主要涉及三個要素:

其一:抽象類接口

其二:被代理類(具體實現抽象接口的類)

其三:動態代理類:實際調用被代理類的方法和屬性的類

實現方式:

實現動態代理的方式很多,比如 JDK 自身提供的動態代理,就是主要利用了反射機制。還有其他的實現方式,比如利用字節碼操作機制,類似 ASM、CGLIB(基於 ASM)、Javassist 等。

舉例,常可採用的JDK提供的動態代理接口InvocationHandler來實現動態代理類。其中invoke方法是該接口定義必須實現的,它完成對真實方法的調用。通過InvocationHandler接口,所有方法都由該Handler來進行處理,即所有被代理的方法都由InvocationHandler接管實際的處理任務。此外,我們常可以在invoke方法實現中增加自定義的邏輯實現,實現對被代理類的業務邏輯無侵入。

反射原理

動態代理分類:JDK動態代理原理,cglib動態代理原理

優缺點,適用場景

 

7 int和Integer有什麼區別

int 和 Integer 有什麼區別?談談 Integer 的值緩存範圍。

典型回答

int 是我們常說的整形數字,是 Java 的 8 個原始數據類型(Primitive Types,boolean、byte 、short、char、int、float、double、long)之一。Java 語言雖然號稱一切都是對象,但原始數據類型是例外。

Integer 是 int 對應的包裝類,它有一個 int 類型的字段存儲數據,並且提供了基本操作,比如數學運算、int 和字符串之間轉換等。在 Java 5 中,引入了自動裝箱和自動拆箱功能(boxing/unboxing),Java 可以根據上下文,自動進行轉換,極大地簡化了相關編程。

關於 Integer 的值緩存,這涉及 Java 5 中另一個改進。構建 Integer 對象的傳統方式是直接調用構造器,直接 new 一個對象。但是根據實踐,我們發現大部分數據操作都是集中在有限的、較小的數值範圍,因而,在 Java 5 中新增了靜態工廠方法 valueOf,在調用它的時候會利用一個緩存機制,帶來了明顯的性能改進。按照 Javadoc,這個值默認緩存是 -128 到 127 之間。

基本類型和包裝類型

包裝類型的緩存

自動裝箱實際上算是一種語法糖。什麼是語法糖?可以簡單理解爲 Java 平臺爲我們自動進行了一些轉換,保證不同的寫法在運行時等價,它們發生在編譯階段,也就是生成的字節碼是一致的。

像前面提到的整數,javac 替我們自動把裝箱轉換爲 Integer.valueOf(),把拆箱替換爲 Integer.intValue(),這似乎這也順道回答了另一個問題,既然調用的是 Integer.valueOf,自然能夠得到緩存的好處啊。

適應場景,性能影響

 

對象由三部分組成,對象頭,對象實例,對齊填充。

其中對象頭一般是十六個字節,包括兩部分,第一部分有哈希碼,鎖狀態標誌,線程持有的鎖,偏向線程id,gc分代年齡等。第二部分是類型指針,也就是對象指向它的類元數據指針,可以理解,對象指向它的類。

對象實例就是對象存儲的真正有效信息,也是程序中定義各種類型的字段包括父類繼承的和子類定義的,這部分的存儲順序會被虛擬機和代碼中定義的順序影響(這裏問一下,這個被虛擬機影響是不是就是重排序??如果是的話,我知道的volatile定義的變量不會被重排序應該就是這裏不會受虛擬機影響吧??)。

第三部分對齊填充只是一個類似佔位符的作用,因爲內存的使用都會被填充爲八字節的倍數。

 

注意事項

[1] 基本類型均具有取值範圍,在大數*大數的時候,有可能會出現越界的情況。

[2] 基本類型轉換時,使用聲明的方式。例:long result= 1234567890 * 24 * 365;結果值一定不會是你所期望的那個值,因爲1234567890 * 24已經超過了int的範圍,如果修改爲:long result= 1234567890L * 24 * 365;就正常了。

[3] 慎用基本類型處理貨幣存儲。如採用double常會帶來差距,常採用BigDecimal、整型(如果要精確表示分,可將值擴大100倍轉化爲整型)解決該問題。

[4] 優先使用基本類型。原則上,建議避免無意中的裝箱、拆箱行爲,尤其是在性能敏感的場合,

[5] 如果有線程安全的計算需要,建議考慮使用類型AtomicInteger、AtomicLong 這樣的線程安全類。部分比較寬的基本數據類型,比如 float、double,甚至不能保證更新操作的原子性,可能出現程序讀取到只更新了一半數據位的數值。

 

8 對比Vector、ArrayList、LinkedList有何區別

典型回答

這三者都是實現集合框架中的 List,也就是所謂的有序集合,因此具體功能也比較近似,比如都提供按照位置進行定位、添加或者刪除的操作,都提供迭代器以遍歷其內容等。但因爲具體的設計區別,在行爲、性能、線程安全等方面,表現又有很大不同。

Vector 是 Java 早期提供的線程安全的動態數組,如果不需要線程安全,並不建議選擇,畢竟同步是有額外開銷的。Vector 內部是使用對象數組來保存數據,可以根據需要自動的增加容量,當數組已滿時,會創建新的數組,並拷貝原有數組數據。

ArrayList 是應用更加廣泛的動態數組實現,它本身不是線程安全的,所以性能要好很多。與 Vector 近似,ArrayList 也是可以根據需要調整容量,不過兩者的調整邏輯有所區別,Vector 在擴容時會提高 1 倍,而 ArrayList 則是增加 50%。

LinkedList 顧名思義是 Java 提供的雙向鏈表,所以它不需要像上面兩種那樣調整容量,它也不是線程安全的。

考點分析

一般來說,也可以補充一下不同容器類型適合的場景:

  • Vector 和 ArrayList 作爲動態數組,其內部元素以數組形式順序存儲的,所以非常適合隨機訪問的場合。除了尾部插入和刪除元素,往往性能會相對較差,比如我們在中間位置插入一個元素,需要移動後續所有元素。

  • 而 LinkedList 進行節點插入、刪除卻要高效得多,但是隨機訪問性能則要比動態數組慢。

所以,在應用開發中,如果事先可以估計到,應用操作是偏向於插入、刪除,還是隨機訪問較多,就可以針對性的進行選擇。這也是面試最常見的一個考察角度,給定一個場景,選擇適合的數據結構,所以對於這種典型選擇一定要掌握清楚。

Vector、ArrayList、LinkedList均爲線型的數據結構,但是從實現方式與應用場景中又存在差別。

1 底層實現方式

ArrayList內部用數組來實現;LinkedList內部採用雙向鏈表實現;Vector內部用數組實現。

2 讀寫機制

ArrayList在執行插入元素是超過當前數組預定義的最大值時,數組需要擴容,擴容過程需要調用底層System.arraycopy()方法進行大量的數組複製操作;在刪除元素時並不會減少數組的容量(如果需要縮小數組容量,可以調用trimToSize()方法);在查找元素時要遍歷數組,對於非null的元素採取equals的方式尋找。

LinkedList在插入元素時,須創建一個新的Entry對象,並更新相應元素的前後元素的引用;在查找元素時,需遍歷鏈表;在刪除元素時,要遍歷鏈表,找到要刪除的元素,然後從鏈表上將此元素刪除即可。

Vector與ArrayList僅在插入元素時容量擴充機制不一致。對於Vector,默認創建一個大小爲10的Object數組,並將capacityIncrement設置爲0;當插入元素數組大小不夠時,如果capacityIncrement大於0,則將Object數組的大小擴大爲現有size+capacityIncrement;如果capacityIncrement<=0,則將Object數組的大小擴大爲現有大小的2倍。

3 讀寫效率

ArrayList對元素的增加和刪除都會引起數組的內存分配空間動態發生變化。因此,對其進行插入和刪除速度較慢,但檢索速度很快。

LinkedList由於基於鏈表方式存放數據,增加和刪除元素的速度較快,但是檢索速度較慢。

4 線程安全性

ArrayList、LinkedList爲非線程安全;Vector是基於synchronized實現的線程安全的ArrayList。

我們可以看到 Java 的集合框架,Collection 接口是所有集合的根,然後擴展開提供了三大類集合,分別是:

* List,也就是我們前面介紹最多的有序集合,它提供了方便的訪問、插入、刪除等操作。

* Set,Set 是不允許重複元素的,這是和 List 最明顯的區別,也就是不存在兩個對象 equals 返回 true。我們在日常開發中有很多需要保證元素唯一性的場合。

* Queue/Deque,則是 Java 提供的標準隊列結構的實現,除了集合的基本功能,它還支持類似先入先出(FIFO, First-in-First-Out)或者後入先出(LIFO,Last-In-First-Out)等特定行爲。這裏不包括 BlockingQueue,因爲通常是併發編程場合,所以被放置在併發包裏。

集合:就像是一種容器。用於存儲、獲取、操作對象的容器。

1. 數組的弊端

①數組的長度不可變     ②數組沒有提供可以查看有效元素個數的方法

 

2. 集合的特點

①集合的長度是可變的

②集合可以存儲任意類型的對象

③集合只能存儲對象

3. 集合框架

java.util.Collection : 集合層次的根接口

    |--- java.util.List: 有序的,可以重複的。

        |--- ArrayList: 採用數組結構存儲元素。 查詢操作多時選擇

        |--- LinkedList: 採用鏈表結構存儲元素。 增刪操作多時選擇

        |--- Vector:

    |--- java.util.Set: 無序的,不允許重複。

        |--- HashSet : 是 Set 接口的典型實現類。

            判斷元素是否存在的依據是:先比較 hashCode 值,若 hashCode 存在,再通過 equals() 比較內容

                                     若 hashCode 值不存在,則直接存儲

 

            注意:重寫 hashCode 和 equals 二者需要保持一致!

            |--- LinkedHashSet: 相較於 HashSet 多了鏈表維護元素的順序。遍歷效率高於 HashSet , 增刪效率低於 HashSet

        |--- TreeSet : 擁有自己排序方式

            |-- 自然排序(Comparable):

                ①需要添加 TreeSet 集合中對象的類實現  Comparable 接口

                ②實現 compareTo(Object o) 方法

            |-- 定製排序(Comparator)

                ①創建一個類實現 Comparator 接口

                ②實現 compare(Object o1, Object o2) 方法

                ③將該實現類的實例作爲參數傳遞給 TreeSet 的構造器       

Collection 各種類的實現原理,使用場景

List 的各種實現,Set 的各種實現,Queue的各種實現

理解Java提供的各種排序算法 Collections.sort() Arrays.sort()

 

9 對比Hashtable、HashMap、TreeMap有什麼不同

典型回答

Hashtable、HashMap、TreeMap 都是最常見的一些 Map 實現,是以鍵值對的形式存儲和操作數據的容器類型。

Hashtable 是早期 Java 類庫提供的一個哈希表實現,本身是同步的,不支持 null 鍵和值,由於同步導致的性能開銷,所以已經很少被推薦使用。

HashMap 是應用更加廣泛的哈希表實現,行爲上大致上與 HashTable 一致,主要區別在於 HashMap 不是同步的,支持 null 鍵和值等。通常情況下,HashMap 進行 put 或者 get 操作,可以達到常數時間的性能,所以它是絕大部分利用鍵值對存取場景的首選,比如,實現一個用戶 ID 和用戶信息對應的運行時存儲結構。

TreeMap 則是基於紅黑樹的一種提供順序訪問的 Map,和 HashMap 不同,它的 get、put、remove 之類操作都是 O(log(n))的時間複雜度,具體順序可以由指定的 Comparator 來決定,或者根據鍵的自然順序來判斷。

 

Hashtable 比較特別,作爲類似 Vector、Stack 的早期集合相關類型,它是擴展了 Dictionary 類的,類結構上與 HashMap 之類明顯不同。

HashMap 等其他 Map 實現則是都擴展了 AbstractMap,裏面包含了通用方法抽象。不同 Map 的用途,從類圖結構就能體現出來,設計目的已經體現在不同接口上。

大部分使用 Map 的場景,通常就是放入、訪問或者刪除,而對順序沒有特別要求,HashMap 在這種情況下基本是最好的選擇。HashMap 的性能表現非常依賴於哈希碼的有效性,請務必掌握 hashCode 和 equals 的一些基本約定,比如:

* equals 相等,hashCode 一定要相等。

* 重寫了 hashCode 也要重寫 equals。

* hashCode 需要保持一致性,狀態改變返回的哈希值仍然要一致。

* equals 的對稱、反射、傳遞等特性。

 

  • HashMap 實現原理,源碼分析

首先,我們來一起看看 HashMap 內部的結構,它可以看作是數組(Node<K,V>[] table)和鏈表結合組成的複合結構,數組被分爲一個個桶(bucket),通過哈希值決定了鍵值對在這個數組的尋址;哈希值相同的鍵值對,則以鏈表形式存儲,你可以參考下面的示意圖。這裏需要注意的是,如果鏈表大小超過閾值(TREEIFY_THRESHOLD, 8),圖中的鏈表就會被改造爲樹形結構。

Java 8 HashMap 中的樹化

容量和負載因子

解決哈希衝突的方法

Hashtable、HashMap、TreeMap心得

(1)    元素特性

HashTable中的key、value都不能爲null;HashMap中的key、value可以爲null,很顯然只能有一個key爲null的鍵值對,但是允許有多個值爲null的鍵值對;TreeMap中當未實現 Comparator 接口時,key 不可以爲null;當實現 Comparator 接口時,若未對null情況進行判斷,則key不可以爲null,反之亦然。

 

(2)順序特性

HashTable、HashMap具有無序特性。TreeMap是利用紅黑樹來實現的(樹中的每個節點的值,都會大於或等於它的左子樹種的所有節點的值,並且小於或等於它的右子樹中的所有節點的值),實現了SortMap接口,能夠對保存的記錄根據鍵進行排序。所以一般需要排序的情況下是選擇TreeMap來進行,默認爲升序排序方式(深度優先搜索),可自定義實現Comparator接口實現排序方式。

 

(3)初始化與增長方式

初始化時:HashTable在不指定容量的情況下的默認容量爲11,且不要求底層數組的容量一定要爲2的整數次冪;HashMap默認容量爲16,且要求容量一定爲2的整數次冪。

擴容時:Hashtable將容量變爲原來的2倍加1;HashMap擴容將容量變爲原來的2倍。

 

(4)線程安全性

HashTable其方法函數都是同步的(採用synchronized修飾),不會出現兩個線程同時對數據進行操作的情況,因此保證了線程安全性。也正因爲如此,在多線程運行環境下效率表現非常低下。因爲當一個線程訪問HashTable的同步方法時,其他線程也訪問同步方法就會進入阻塞狀態。比如當一個線程在添加數據時候,另外一個線程即使執行獲取其他數據的操作也必須被阻塞,大大降低了程序的運行效率,在新版本中已被廢棄,不推薦使用。

HashMap不支持線程的同步,即任一時刻可以有多個線程同時寫HashMap;可能會導致數據的不一致。如果需要同步(1)可以用 Collections的synchronizedMap方法;(2)使用ConcurrentHashMap類,相較於HashTable鎖住的是對象整體, ConcurrentHashMap基於lock實現鎖分段技術。首先將Map存放的數據分成一段一段的存儲方式,然後給每一段數據分配一把鎖,當一個線程佔用鎖訪問其中一個段的數據時,其他段的數據也能被其他線程訪問。ConcurrentHashMap不僅保證了多線程運行環境下的數據訪問安全性,而且性能上有長足的提升。

 

(5)一段話HashMap

HashMap基於哈希思想,實現對數據的讀寫。當我們將鍵值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,讓後找到bucket位置來儲存值對象。當獲取對象時,通過鍵對象的equals()方法找到正確的鍵值對,然後返回值對象。HashMap使用鏈表來解決碰撞問題,當發生碰撞了,對象將會儲存在鏈表的下一個節點中。 HashMap在每個鏈表節點中儲存鍵值對對象。當兩個不同的鍵對象的hashcode相同時,它們會儲存在同一個bucket位置的鏈表中,可通過鍵對象的equals()方法用來找到鍵值對。如果鏈表大小超過閾值(TREEIFY_THRESHOLD, 8),鏈表就會被改造爲樹形結構。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章