<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                # Java簡答1 ## 語言類(概念) ### 面向對象思想 todo: * 面向對象、面向過程 * 面向對象的三大基本特征和五大基本原則 #### 面向過程思想概述 我們來回想一下,這幾天我們完成一個需求的步驟:首先是搞清楚我們要做什么,然后在分析怎么做,最后我們再代碼體現。一步一步去實現,而具體的每一步都需要我們去實現和操作。這些步驟相互調用和協作,完成我們的需求。在上面的每一個具體步驟中我們都是參與者,并且需要面對具體的每一個步驟和過程,這就是面向過程最直接的體現。那么什么是面向過程開發呢? 面向過程開發,其實就是面向著具體的每一個步驟和過程,把每一個步驟和過程完成,然后由這些功能方法相互調用,完成需求。 面向過程的代表語言:C語言 #### 面向對象思想概述 當需求單一,或者簡單時,我們一步一步去操作沒問題,并且效率也挺高。可隨著需求的更改,功能的增多,發現需要面對每一個步驟很麻煩了。這時就開始思索,能不能把這些步驟和功能在進行封裝,封裝時根據不同的功能,進行不同的封裝,功能類似的封裝在一起。這樣結構就清晰了很多。用的時候,找到對應的類就可以了。這就是面向對象的思想。 #### 面向對象思想特點 a:是一種更符合我們思想習慣的思想 b:可以將復雜的事情簡單化 c:將我們從執行者變成了指揮者,角色發生了轉換 ### Java基本特征 #### Java是如何實現跨平臺 這就要談及Java虛擬機(Java Virtual Machine,簡稱 JVM)。 JVM也是一個軟件,不同的平臺有不同的版本。我們編寫的Java源碼,編譯后會生成一種 .class 文件,稱為字節碼文件。Java虛擬機就是負責將字節碼文件翻譯成特定平臺下的機器碼然后運行。也就是說,只要在不同平臺上安裝對應的JVM,就可以運行字節碼文件,運行我們編寫的Java程序。 而這個過程中,我們編寫的Java程序沒有做任何改變,僅僅是通過JVM這一”中間層“,就能在不同平臺上運行,真正實現了”一次編譯,到處運行“的目的。 JVM是一個”橋梁“,是一個”中間件“,是實現跨平臺的關鍵,Java代碼首先被編譯成字節碼文件,再由JVM將字節碼文件翻譯成機器語言,從而達到運行Java程序的目的。 注意:編譯的結果不是生成機器碼,而是生成字節碼,字節碼不能直接運行,必須通過JVM翻譯成機器碼才能運行。不同平臺下編譯生成的字節碼是一樣的,但是由JVM翻譯成的機器碼卻不一樣。 所以,運行Java程序必須有JVM的支持,因為編譯的結果不是機器碼,必須要經過JVM的再次翻譯才能執行。即使你將Java程序打包成可執行文件(例如 .exe),仍然需要JVM的支持。 注意:跨平臺的是Java程序,不是JVM。JVM是用C/C++開發的,是編譯后的機器碼,不能跨平臺,不同平臺下需要安裝不同版本的JVM。面向對象思想 #### 為什么說 Java 中只有值傳遞 todo: * 值傳遞、引用傳遞 * 為什么說 Java 中只有值傳遞 ### Java的四大特性 封裝、繼承、多態、(抽象)。也有說三大特征的,沒了抽象 #### 封裝 通常我們將某事物的屬性和行為包裝到對象中,隱藏對象的屬性和實現細節,僅對外提供公共訪問方式。 這樣做有這些好處: - 隱藏實現細節,提供公共的訪問方式 - 提高代碼復用性 - 提高安全性[禁止對象之間的不良交互提高模塊化] #### 繼承 多個類中存在相同屬性和行為時,將這些內容抽取到單獨一個類中,那么多個類無需再定義這些屬性和行為,只要繼承那個類即可。通過使用繼承我們能夠非常方便地復用以前的代碼。 **繼承的好處** - a:提高了代碼的復用性 - b:提高了代碼的維護性 - c:讓類與類之間產生了關系,是多態的前提 **繼承的弊端** 使用繼承是,有一個很明顯高的弊端就是類的耦合性增強了。 **關于繼承如下 3 點請記住:** 1. 子類擁有父類非 private 的成員方法和成員變量)。 2. 子類不能繼承父類的構造方法,但是可以通過super關鍵字去訪問父類構造方法 3. 子類可以擁有自己屬性和方法,即子類可以對父類進行擴展。 4. 子類可以用自己的方式實現父類的方法(重寫)。 #### 多態 在Java中有兩種形式可以實現多態:繼承(多個子類對同一方法的重寫)和接口(實現接口并覆蓋接口中同一方法)。 多態是指程序中定義的引用變量所指向的具體類型和通過該引用變量發出的方法調用在編程時并不確定,而是在程序運行期間才確定,即一個引用變量倒底會指向哪個類的實例對象,該引用變量發出的方法調用到底是哪個類中實現的方法,必須在由程序運行期間才能決定。因為在程序運行時才確定具體的類,這樣,不用修改源程序代碼,就可以讓引用變量綁定到各種不同的類實現上,從而導致該引用調用的具體方法隨之改變,即不修改程序代碼就可以改變程序運行時所綁定的具體代碼,讓程序可以選擇多個運行狀態,這就是多態性 #### 抽象 現實世界的事物進行概括,抽象為在計算機虛擬世界中有意義的實體 ### 多態 多態性就是相同的消息使得不同的類做出不同的響應。 #### 多態實現條件 Java實現多態有三個必要條件:繼承、重寫、向上轉型: - **繼承**。在多態中必須存在有繼承關系的子類和父類。 - **重寫**。子類對父類中某些方法進行重新定義,在調用這些方法時就會調用子類的方法。 - **向上轉型**。在多態中需要將子類的引用賦給父類對象,只有這樣該引用才能夠具備技能調用父類的方法和子類的方法。 #### 多態的實現方式有哪些? todo:這幾句解釋,我自己都看不太明白。。 **1、基于繼承實現的多態** 基于繼承的實現機制,主要表現在父類和繼承該父類的一個或多個子類對某些方法的重寫,多個子類對同一方法的重寫可以表現出不同的行為。多態的表現就是不同的對象可以執行相同的行為,但是他們都需要通過自己的實現方式來執行,這就要得益于向上轉型了。 **2、基于接口實現的多態** 繼承是通過重寫父類的同一方法的幾個不同子類來體現的,那么就可就是通過實現接口并覆蓋接口中同一方法的幾不同的類體現的。 在接口的多態中,指向接口的引用必須是指定這實現了該接口的一個類的實例程序,在運行時,根據對象引用的實際類型來執行對應的方法。 繼承都是單繼承,只能為一組相關的類提供一致的服務接口。但是接口可以是多繼承多實現,它能夠利用一組相關或者不相關的接口進行組合與擴充,能夠對外提供一致的服務接口。所以它相對于繼承來說有更好的靈活性。 #### 多態有哪些弊端 todo: ### 接口和抽象類 #### 接口、抽象的區別 - 多繼承。一個類可以實現多個接口,但只能單繼承一個抽象類 - 方法。接口的方法默認是 public,所有方法在接口中不能有實現(Java 8 開始接口方法可以有默認實現),抽象類可以有非抽象的方法。 - 變量。接口的變量默認(必須)是 static final類型;抽象類則沒有限制 - 實例化。接口不能用 new 實例化,但可以聲明,但是必須引用一個實現該接口的對象 - 設計層面。設計層面來說,抽象是對類的抽象,是一種模板設計,接口是行為的抽象,是一種行為的規范。 #### 接口意義 接口更多的意義是 一種行為規范: * 2、簡單、規范性:如果一個項目比較龐大,那么就需要一個能理清所有業務的架構師來定義一些主要的接口,這些接口不僅告訴開發人員你需要實現那些業務,而且也將命名規范限制住了(防止一些開發人員隨便命名導致別的程序員無法看明白)。 * 3、維護、拓展性:比如你要做一個畫板程序,其中里面有一個面板類,主要負責繪畫功能,然后你就這樣定義了這個類。可是在不久將來,你突然發現這個類滿足不了你了,然后你又要重新設計這個類,更糟糕是你可能要放棄這個類,那么其他地方可能有引用他,這樣修改起來很麻煩。如果你一開始定義一個接口,把繪制功能放在接口里,然后定義類時實現這個接口,然后你只要用這個接口去引用實現它的類就行了,以后要換的話只不過是引用另一個類而已,這樣就達到維護、拓展的方便性。 * 4、安全、嚴密性:接口是實現軟件松耦合的重要手段,它描敘了系統對外的所有服務,而不涉及任何具體的實現細節。這樣就比較安全、嚴密一些(一般軟件服務商考慮的比較多)。 #### 抽象意義 抽象更多是為了復用 邏輯: - 1.因為抽象類不能實例化對象,所以必須要有子類來實現它之后才能使用。這樣就可以把一些具有相同屬性和方法的組件進行抽象,這樣更有利于代碼和程序的維護。 - 2.當又有一個具有相似的組件產生時,只需要實現該抽象類就可以獲得該抽象類的那些屬性和方法。 #### 如何選擇 從設計層面上看,抽象類提供了一種 IS-A 關系,那么就必須滿足里式替換原則,即子類對象必須能夠替換掉所有父類對象。而接口更像是一種 LIKE-A 關系,它只是提供一種方法實現契約,并不要求接口和實現接口的類具有 IS-A 關系 * 使用接口: * 需要讓不相關的類都實現一個方法,例如不相關的類都可以實現 Compareable 接口中的 compareTo() 方法; * 需要使用多重繼承。 * 使用抽象類: * 需要在幾個相關的類中共享代碼。 * 需要能控制繼承來的成員的訪問權限,而不是都為 public。 * 需要繼承非靜態和非常量字段。 ### 重載和重寫 ##### 重載和重寫的區別 **重載** - 發生在同一個類中, - 方法名必須相同,參數類型不同、個數不同、順序不同,方法返回值和訪問修飾符可以不同, - 發生在編譯時。 **重寫** - 發生在父子類中, - 方法名、參數列表必須相同, - 返回值范圍小于等于父類,拋出的異常范圍小于等于父類,訪問修飾符范圍大于等于父類; - 如果父類方法訪問修飾符為 private 則子類就不能重寫該方法。 ##### 重載和重寫綁定機制有何區別 **重載**:類內多態,靜態綁定機制(編譯時已經知道具體執行哪個方法),方法同名,參數不同 **重寫**:類間多態,動態綁定機制(運行時確定),實例方法,兩小兩同一大(方法簽名相同,子類的方法所拋出的異常、返回值的范圍不大于父類的對應方法,子類的方法可見性不小于父類的對應方法)方法簽名相同,子類的方法所拋出的異常、返回值的范圍不大于父類的對應方法,子類的方法可見性不小于父類的對應方法。 ##### 父類的靜態方法能否被子類重寫 - 父類的靜態方法是不能被子類重寫的,其實重寫只能適用于實例方法,不能用于靜態方法,對于上面這種靜態方法而言,我們應該稱之為隱藏。 - Java靜態方法形式上可以重寫,但從本質上來說不是Java的重寫。因為靜態方法只與類相關,不與具體實現相關。聲明的是什么類,則引用相應類的靜態方法(本來靜態無需聲明,可以直接引用)。并且static方法不是后期綁定的,它在編譯期就綁定了。換句話說,這個方法不會進行多態的判斷,只與聲明的類有關。 ### 常見修飾符 **Java中常見的修飾符** - 權限修飾符:private,默認的,protected,public - 狀態修飾符:static,final - 抽象修飾符:abstract **適用于類** - 權限修飾符:默認修飾符,public - 狀態修飾符:final、(內部類可用static) - 抽象修飾符:abstract **適用于構造方法** - 權限修飾符:private,默認的,protected,public **適用于成員變量** - 權限修飾符:private,默認的,protected,public - 狀態修飾符:static,final **適用于成員方法** - 權限修飾符:private,默認的,protected,public - 狀態修飾符:static,final - 抽象修飾符:abstract **范圍權限修飾符** java中的訪問權限修飾符:public、private、protected,已經默認的default 類的成員不寫訪問修飾時默認為default。默認對于同一個包中的其他類相當于公開(public),對于不是同一個包中的其他類相當于私有(private)。受保護(protected)對子類相當于公開,對不是同一包中的沒有父子關系的類相當于私有。 | 作用域 | 當前類 | 同包 | 子類 | 其他 | | --------- | :----: | :--: | :--: | :--: | | public | o | o | o | o | | protected | o | o | o | x | | default | o | o | x | x | | private | o | x | x | x | ### 內部類 關于這部分的詳細解答可以參考這邊文章[從反編譯深入理解JAVA內部類類結構以及final關鍵字](https://blog.csdn.net/Davidluo001/article/details/50377919) #### 定義 內部類就是定義在另外一個類里面的類。它隱藏在外部類中,封裝性更強,不允許除外部類外的其他類訪問它;但它可直接訪問外部類的成員(持有外類的this指針)。 #### 分類 **成員內部類** 成員內部類是外圍類的一個成員,是依附于外圍類的. 編譯后class文件的命名格式:主類+$+內部類名. 通過反編譯代碼,我門會發現,即便我們在定義的內部類的構造器是無參構造器,編譯器還是會默認添加一個參數,該參數的類型為指向外部類對象的一個引用,所以成員內部類中的Outter this&0 指針便指向了外部類對象,因此可以在成員內部類中隨意訪問外部類的成員。從這里也間接說明了成員內部類是依賴于外部類的, 這也引出另一個問題,如果在外部類沒有人引用的時候,而成員內部類有人引用,外部類因為被內部類引用所以不會被回收。這就會造成Android中常見的Activity內存泄露。 **靜態內部類** 靜態內部類,就是修飾為static的內部類,該內部類對象不依賴于外部類對象,就是說我們可以直接創建內部類對象,但其只可以直接訪問外部類的所有靜態成員和靜態方法; 編譯后class文件的命名格式:主類+.+內部類名 **匿名內部類** 匿名內部類: 就是局部內部類的簡化寫法。本質上來說,它是一個繼承了該類或者實現了該接口的子類匿名對象. 編譯后class文件的命名格式:主類+$+(1,2,3....) 常用格式如下: ``` new 類名或者接口名(){ 重寫方法; } ; ``` 定義匿名內部類的前提是,內部類必須要繼承一個類或者實現接口,格式為 new 父類或者接口(){定義子類的內容(如函數等)}。也就是說,匿名內部類最終提供給我們的是一個 匿名子類的對象。 **局部內部類** 局部內部類是嵌套在方法和作用域內的,對于這個類的使用主要是應用與解決比較復雜的問題,想創建一個類來輔助我們的解決方案,到那時又不希望這個類是公共可用的,所以就產生了局部內部類,局部內部類和成員內部類一樣被編譯,只是它的作用域發生了改變,它只能在該方法和屬性中被使用,出了該方法和屬性就會失效。 #### 靜態內部類和非靜態內部類的區別 靜態內部類與非靜態內部類之間存在一個最大的區別:**非靜態內部類在編譯完成之后會隱含地保存著一個引用,該引用是指向創建它的外圍內,但是靜態內部類卻沒有** 沒有這個引用就意味著: - 它的創建是不需要依賴于外圍類的。 - 它不能使用任何外圍類的非static成員變量和方法。 官方一點的語言就是: - 實例化。靜態內部類是指被聲明為static的內部類,可不依賴外部類實例化;而非靜態內部類需要通過生成外部類來間接生成。 - 訪問。靜態內部類只能訪問外部類的靜態成員變量和靜態方法,而非靜態內部類由于持有對外部類的引用,可以訪問外部類的所用成員 #### 為什么內部類調用的外部變量必須是final修飾的? **簡單解答** **生命周期**。由于方法中的局部變量的生命周期很短,一旦方法結束變量就要被銷毀,為了保證在內部類中能找到外部局部變量,通過final關鍵字可得到一個外部變量的引用; **一致性**。通過final關鍵字也不會在內部類去做修改該變量的值,保護了數據的一致性。 **詳細一點** **生命周期的原因**。方法中的局部變量(保存在棧幀中),方法結束后這個變量就要釋放掉,final保證這個變量始終指向一個對象。首先,內部類和外部類其實是處于同一個級別,內部類不會因為定義在方法中就會隨著方法的執行完畢而跟隨者被銷毀。問題就來了,如果外部類的方法中的變量不定義final,那么當外部類方法執行完畢的時候,這個局部變量肯定也就被GC了,然而內部類的某個方法還沒有執行完,這個時候他所引用的外部變量已經找不到了。 **數據一致性**。如果定義為final,java會將這個變量復制一份作為成員變量內置于內部類中,這樣的話,由于final所修飾的值始終無法改變,所以這個變量所指向的內存區域就不會變。為了解決:局部變量的生命周期與局部內部類的對象的生命周期的不一致性問題 這里先上一段代碼進行說明: ``` public void test(final int b) { final int a = 10; new Thread(){ public void run() { System.out.println(a); }; }.start(); } ``` **生命周期不一致** 方法中的局部變量(保存在棧幀中),當方法執行完畢之后,局部變量a的生命周期就結束了,而此時Thread對象的生命周期很可能還沒有結束,Thread的run方法中繼續訪問變量a就變成不可能了。 為了實現這樣的效果,Java采用了 **復制** 的手段來解決這個問題: * 如果局部變量的值在編譯期間就可以確定,則直接在匿名內部里面創建一個拷貝。 * 如果局部變量的值無法在編譯期間確定,則通過構造器傳參的方式來對拷貝進行初始化賦值。 但是新的問題又來了, 既然在run方法中訪問的變量a和test方法中的變量a不是同一個變量,當在run方法中改變變量a的值的話,會出現什么情況? 對,會造成數據不一致性,這樣就達不到原本的意圖和要求。為了解決這個問題,java編譯器就限定必須將變量a限制為final變量,不允許對變量a進行更改(對于引用類型的變量,是不允許指向新的對象),這樣數據不一致性的問題就得以解決了。 ### 變量 #### 靜態變量和實例變量的區別 **靜態變量**是被static修飾符修飾的變量,也稱為類變量,它屬于類,不屬于類的任何一個對象,一個類不管創建多少個對象,靜態變量在內存中有且僅有一個拷貝。靜態變量可以實現讓多個對象共享內存。在Java開發中,上下文類和工具類中通常會有大量的靜態成員。 **實例變量**必須依存于某一實例,需要先創建對象然后通過對象才能訪問到它 #### 成員變量與局部變量 **1.從語法形式上看** 成員變量是屬于類的,而局部變量是在方法中定義的變量或是方法的參數;成員變量可以被 public,private,static 等修飾符所修飾,而局部變量不能被訪問控制修飾符及 static 所修飾; 但是,成員變量和局部變量都能被 final 所修飾; **2.從變量在內存中的存儲方式來看** 成員變量是對象的一部分,而對象存在于堆內存,局部變量存在于棧內存 3.從變量在內存中的生存時間上看,成員變量是對象的一部分,它隨著對象的創建而存在,而局部變量隨著方法的調用而自動消失。 4.成員變量如果沒有被賦初值,則會自動以類型的默認值而賦值(一種情況例外被 final 修飾但沒有被 static 修飾的成員變量必須顯示地賦值);而局部變量則不會自動賦值。 ### 代碼塊 #### 定義 在Java中,使用{}括起來的代碼被稱為代碼塊。 根據其位置和聲明的不同,可以分為: * **局部代碼塊**。在方法中出現;限定變量生命周期,及早釋放,提高內存利用率 * **構造代碼塊**。類中直接用{}定義,每一次創建對象時執行;一般都是為了將多個構造方法方法中相同的代碼存放到一起,每次調用構造都執行,并且在構造方法前執行 * **靜態代碼塊**。在類中方法外出現,加了static修飾。jvm加載類時執行,僅執行一次。 * **同步代碼塊**。 #### 類內代碼塊執行順序 對于一個類而言,按照如下順序執行: * 執行靜態代碼塊 * 執行構造代碼塊 * 執行構造函數 對于靜態變量、靜態初始化塊、變量、初始化塊、構造器,它們的初始化順序依次是 * 靜態變量、靜態初始化塊 * 變量、初始化塊 * 構造器 上面三個等級中,同級的執行順序,根據在類的位置而定,至上而下執行。 #### 繼承類代碼塊的執行順序 ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g1obesqwh1j30gg0d1gme.jpg) 當涉及到繼承時,按照如下順序執行: 1. 執行父類的靜態代碼塊、初始化父類靜態成員變量。 2. 執行子類的靜態代碼塊、初始化子類靜態成員變量。 3. 執行父類的構造代碼塊、初始化父類普通成員變量;執行父類的構造函數。 4. 執行子類的構造代碼塊、 初始化子類普通成員變量;執行子類的構造函數。 #### 執行順序釋疑 這里通過對類加載過程的簡述來說明 上面提到的執行順序: 1. 訪問SubClass.main(),(這是一個static方法),于是裝載器就會為你尋找已經編譯的SubClass類的代碼(也就是SubClass.class文件)。在裝載的過程中,裝載器注意到它有一個基類(也就是extends所要表示的意思),于是它再裝載基類。不管你創不創建基類對象,這個過程總會發生。如果基類還有基類,那么第二個基類也會被裝載,依此類推。 2. 執行根基類的static初始化,然后是下一個派生類的static初始化,依此類推。這個順序非常重要,因為派生類的“static初始化”有可能要依賴基類成員的正確初始化。 3. 當所有必要的類都已經裝載結束,開始執行main()方法體,并用new SubClass()創建對象。 4. 類SubClass存在父類,則調用父類的構造函數,你可以使用super來指定調用哪個構造函數。基類的構造過程以及構造順序,同派生類的相同。首先基類中各個變量按照字面順序進行初始化,然后執行基類的構造函數的其余部分。 5. 對子類成員數據按照它們聲明的順序初始化,執行子類構造函數的其余部分。 ### 動態代理是基于什么原理? 36講-6 ## 語言類(具體) ### Java基本數據類型 | 類型 | 位寬 | 范圍 | | :------ | :------------ | :----------------- | | byte | 1個字節,8位 | -128到127之間 | | short | 2個字節,16位 | 2^15 到 2^15 -1 | | int | 4個字節,32位 | -2^31 到 2^31 -1 | | long | 8個字節,64位 | -2^63 到 2^63 -1 | | float | 4個字節,32位 | | | double | 8個字節,64位 | | | char | 2個字節,16位 | 1個漢字剛好2個字節 | | boolean | 1個字節 | true和false | ### 運算符 a+ +和- -a區別 ### Object公有方法 - `equals()`: 和==作用相似 - `hashCode()`:用于哈希查找,重寫了equals()一般都要重寫該方法 - `getClass()`: 獲取Class對象 - `wait()`:讓當前線程進入等待狀態,并釋放它所持有的鎖 - `notify()`、`notifyAll()`: 喚醒一個(所有)正處于等待狀態的線程 - `toString()`:轉換成字符串 - `clone()`:進行對象拷貝 ### 包裝類 #### int和Integer有什么區別 * Integer是int的包裝類,int則是java的一種基本數據類型 * Integer變量必須實例化后才能使用,而int變量不需要 * Integer實際是對象的引用,當new一個Integer時,實際上是生成一個指針指向此對象;而int則是直接存儲數據值 * Integer的默認值是null,int的默認值是0 #### 自動裝拆箱 **裝箱**:自動將 基本數據類型 轉換為 包裝器類型; **拆箱**:自動將 包裝器類型 轉換為 基本數據類型; ```java //拆箱 int yc = 5; //裝箱 Integer yc = 5; ``` #### JDK中如何操作裝箱、拆箱 在JDK中,裝箱過程是通過調用包裝器的`valueOf`方法實現的,而拆箱過程是通過調用包裝器的`xxxValue`方法實現的(xxx代表對應的基本數據類型)。 Integer、Short、Byte、Character、Long 這幾個類的valueOf方法的實現是類似的,有限可列舉,共享常量池[-128,127]; Double、Float的valueOf方法的實現是類似的,無限不可列舉,不共享; Boolean的valueOf方法的實現不同于以上的整型和浮點型,只有兩個值,有限可列舉,共享; #### 什么時候裝箱/拆箱 什么時候拆箱主要取決于:在當前場景下,你需要的是引用類型還是基本類型。若需要引用類型,但傳進來的值是基本類型,則自動裝箱(例如,使用equals方法時傳進來原生類型的值);若需要的是原生類型,但傳進來的值是引用類型,則自動拆箱(例如,使用運算符進行運算時,操作數是包裝類型) #### 為何要引用基本數據包裝類? **原始數據類型和引用類型局限性**: * 原始數據類型和 Java 泛型并不能配合使用 * Java 的泛型某種程度上可以算作偽泛型,它完全是一種編譯期的技巧,Java 編譯期會自動將類型轉換為對應的特定類型,這就決定了使用泛型,必須保證相應類型可以轉換為Object。 **為何要引用基本數據包裝類** 就比如,我們使用泛型,需要用到基本數據類型的包裝類。 Java 的對象都是引用類型,如果是一個原始數據類型數組,它在內存里是一段連續的內存,而對象數組則不然,數據存儲的是引用,對象往往是分散地存儲在堆的不同位置。這種設計雖然帶來了極大靈活性,但是也導致了數據操作的低效,尤其是無法充分利用現代 CPU 緩存機制。 Java 為對象內建了各種多態、線程安全等方面的支持,但這不是所有場合的需求,尤其是數據處理重要性日益提高,更加高密度的值類型是非常現實的需求。 #### 基本類型的緩沖池 基本類型對應的緩沖池如下: boolean values true and false all byte values short values between -128 and 127 int values between -128 and 127 char in the range \u0000 to \u007F 在使用這些基本類型對應的包裝類型時,就可以直接使用緩沖池中的對象。 #### 包裝類int的常見問題 1、由于Integer變量實際上是對一個Integer對象的引用,所以兩個通過new生成的Integer變量永遠是不相等的(因為new生成的是兩個對象,其內存地址不同)。 ```java Integer i = new Integer(100); Integer j = new Integer(100); System.out.print(i == j); //false ``` 2、Integer變量和int變量比較時,只要兩個變量的值是向等的,則結果為true(因為包裝類Integer和基本數據類型int比較時,java會自動拆包裝為int,然后進行比較,實際上就變為兩個int變量的比較) ```java Integer i = new Integer(100); int j = 100; System.out.print(i == j); //true ``` 3、非new生成的Integer變量和new Integer()生成的變量比較時,結果為false。(因為非new生成的Integer變量指向的是java常量池中的對象,而new Integer()生成的變量指向堆中新建的對象,兩者在內存中的地址不同) ```java Integer i = new Integer(100); Integer j = 100; System.out.print(i == j); //false ``` 4、對于兩個非new生成的Integer對象,進行比較時,如果兩個變量的值在區間-128到127之間,則比較結果為true,如果兩個變量的值不在此區間,則比較結果為false ```java Integer i = 100; Integer j = 100; System.out.print(i == j); //true Integer i = 128; Integer j = 128; System.out.print(i == j); //false ``` 對于第4條的原因: java在編譯Integer i = 100 ;會自動裝箱,會翻譯成為Integer i = Integer.valueOf(100);,而java API中對Integer類型的valueOf的定義如下: ```java public static Integer valueOf(int i){ assert IntegerCache.high >= 127; if (i >= IntegerCache.low && i <= IntegerCache.high){ return IntegerCache.cache[i + (-IntegerCache.low)]; } return new Integer(i); } ``` java對于-128到127之間的數,會進行緩存,Integer i = 127時,會將127進行緩存,下次再寫Integer j = 127時,就會直接從緩存中取,就不會new了 在 Java 8 中,Integer 緩存池的大小默認為 -128~127。 ```java static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } ``` #### AtomicInteger底層實現原理是什么? 36講-22 ### 深克隆、淺克隆 #### 如何實現對象克隆 - 有兩種方式: - 1.實現Cloneable接口并重寫Object類中的clone()方法; - 2.實現Serializable接口,通過對象的序列化和反序列化實現克隆,可以實現真正的深度克隆,代碼如下。 - 注意問題: - 基于序列化和反序列化實現的克隆不僅僅是深度克隆,更重要的是通過泛型限定,可以檢查出要克隆的對象是否支持序列化,這項檢查是編譯器完成的,不是在運行時拋出異常,這種是方案明顯優于使用Object類的clone方法克隆對象。 - 代碼如下所示 ```java public class MyUtil { private MyUtil() { throw new AssertionError(); } public static <T> T clone(T obj) throws Exception { ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bout); oos.writeObject(obj); ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bin); return (T) ois.readObject(); // 說明:調用ByteArrayInputStream或ByteArrayOutputStream對象的close方法沒有任何意義 // 這兩個基于內存的流只要垃圾回收器清理對象就能夠釋放資源 } } class CloneTest { public static void main(String[] args) { try { Person p1 = new Person("Hao LUO", 33, new Car("Benz", 300)); Person p2 = MyUtil.clone(p1); // 深度克隆 p2.getCar().setBrand("BYD"); // 修改克隆的Person對象p2關聯的汽車對象的品牌屬性 // 原來的Person對象p1關聯的汽車不會受到任何影響 // 因為在克隆Person對象時其關聯的汽車對象也被克隆了 System.out.println(p1); } catch (Exception e) { e.printStackTrace(); } } } ``` 通過反射獲得泛型的實際類型參數 - 把泛型變量當成方法的參數,利用Method類的getGenericParameterTypes方法來獲取泛型的實際類型參數 - 例子: ```java public class GenericTest { public static void main(String[] args) throws Exception { getParamType(); } /*利用反射獲取方法參數的實際參數類型*/ public static void getParamType() throws NoSuchMethodException{ Method method = GenericTest.class.getMethod("applyMap",Map.class); //獲取方法的泛型參數的類型 Type[] types = method.getGenericParameterTypes(); System.out.println(types[0]); //參數化的類型 ParameterizedType pType = (ParameterizedType)types[0]; //原始類型 System.out.println(pType.getRawType()); //實際類型參數 System.out.println(pType.getActualTypeArguments()[0]); System.out.println(pType.getActualTypeArguments()[1]); } /*供測試參數類型的方法*/ public static void applyMap(Map<Integer,String> map){ } } ``` - 輸出結果: ```java java.util.Map<java.lang.Integer, java.lang.String> interface java.util.Map class java.lang.Integer class java.lang.String ``` #### ### 泛型 關于泛型更詳細的說明,可以查看[專案-淺談泛型] #### 泛型由來 我們的集合可以存儲多種數據類型的元素,那么在存儲的時候沒有任何問題,但是在獲取元素,并向下轉型的時候,可能會存在一個錯誤,而這個錯誤就是ClassCastException . 很顯然,集合的這種可以存儲多種數據類型的元素的這個特點,不怎么友好 , 程序存在一些安全隱患,那么為了出來這種安全隱患,我們應該限定一個集合存儲元素的數據類型,我們只讓他存儲統一中數據類型的元素,那么在做向下轉型的是就不會存在這種安全隱患了. 怎么限定集合只能給我存儲同一種數據類型的元素呢? 需要使用泛型。 泛型的使用把類型明確的工作推遲到創建對象或者調用方法的時候才去明確的特殊的類型。參數化類型,把類型當作參數一樣的傳遞。 泛型的出現減少了很多強轉的操作,同時避免了很多運行時的錯誤,在編譯期完成檢查類型轉化 **好處**: - (1):把運行時期的問題提前到了編譯期間 - (2):避免了強制類型轉換 - (3):優化了程序設計,解決了黃色警告線 #### 什么是泛型擦除? 泛型是只是提供給javac編譯器使用的,用來限定集合的輸入類型,編譯器編譯帶類型說明的集合時會擦掉“類型”信息。 簡單來說,泛型僅在編譯器有效,編譯生成的.class文件中,并沒有相關的類型信息。 #### 泛型通配符 **為什么要使用通配符?** 通配符的設計存在一定的場景,例如在使用泛型后,首先聲明了一個Animal的類,而后聲明了一個繼承Animal類的Cat類,顯然Cat類是Animal類的子類,但是List卻不是List的子類型,而在程序中往往需要表達這樣的邏輯關系。為了解決這種類似的場景,在泛型的參數類型的基礎上新增了通配符的用法。 **<? extends T> 上界通配符** 上界通配符顧名思義,<? extends T>表示的是類型的上界(**包含自身**),因此通配的參數化類型可能是T或T的子類。正因為無法確定具體的類型是什么,add方法受限(可以添加null,因為null表示任何類型),但可以從列表中獲取元素后賦值給父類型。 **<? super T> 下界通配符** 下界通配符<? super T> ,表示的是參數化類型是T的超類型(**包含自身**),層層至上,直至Object,編譯器無從判斷get()返回的對象的類型是什么,因此get()方法受限。但是可以進行add()方法,add()方法可以添加T類型和T類型的子類型 **<?> 無界通配符** 任意類型,如果沒有明確,那么就是Object以及任意的Java類了 無界通配符用<?>表示,?代表了任何的一種類型,能代表任何一種類型的只有null(Object本身也算是一種類型,但卻不能代表任何一種類型,所以List和List的含義是不同的,前者類型是Object,也就是繼承樹的最上層,而后者的類型完全是未知的) #### 如何獲取泛型的具體的類型 把泛型變量當成方法的參數,利用Method類的getGenericParameterTypes方法來獲取泛型的實際類型參數 ```java public class GenericTest { public static void main(String[] args) throws Exception { getParamType(); } /*利用反射獲取方法參數的實際參數類型*/ public static void getParamType() throws NoSuchMethodException{ Method method = GenericTest.class.getMethod("applyMap",Map.class); //獲取方法的泛型參數的類型 Type[] types = method.getGenericParameterTypes(); System.out.println(types[0]); //參數化的類型 ParameterizedType pType = (ParameterizedType)types[0]; //原始類型 System.out.println(pType.getRawType()); //實際類型參數 System.out.println(pType.getActualTypeArguments()[0]); System.out.println(pType.getActualTypeArguments()[1]); } /*供測試參數類型的方法*/ public static void applyMap(Map<Integer,String> map){ } } //輸出結果 // java.util.Map<java.lang.Integer, java.lang.String> // interface java.util.Map // class java.lang.Integer // class java.lang.String ``` ### 線程安全類型 #### 如何驗證int類型是否線程安全? int類型并不是線程安全的。int 作為基本類型,直接存儲在內存棧,且對其進行+,-操作以及++,–操作都不是原子操作,都有可能被其他線程搶斷,所以不是線程安全。int 用于單線程變量存取,開銷小,速度快。 ```java int count = 0; private void startThread() { for (int i = 0;i < 200; i++){ new Thread(new Runnable() { @Override public void run() { for (int k = 0; k < 50; k++){ count++; } } }).start(); } // 休眠10秒,以確保線程都已啟動 try { Thread.sleep(1000*10); } catch (InterruptedException e) { e.printStackTrace(); }finally { Log.e("打印日志----",count+""); } } //期望輸出10000,最后輸出的是9818 //注意:打印日志----: 9818 ``` #### 哪些類型是線程安全的? Java自帶的線程安全的基本類型包括: AtomicInteger, AtomicLong, AtomicBoolean, AtomicIntegerArray,AtomicLongArray等 如何實現? AtomicInteger類中有有一個變量valueOffset,用來描述AtomicInteger類中value的內存位置 。當需要變量的值改變的時候,先通過get()得到valueOffset位置的值,也即當前value的值.給該值進行增加,并賦給next。compareAndSet()比較之前取到的value的值當前有沒有改變,若沒有改變的話,就將next的值賦給value,倘若和之前的值相比的話發生變化的話,則重新一次循環,直到存取成功,通過這樣的方式能夠保證該變量是線程安全的 value使用了volatile關鍵字,使得多個線程可以共享變量,使用volatile將使得VM優化失去作用,在線程數特別大時,效率會較低。 ```java private static AtomicInteger atomicInteger = new AtomicInteger(1); static Integer count1 = Integer.valueOf(0); private void startThread1() { for (int i = 0;i < 200; i++){ new Thread(new Runnable() { @Override public void run() { for (int k = 0; k < 50; k++){ // getAndIncrement: 先獲得值,再自增1,返回值為自增前的值 count1 = atomicInteger.getAndIncrement(); } } }).start(); } // 休眠10秒,以確保線程都已啟動 try { Thread.sleep(1000*10); } catch (InterruptedException e) { e.printStackTrace(); }finally { Log.e("打印日志----",count1+""); } } //期望輸出10000,最后輸出的是10000 //注意:打印日志----: 10000 //AtomicInteger使用了volatile關鍵字進行修飾,使得該類可以滿足線程安全。 private volatile int value; public AtomicInteger(int initialValue) { value = initialValue; } ``` ### hashcode與equal #### hashcode與equal區別 * equals()比較兩個對象的地址值是否相等 ;hashCode()得到的是對象的存儲位置,可能不同對象會得到相同值 * 有兩個對象,若equals()相等,則hashcode()一定相等;hashcode()不等,則equals()一定不相等;hashcode()相等,equals()可能相等、可能不等 * 使用equals()比較兩個對象是否相等,效率較低,最快辦法是先用hashCode()比較,如果hashCode()不相等,則這兩個對象肯定不相等;如果hashCode()相等,此時再用equal()比較,如果equal()也相等,則這兩個對象的確相等。 #### 為什么要同時重寫hashCode和equals兩個方法 #### 請手寫equal方法,講講具體的原理? 代碼如下所示,如果是手寫代碼,一定要弄清楚邏輯思路! ```java public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String) anObject; int n = count; if (n == anotherString.count) { int i = 0; while (n-- != 0) { if (charAt(i) != anotherString.charAt(i)) return false; i++; } return true; } } return false; } ``` ### final、finally、finalize #### 各自作用 * final可以修飾類,方法,變量 * final修飾類代表類不可以繼承拓展 * final修飾變量表示變量不可以修改 * final修飾方法表示方法不可以被重寫 * finally則是Java保證重點代碼一定要被執行的一種機制 * 可以使用 try-finally 或者 try-catch-finally 來進行類似關閉 JDBC連接、保證 unlock 鎖等動作。 * finalize 是基礎類 java.lang.Object的一個方法 * 它的設計目的是保證對象在被垃圾收集前完成特定資源的回收。finalize 機制現在已經不推薦使用,并且在 JDK 9開始被標記為 deprecated。 #### final **final修飾類、方法** 將方法或者類聲明為 final,這樣就可以明確告知別人,這些行為是不許修改的。 如果你關注過 Java 核心類庫的定義或源碼, 有沒有發現java.lang 包下面的很多類,相當一部分都被聲明成為final class。在第三方類庫的一些基礎類中同樣如此,這可以有效避免 API 使用者更改基礎功能,某種程度上,這是保證平臺安全的必要手段。 **final修飾變量** 使用 final 修飾參數或者變量,也可以清楚地避免意外賦值導致的編程錯誤,甚至,有人明確推薦將所有方法參數、本地變量、成員變量聲明成 final。 final 變量產生了某種程度的不可變(immutable)的效果,所以,可以用于保護只讀數據,尤其是在并發編程中,因為明確地不能再賦值 final 變量,有利于減少額外的同步開銷,也可以省去一些防御性拷貝的必要。 #### finally 關于finally塊詳細作用,可以查看[專案-淺談Java異常] 在以下幾種特殊情況下,finally代碼塊不會被執行: - 1.在finally語句塊中發生了異常。 - 2.在前面的代碼中用了System.exit()退出程序。 - 3.程序所在的線程死亡。 - 4.關閉CPU。 ### String字符串 String、StringBuffer、StringBuilder #### String的創建機制 由于String在Java世界中使用過于頻繁,Java為了避免在一個系統中產生大量的String對象,引入了字符串常量池。其運行機制是:創建一個字符串時,首先檢查池中是否有值相同的字符串對象,如果有則不需要創建直接從池中剛查找到的對象引用;如果沒有則新建字符串對象,返回對象引用,并且將新創建的對象放入池中。但是,通過new方法創建的String對象是不檢查字符串池的,而是直接在堆區或棧區創建一個新的對象,也不會把對象放入池中。上述原則只適用于通過直接量給String對象引用賦值的情況。 舉例: ```java String str1 = "123"; //通過直接量賦值方式,放入字符串常量池 String str2 = new String(“123”);//通過new方式賦值方式,不放入字符串常量池 ``` 區別: * 通過String a=""直接賦值的方式得到的是一個字符串常量,存在于常量池;注意,相同內容的字符串在常量池中只有一個,即如果池已包含內容相等的字符串會返回池中的字符串,反之會將該字符串放入池中。 * 通過new String("")創建的字符串不是常量是實例對象,會在堆內存開辟空間并存放數據,且每個實例對象都有自己的地址空間 #### String的不可變性 這里主要回答這幾個問題: * 為什么Java中的 String 是不可變的(Immutable)? * 字符串設計和實現考量? * String不可變的好處? **不可變類String的原因** * String主要的三個成員變量 char value[], int offset, int count均是private,final的,并且沒有對應的 getter/setter; * String 對象一旦初始化完成,上述三個成員變量就不可修改;并且其所提供的接口任何對這些域的修改都將返回一個新對象; * 是典型的 Immutable 類,被聲明成為 final class,所有屬性也都是final的。也由于它的不可變,類似拼接、裁剪字符串等動作,都會產生新的 String 對象。 **字符串設計和實現考量?** * String 是 Immutable 類的典型實現,原生的保證了基礎線程安全,因為你無法對它內部數據進行任何修改,這種便利甚至體現在拷貝構造函數中,由于不可變,Immutable 對象在拷貝時不需要額外復制數據。 * 為了實現修改字符序列的目的,StringBuffer 和 StringBuilder 底層都是利用可修改的(char,JDK 9 以后是 byte)數組,二者都繼承了 AbstractStringBuilder,里面包含了基本操作,區別僅在于最終的方法是否加了 synchronized。 * 這個內部數組應該創建成多大的呢?如果太小,拼接的時候可能要重新創建足夠大的數組;如果太大,又會浪費空間。目前的實現是,構建時初始字符串長度加 16(這意味著,如果沒有構建對象時輸入最初的字符串,那么初始值就是 16)。我們如果確定拼接會發生非常多次,而且大概是可預計的,那么就可以指定合適的大小,避免很多次擴容的開銷。擴容會產生多重開銷,因為要拋棄原有數組,創建新的(可以簡單認為是倍數)數組,還要進行arraycopy。 **String不可變的好處?** * **可以緩存 hash 值** 因為 String 的 hash 值經常被使用,例如 String 用做 HashMap 的 key。不可變的特性可以使得 hash 值也不可變,因此只需要進行一次計算。 * **String Pool 的需要** 如果一個String對象已經被創建過了,那么就會從 String Pool 中取得引用。只有 String 是不可變的,才可能使用 String Pool。 * **安全性(一致性)** String 經常作為參數,String 不可變性可以保證參數不可變。例如在作為網絡連接參數的情況下如果 String 是可變的,那么在網絡連接過程中,String 被改變,改變 String 對象的那一方以為現在連接的是其它主機,而實際情況卻不一定是。 * **線程安全** String 不可變性天生具備線程安全,可以在多個線程中安全地使用。 #### String演進歷史 - String 在 Java 6 以后提供了 intern()方法,目的是提示 JVM 把相應字符串緩存起來,以備重復使用。在我們創建字符串對象并調用 intern() 方法的時候,如果已經有緩存的字符串,就會返回緩存里的實例,否則將其緩存起來。 - 在后續版本中,這個緩存被放置在堆中,這樣就極大避免了永久代占滿的問題,甚至永久代在 JDK 8 中被 MetaSpace(元數據區)替代了。而且,默認緩存大小也在不斷地擴大中,從最初的 1009,到 7u40 以后被修改為 60013。 - Java9中,String實現由巨大變化 todo #### String、StringBuffer、StringBuilder的區別 區別: **可變性** 簡單的來說:,String是字符串常量,而StringBuffer、StringBuilder都是字符串變量,即String對象一創建后不可更改,而后兩者的對象是可更改的。 String 類中使用 final 關鍵字字符數組保存字符串,`private final char value[]`,所以 String 對象是不可變的。而StringBuilder 與 StringBuffer 都繼承自 AbstractStringBuilder 類,在 AbstractStringBuilder 中也是使用字符數組保存字符串`char[]value`但是沒有用 final 關鍵字修飾,所以這兩種對象都是可變的。 StringBuilder 與 StringBuffer 的構造方法都是調用父類構造方法也就是 AbstractStringBuilder 實現的,大家可以自行查閱源碼。 AbstractStringBuilder.java ~~~java abstract class AbstractStringBuilder implements Appendable, CharSequence { char[] value; int count; AbstractStringBuilder() { } AbstractStringBuilder(int capacity) { value = new char[capacity]; } ~~~ **線程安全性** String 中的對象是final不可變的,也就可以理解為常量,線程安全。AbstractStringBuilder 是 StringBuilder 與 StringBuffer 的公共父類,定義了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 對方法加了同步鎖或者對調用的方法加了同步鎖,所以是線程安全的。StringBuilder 并沒有對方法進行加同步鎖,所以是非線程安全的。 **性能** 每次對 String 類型進行改變的時候,都會生成一個新的 String 對象,然后將指針指向新的 String 對象。StringBuffer 每次都會對 StringBuffer 對象本身進行操作,而不是生成新的對象并改變對象引用。相同情況下使用 StringBuilder 相比使用 StringBuffer 僅能獲得 10%~15% 左右的性能提升,但卻要冒多線程不安全的風險。 **對于三者使用的總結:** 1. 操作少量的數據 = String 2. 單線程操作字符串緩沖區下操作大量數據 = StringBuilder 3. 多線程操作字符串緩沖區下操作大量數據 = StringBuffer #### substring的原理及區別 #### replaceFirst、replaceAll、replace 區別 #### String 對“+”的重載、字符串拼接的幾種方式和區別 #### String.valueOf 和 Integer.toString 的區別 #### switch 對 String 的支持 ### static #### static關鍵字可以修飾什么? 可以用來修飾:成員變量,成員方法,代碼塊,內部類等。具體如下所示: * **修飾成員變量和成員方法** * 被 static 修飾的成員屬于類,不屬于單個這個類的某個對象,被類中所有對象共享,可以并且建議通過類名調用。 * 被 static 聲明的成員變量屬于靜態成員變量,靜態變量存放在Java內存區域的方法區 * **靜態代碼塊** * 靜態代碼塊定義在類中方法外,靜態代碼塊在非靜態代碼塊之前執行(靜態代碼塊—>非靜態代碼塊—>構造方法) * 該類不管創建多少對象,靜態代碼塊只執行一次 * **靜態內部類(static修飾類的話只能修飾內部類)** * **靜態導包(用來導入類中的靜態資源,1.5之后的新特性)** * 這兩個關鍵字連用可以指定導入某個類中的指定靜態資源,并且不需要使用類名調用類中靜態成員,可以直接使用類中靜態成員變量和成員方法。 #### static關鍵字的特點 靜態內部類與非靜態內部類之間存在一個最大的區別:非靜態內部類在編譯完成之后會隱含地保存著一個引用,該引用是指向創建它的外圍內,但是靜態內部類卻沒有。 沒有這個引用就意味著: * 1.它的創建是不需要依賴外圍類的創建。 * 2.它不能使用任何外圍類的非static成員變量和方法。 此外,還有下面使用區別: * 隨著類的加載而加載 * 優先于對象存在 * 被類的所有對象共享 * 可以通過類名調用【靜態修飾的內容一般我們稱其為:與類相關的,類成員】 * 在靜態方法中是沒有this關鍵字的(this指針) #### static編譯時有啥不同 todo:待添加 ## 數據結構 ### 常用數據結構簡介 #### 這里先用幾張圖,大致介紹下數據結構: 簡單來說 ![image](https://upload-images.jianshu.io/upload_images/4432347-95c1a4cac03f1510.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 具體分類 ![image](https://upload-images.jianshu.io/upload_images/4432347-84cb744b434c0bc3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) Java中的數據結構 ![image](https://upload-images.jianshu.io/upload_images/4432347-99e078e9f90f1366.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) **數據結構說明** | 類型 | 簡述 | | ---------- | ------------------------------------------------------------ | | 數組(無序) | 優點:查詢快,如果知道索引可以快速地存取 <br />缺點:刪除慢,大小固定 | | 數組(有序) | 優點:比無序數組查找快 <br />缺點:刪除和插入慢,大小固定 | | 棧 | 優點:提供后進先出的存取方式 <br />缺點:存取其他項很慢 | | 隊列 | 優點:提供先進先出的存取方式 <br />缺點:存取其他項都很慢 | | 鏈表 | 優點:插入快,刪除快 <br />缺點:查找慢(一個個節點查) | | 二叉樹 | 優點:查找,插入,刪除都快(平衡二叉樹) <br />缺點:刪除算法復雜 | | 紅黑樹 | 優點:查找,插入,刪除都快,樹總是平衡的(局部調整) <br />缺點:算法復雜 | | 哈希表 | 優點:如果關鍵字已知則存取速度極快,插入快 <br />缺點:刪除慢,如果不知道關鍵字則存取很慢,對存儲空間使用不充分 | | 堆 | 優點:插入,刪除快,對最大數據的項存取很快 <br />缺點:對其他數據項存取很慢 | | 圖 | 優點:對現實世界建模 <br />缺點:有些算法慢且復雜 | ### List #### Vector、ArrayList、LinkedList有什么區別 36講-8 ### Map #### Hashtable、HashMap、TreeMap有什么不同 36講-9 ### Set #### Set 和 List 區別? #### Set 如何保證元素不重復? ### 其他問題 #### System.arraycopy()、Arrays.copyOf() 先來看看方法定義 **System.arraycopy()** 主要是 從源數組A的指定位置index1開始,復制指定長度的元素,到目標數組的制定位置index2。 ```java //System.arraycopy() FastNative public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length); ``` **Arrays.copyOf()** 根據原數組、傳入長度, 系統自動在內部新建一個數組,并返回該數組。 ```java public static <T> T[] copyOf(T[] original, int newLength) {} ``` #### Collection 和 Collections 區別 Arrays.asList 獲得的 List 使用時需要注意什么 #### 如何保證集合線程安全? 36講-10 #### ConcurrentHashMap如何實現高效地線程安全 36講-10 #### fail-fast 和 fail-safe ## 數據結構(具體) ### SparseArray SparseArray位于android.util包下,是Android 對移動端專門優化的的數據結構,類似于 HashMap,key:int ,value:object(是的,你沒看錯,key只能是int)。 內部實現上,SpareArray中key 和 value 分別采用int[]和object[]兩個數組進行存儲。存儲 key 的數組是 int 類型,不需要進行裝箱操作,提供了速度保障。同理,還有 LongSparseArray, BooleanSparseArray 等,都是用來通過減少裝箱操作來節省內存空間的。 因為它內部使用二分查找尋找鍵,所以其效率不如 HashMap 高;在數據量比較少(不超過1000)的情況下,性能會好過 HashMap,數據量大的時候考慮使用HashMap。 添加元素。先采用二分查找法 找到位置,在執行插入,所以兩個數組是按照從小到大進行排序的。 查找元素。進行二分查找,數據量少的情況下,速度比較快。 關于SpareArray更詳細的分析,可以查看[淺談SpareArray]() ### HashMap hashmap 如何 put 數據(從 hashmap 源碼角度講解)? (掌握 put 元素的邏輯) 可以參考這里的簡單寫法(https://juejin.im/post/5c6e97e65188256559172b51) ### HashSet ### TreeMap ## 枚舉 ### 枚舉的用法、枚舉的實現、枚舉與單例、Enum 類 ### Java 枚舉如何比較 ### switch 對枚舉的支持 ### 枚舉的序列化如何實現 ### 枚舉的線程安全性問題 ## 異常 ### 1、Exception和Error有什么區別 36講-2 ### 2、try-catch應該包括大段代碼嗎 這里會嘗試說明編譯期對 try-catch代碼塊的處理,才能說明 ### 3、如何合理、高效的使用自定義異常 ## 反射 ## IO ### Java中IO體系 Java IO流共涉及40多個類,這些類看上去很雜亂,但實際上很有規則,而且彼此之間存在非常緊密的聯系, Java Io流的40多個類都是從如下4個抽象類基類中派生出來的。 - **InputStream/Reader**: 所有的輸入流的基類,前者是字節輸入流,后者是字符輸入流。 - **OutputStream/Writer**: 所有輸出流的基類,前者是字節輸出流,后者是字符輸出流。 **IO流的分類** - 按照流的流向分,可以分為輸入流和輸出流; - 按照操作單元劃分,可以劃分為字節流和字符流; - 按照流的角色劃分為節點流和處理流。 按操作方式分類結構圖: ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0ggoexq4qj30k00u0tab.jpg) 按操作對象分類結構圖 ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0ggqb56c3j30k00ev0tp.jpg) ### 字節流、字符流 #### bit、byte、char區別 | 名稱 | 內容 | | :--- | :----------------------------------------------------------- | | bit | Bit是最小的二進制單位 ,是計算機的操作部分 取值0或者1 | | byte | Byte是計算機操作數據的最小單位由8位bit組成 取值(-128-127) | | char | Char是用戶的可讀寫的最小單位,在Java里面由16位bit組成 取值(0-65535) | #### 字節流、字符流區別 **簡單點說** * 底層設備永遠只接受字節數據。 * 由于字節流操作中文不是特別方便,所以,java就提供了字符流。 * 字符流 = 字節流 + 編碼表 **扯淡點說** 把二進制數據數據逐一輸出到某個設備中,或者從某個設備中逐一讀取一片二進制數據,不管輸入輸出設備是什么,我們要用統一的方式來完成這些操作,用一種抽象的方式進行描述,這個抽象描述方式起名為IO流,對應的抽象類為OutputStream和InputStream ,不同的實現類就代表不同的輸入和輸出設備,它們都是針對字節進行操作的。 在應用中,經常要完全是字符的一段文本輸出去或讀進來,用字節流可以嗎?計算機中的一切最終都是二進制的字節形式存在。對于“中國”這些字符,首先要得到其對應的字節,然后將字節寫入到輸出流。讀取時,首先讀到的是字節,可是我們要把它顯示為字符,我們需要將字節轉換成字符。由于這樣的需求很廣泛,人家專門提供了字符流的包裝類。 底層設備永遠只接受字節數據,有時候要寫字符串到底層設備,需要將字符串轉成字節再進行寫入。字符流是字節流的包裝,字符流則是直接接受字符串,它內部將串轉成字節,再寫入底層設備,這為我們向IO設別寫入或讀取字符串提供了一點點方便。 #### 如何選擇字節流或者字符流? 字符流是由Java虛擬機將字節轉化為2個字節的Unicode字符為單位的字符而成的 - 如果是音頻文件、圖片、歌曲,就用字節流好點(避免數據丟失) - 如果是關系到中文(文本)的,用字符流好點 #### 緩沖區 緩沖區就是一段特殊的內存區域,很多情況下當程序需要頻繁地操作一個資源(如文件或數據庫)則性能會很低,所以為了提升性能就可以將一部分數據暫時讀寫到緩存區,以后直接從此區域中讀寫數據即可,這樣就顯著提升了性能。 對于 Java 字符流的操作都是在緩沖區操作的,所以如果我們想在字符流操作中主動將緩沖區刷新到文件則可以使用 flush() 方法操作。 #### 字符流的讀寫方式 5種寫數據方式: ```java public void write(int c) public void write(char[] cbuf) public void write(char[] cbuf,int off,int len) public void write(String str) public void write(String str,int off,int len) ``` 2種讀數據方式 ```java public int read() public int read(char[] cbuf) ``` ### IO流中使用到的設計模式? #### IO流中用到哪些模式 Todo 這里需要手動補全下 大概有裝飾者模式和適配器模式! 要知道裝飾者模式和適配器模式的作用;其次,可以自己舉個例子把它的作用生動形象地講出來;最后,簡要說一下要完成這樣的功能需要什么樣的條件 #### 談一談IO流中兩種設計模式的區別 **裝飾器模式** 裝飾器模式,就是動態地給一個對象添加一些額外的職責,來實現對原有功能的擴展。 他會有以下三個特征: 1. 它必須持有一個被裝飾的對象(作為成員變量)。 2. 它必須擁有與被裝飾對象相同的接口(多態調用、擴展需要)。 3. 它可以給被裝飾對象添加額外的功能。 這里以java中IO為例 ```java //把InputStreamReader裝飾成BufferedReader來成為具備緩沖能力的Reader。 BufferedReader bufferedReader = new BufferedReader(inputStreamReader); ``` 比如,在io流中,FilterInputStream類就是裝飾角色,它實現了InputStream類的所有接口,并持有InputStream的對象實例的引用,BufferedInputStream是具體的裝飾器實現者,這個裝飾器類的作用就是使得InputStream讀取的數據保存在內存中,而提高讀取的性能。 **適配器模式** 適配器模式,將一個類的接口轉換成客戶期望的另一個接口,讓原本不兼容的接口可以合作無間。 會有以下幾個特征: 1. 適配器對象實現原有接口 2. 適配器對象組合一個實現新接口的對象 3. 對適配器原有接口方法的調用被委托給新接口的實例的特定方法(重寫舊接口方法來調用新接口功能) ```java //把FileInputStream文件字節流適配成InputStreamReader字符流來操作文件字符串。 FileInputStream fileInput = new FileInputStream(file); InputStreamReader inputStreamReader = new InputStreamReader(fileInput); ``` 比如,在io流中,InputStreamReader類繼承了Reader接口,但要創建它必須在構造函數中傳入一個InputStream的實例,InputStreamReader的作用也就是將InputStream適配到Reader。InputStreamReader實現了Reader接口,并且持有了InputStream的引用。這里,適配器就是InputStreamReader類,而源角色就是InputStream代表的實例對象,目標接口就是Reader類 **兩者的區別** 在Java IO設計中: * **適配器模式**:主要在于將一個接口轉變成另一個接口,它的目的是通過改變接口來達到重復使用的目的; * **裝飾器模式**:不是要改變被裝飾對象的接口,而是保持原有的接口,但是增強原有對象的功能,或改變原有對象的方法而提高性能。 ### IO流案例 #### 字節流復制 一次讀取一個字節 ```java public static void main(String[] args) throws IOException { /** * 復制文本文件: * 讀和寫 * 分析: * 1: 創建兩個對象一個是字節輸入流對象,一個是字節輸出流對象 * 2: 一次讀取一個字節,一次寫一個字節 * 3: 釋放資源 */ // 創建兩個對象一個是字節輸入流對象,一個是字節輸出流對象 FileInputStream fis = new FileInputStream("FileOutputStreamDemo.java") ; FileOutputStream fos = new FileOutputStream("copyFile.java") ; // 一次讀取一個字節,一次寫一個字節 // int by = 0 ; // while((by = fis.read()) != -1){ // fos.write(by) ; // } // 一次讀取一個字節數組復制文件 byte[] bytes = new byte[1024] ; int len = 0 ; // 作用: 記錄讀取到的有效的字節個數 while((len = fis.read(bytes)) != -1){ fos.write(bytes, 0, len) ; } // 釋放資源 fos.close() ; fis.close() ; } ``` #### BufferedInputStream讀取數據 ```java public static void main(String[] args) throws IOException { /** * BufferedInputStream構造方法: * public BufferedInputStream(InputStream in) */ BufferedInputStream bis = new BufferedInputStream(new FileInputStream("e.txt")) ; // 一次讀取一個字節 //int by = 0 ; //while((by = bis.read()) != -1){ // System.out.print((char)by); //} // 一次讀取一個字節數組 byte[] bytes = new byte[1024] ; int len = 0 ; while((len = bis.read(bytes)) != -1){ System.out.print(new String(bytes , 0 , len)); } // 釋放資源 bis.close() ; } ``` #### BufferedOutputStream寫出數據 ```java public static void main(String[] args) throws IOException { /** * BufferedOutputStream構造方法: * public BufferedOutputStream(OutputStream out) */ // 創建FileOutputStream對象 //FileOutputStream fos = new FileOutputStream("buf.txt") ; //BufferedOutputStream bof = new BufferedOutputStream(fos) ; BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("buf.txt")) ; // 調用方法 bos.write("哈哈,我來了".getBytes()) ; // 釋放資源 bos.close() ; } ``` #### 轉換流OutputStreamWriter的使用 ```java /** * 字符輸出流: OutputStreamWriter (轉換輸出流) 字符流通向字節流的橋梁 * 構造方法: * public OutputStreamWriter(OutputStream out): 使用默認字符集 * public OutputStreamWriter(OutputStream out , String charsetName): 使用指定的字符集 * 字符輸入流: InputStreamReader (轉換輸入流) 字節流通向字符流的橋梁 * 構造方法: * public InputStreamReader(InputStream in):使用的默認的編碼表(GBK) * public InputStreamReader(InputStream in , String charsetName):使用指定的編碼表 */ public class OutputStreamWriterDemo { public static void main(String[] args) throws IOException { // 創建: OutputStreamWriter // 創建: OutputStream的對象 // FileOutputStream fos = new FileOutputStream("a.txt") ; // OutputStreamWriter osw = new OutputStreamWriter(fos) ; // OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("a.txt")) ; // public InputStreamReader(InputStream in , String charsetName) 使用指定的編碼表 OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("a.txt") , "UTF-8") ; // 調用方法 osw.write("中國") ; // 釋放資源 osw.close() ; } } ``` #### 轉換流InputStreamReader的使用 ```java public class InputStreamReaderDemo { public static void main(String[] args) throws IOException { // 創建對象InputStreamReader的對象 // public InputStreamReader(InputStream in): 使用的默認的編碼表(GBK) // FileInputStream fis = new FileInputStream("a.txt") ; // InputStreamReader isr = new InputStreamReader(fis) ; // 默認的字符集就是GBK // InputStreamReader isr = new InputStreamReader(new FileInputStream("a.txt")) ; // public InputStreamReader(InputStream in , String charsetName) 使用指定的編碼表 InputStreamReader isr = new InputStreamReader(new FileInputStream("a.txt") , "utf-8") ; // 讀取數據 int ch = 0 ; while((ch = isr.read()) != -1){ System.out.print((char)ch); } // 釋放資源 isr.close() ; } } ``` #### 字符流復制 ```java public static void main(String[] args) throws IOException { // 創建轉換輸入流對象 InputStreamReader isr = new InputStreamReader(new FileInputStream("OutputStreamWriterDemo.java")) ; // 創建轉換輸出流對象 OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("copyFile.java")) ; // 復制文件 // 一次讀取一個字符復制 // int ch = 0 ; // while((ch = isr.read()) != -1){ // osw.write(ch) ; // } // 一次讀取一個字符數組復制文件 char[] chs = new char[1024] ; int len = 0 ; while((len = isr.read(chs)) != -1){ osw.write(chs, 0, len) ; } // 釋放資源 osw.close() ; isr.close() ; } ``` #### FileWriter和FileReader復制 ```java public static void main(String[] args) throws IOException { // 創建高效的字符輸入流對象 BufferedReader br = new BufferedReader(new FileReader("OutputStreamWriterDemo.java")) ; // 創建高效的字符輸出流對象 BufferedWriter bw = new BufferedWriter(new FileWriter("copyFile3.java")) ; // 一次讀取一個字符數組復制文件 char[] chs = new char[1024] ; int len = 0; while((len = br.read(chs)) != -1){ bw.write(chs, 0, len) ; } // 釋放資源 bw.close() ; br.close() ; } ``` #### 字符緩沖流的特殊功能復制文本 ```java public static void main(String[] args) throws IOException { /** * 需求: 使用高效的字符流中特有的功能復制文本文件 */ // 創建高效的字符輸入流對象 BufferedReader br = new BufferedReader(new FileReader("OutputStreamWriterDemo.java")) ; // 高效的字符輸出流對象 BufferedWriter bw = new BufferedWriter(new FileWriter("copyFile4.java")) ; // 復制文件 // 一次讀取一行復制文件 String line = null ; while((line = br.readLine()) != null) { bw.write(line) ; bw.newLine() ; bw.flush() ; } // 釋放資源 bw.close() ; br.close() ; } ``` #### 四種方式復制MP3并測試效率 這里通過四種方式來演示復制效率: - 基本字節流一次讀寫一個字節 - 基本字節流一次讀寫一個字節數組 - 高效字節流一次讀寫一個字節 - 高效字節流一次讀寫一個字節數組 ```java public class CopyFileDemo { public static void main(String[] args) throws IOException { // 獲取開始時間 long startTime = System.currentTimeMillis() ; // 復制文件 copyFile_4() ; // 獲取結束時間 long endTime = System.currentTimeMillis() ; // 輸出 System.out.println("復制文件使用的時間是:" + (endTime - startTime) + "毫秒"); } /** * 基本流一次讀取一個字節復制文件 * @throws IOException */ private static void copyFile_1() throws IOException { // 復制文件使用的時間是:88670毫秒 // 創建對象 FileInputStream fis = new FileInputStream("C:\\a.avi") ; FileOutputStream fos = new FileOutputStream("D:\\a.avi") ; // 一次讀取一個字節 int by = 0 ; while((by = fis.read()) != -1){ fos.write(by) ; } // 釋放資源 fos.close() ; fis.close() ; } /** * 基本流一次讀取一個字節數組復制文件 * @throws IOException */ private static void copyFile_2() throws IOException { // 復制文件使用的時間是:130毫秒 // 創建對象 FileInputStream fis = new FileInputStream("C:\\a.avi") ; FileOutputStream fos = new FileOutputStream("D:\\a.avi") ; // 一次讀取一個字節數組 byte[] bytes = new byte[1024] ; int len = 0 ; while((len = fis.read(bytes)) != -1){ fos.write(bytes, 0, len) ; } // 釋放資源 fos.close() ; fis.close() ; } /** * 高效流一次讀取一個字節復制文件 * @throws IOException */ public static void copyFile_3() throws IOException { // 復制文件使用的時間是:990毫秒 // 創建高效流對象 BufferedInputStream bis = new BufferedInputStream(new FileInputStream("C:\\a.avi")) ; BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\a.avi")) ; // 一次讀取一個字節 int by = 0 ; while((by = bis.read()) != -1){ bos.write(by) ; } // 釋放資源 bos.close() ; bis.close() ; } /** * 高效流一次讀取一個字節數組賦值文件 * @throws IOException */ public static void copyFile_4() throws IOException { // 復制文件使用的時間是:50毫秒 // 創建高效流對象 BufferedInputStream bis = new BufferedInputStream(new FileInputStream("C:\\a.avi")) ; BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\a.avi")) ; // 一次讀取一個字節數組 byte[] bytes = new byte[1024] ; int len = 0 ; while((len = bis.read(bytes)) != -1){ bos.write(bytes, 0, len) ; } // 釋放資源 bos.close() ; bis.close() ; } } ``` #### 把集合中的數據存儲到文本文件 ```java public static void main(String[] args) throws IOException { /** * 把ArrayList集合中的數據存儲到文本文件中 * 分析: * 1: 創建ArrayList集合對象 * 2: 添加數據 * 3: 創建高效的字符輸出流對象 * 4: 遍歷集合,獲取每一個元素,然后通過流對象寫出去 * 5: 釋放資源 */ // 創建ArrayList集合對象 ArrayList<String> al = new ArrayList<String>() ; // 添加數據 al.add("西施") ; al.add("貂蟬") ; al.add("楊玉環") ; al.add("王昭君") ; // 創建高效的字符輸出流對象 BufferedWriter bw = new BufferedWriter(new FileWriter("names.txt")) ; // 遍歷集合,獲取每一個元素,然后通過流對象寫出去 for(String name : al) { bw.write(name) ; bw.newLine() ; bw.flush() ; } // 釋放資源 bw.close() ; } ``` #### 把文本文件中的數據存儲到集合中 ```java public static void main(String[] args) throws IOException { /** * 從文本文件中讀取數據(每一行為一個字符串數據)到集合中,并遍歷集合 * 分析: * 1: 創建高效的字符輸入流對象 * 2: 創建集合對象 * 3: 讀取文本文件中的數據,將數據存儲到集合中 * 4: 釋放資源 * 5: 遍歷集合 */ // 1: 創建高效的字符輸入流對象 BufferedReader br = new BufferedReader(new FileReader("names.txt")) ; // 2: 創建集合對象 ArrayList<String> al = new ArrayList<String>() ; // 3: 讀取文本文件中的數據,將數據存儲到集合中 String line = null ; // 作用: 用來記錄讀取到的行數據 while((line = br.readLine()) != null) { al.add(line) ; } // 4: 釋放資源 br.close() ; // 5: 遍歷集合 for(String name : al) { System.out.println(name); } } ``` #### 隨機獲取文本文件中的姓名 ```java public static void main(String[] args) throws IOException { // 1: 創建集合對象 ArrayList<String> students = new ArrayList<String> () ; // 2: 創建BufferedReader對象 BufferedReader br = new BufferedReader(new FileReader("students.txt")) ; // 3: 讀取數據,把數據存儲到集合中 String line = null ; while((line = br.readLine()) != null) { students.add(line) ; } // 4: 釋放資源 br.close() ; // 5: 生成一個隨機數 Random random = new Random() ; int index = random.nextInt(students.size()); // 6: 把生成的隨機數作為集合元素的索引,來獲取一個元素 String name = students.get(index) ; // 7: 把獲取到的元素打印到控制臺 System.out.println(name); } ``` #### 復制單級文件夾 ```java public static void main(String[] args) throws IOException { /** * 需求: 把C:\\course這個文件夾復制到D:\\course盤下 * 分析: * 1: 把C:\\course這個目錄封裝成一個File對象 * 2: 把D:\\course這個目錄封裝成一個File對象 * 3: 判斷D:\\course是否存在,如果存在就創建一個文件夾 * 4: 獲取C:\\course這個目錄下所有的文件對應的File數組 * 5: 遍歷數組,獲取元素進行復制 */ // 把C:\\course這個目錄封裝成一個File對象 File srcFolder = new File("C:\\course") ; // 把D:\\course這個目錄封裝成一個File對象 File destFolder = new File("D:\\course") ; // 判斷D:\\course是否存在,如果存在就創建一個文件夾 if(!destFolder.exists()){ destFolder.mkdir() ; } // 復制文件夾 IOUtils.copyFolder(srcFolder, destFolder, null) ; } ``` #### 復制指定目錄下指定后綴名的文件并修改名稱 ```java public static void main(String[] args) throws IOException { /** * 把C:\\demo這個目錄下所有的以.java結尾的文件復制到D:\\demo中,然后將這個文件的后綴名更改為.jad */ // 把C:\\demo這個目錄下所有的以.java結尾的文件復制到D:\\demo中 // 1: 把C:\\demo這個目錄封裝成一個File對象 File srcFolder = new File("C:\\demo") ; // 2: 把D:\\demo這么目錄封裝成一個File對象 File destFolder = new File("D:\\demo") ; // 3: 判斷D:\\demo這個路徑是否存在 if(!destFolder.exists()) { destFolder.mkdir() ; } // 調用方法 IOUtils.copyFolder(srcFolder, destFolder, new FilenameFilter() { @Override public boolean accept(File dir, String name) { return new File(dir , name).isFile() && name.endsWith(".java") ; } }) ; System.out.println("-----------------------------------------------------"); // 獲取destFolder下所有的文件對應的File數組 File[] files = destFolder.listFiles() ; for(File f : files) { // 創建目標文件名稱 String destFileName = f.getName().replace(".java", ".jad") ; // 創建目標文件 File destFile = new File(destFolder , destFileName) ; // 調用 f.renameTo(destFile) ; } } ``` 上面兩步用到的工具類 ```java public class IOUtils { public static void copyFolder(String srcPahtName , String destPathName , FilenameFilter filenameFilter) throws IOException { File srcFolder = new File(srcPahtName) ; File destFolder = new File(destPathName) ; if(!destFolder.exists()) { destFolder.mkdir() ; } copyFolder(srcFolder , destFolder , filenameFilter) ; } public static void copyFolder(File srcFolder , File destFolder , FilenameFilter filenameFilter) throws IOException { File[] files = null ; if(filenameFilter == null) { files = srcFolder.listFiles() ; }else { files = srcFolder.listFiles(filenameFilter) ; } // 遍歷 for(File f : files) { // 創建目標文件 String destFileName = f.getName() ; File destFile = new File(destFolder , destFileName) ; // 復制文件 copyFile(f , destFile) ; } } public static void copyFile(File srcFile , File destFile) throws IOException { // 創建流對象 BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile)) ; BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile)) ; // 一次讀取一個字節數組復制文件 byte[] bytes = new byte[1024] ; int len = 0 ; while((len = bis.read(bytes)) != -1){ bos.write(bytes, 0, len) ; } // 釋放資源 bos.close() ; bis.close() ; } } ``` #### 鍵盤錄入學生信息按照總分排序并寫入文本文件 ```java public static void main(String[] args) throws IOException { /** * 需求:鍵盤錄入3個學生信息(姓名,語文成績(chineseScore),數學成績(mathScore),英語成績(englishScore)),按照總分從高到低存入文本文件 * 分析: * 1: 創建一個學生類 * 2: 創建一個集合對象TreeSet集合 * 3: 鍵盤錄入學生信息,把學生信息封裝到學生對象中,然后把學生對象添加到集合中 * 4: 創建一個高效的字符輸出流對象 * 5: 遍歷集合,獲取每一個元素,把其信息寫入到文件中 * 6: 釋放資源 */ // 創建一個集合對象TreeSet集合 TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() { @Override public int compare(Student s1, Student s2) { // 總分 int num = s2.getTotalScore() - s1.getTotalScore() ; // 比較姓名 int num2 = (num == 0) ? s2.getName().compareTo(s1.getName()) : num ; // 返回 return num2; } }) ; // 3: 鍵盤錄入學生信息,把學生信息封裝到學生對象中,然后把學生對象添加到集合中 for(int x = 1 ; x <= 3 ; x++) { // 創建Scanner對象 Scanner sc = new Scanner(System.in) ; System.out.println("請您輸入第" + x + "個學生的姓名" ); String sutName = sc.nextLine() ; System.out.println("請您輸入第" + x + "個學生的語文成績" ); int chineseScore = sc.nextInt() ; System.out.println("請您輸入第" + x + "個學生的數學成績" ); int mathScore = sc.nextInt() ; System.out.println("請您輸入第" + x + "個學生的英語成績" ); int englishScore = sc.nextInt() ; // 把學生的信封裝到一個學生對象中 Student s = new Student() ; s.setName(sutName) ; s.setMathScore(mathScore) ; s.setChineseScore(chineseScore) ; s.setEnglishScore(englishScore) ; // 把學生的信息添加到集合中 ts.add(s) ; } // 創建一個高效的字符輸出流對象 BufferedWriter bw = new BufferedWriter(new FileWriter("student.info")) ; bw.write("==========================================學生的信息如下====================================================") ; bw.newLine() ; bw.flush() ; bw.write("姓名\t\t總分\t\t數學成績\t\t語文成績\t\t英語成績\t\t") ; bw.newLine() ; bw.flush() ; for(Student t : ts) { bw.write(t.getName() + "\t\t" + t.getTotalScore() + "\t\t" + t.getMathScore() + "\t\t" + t.getChineseScore() + "\t\t" + t.getEnglishScore()) ; bw.newLine() ; bw.flush() ; } // 釋放資源 bw.close() ; } ``` ### java提供了那些IO方式? 36講-11 ### Java中IO與NIO #### IO與NIO簡述 傳統的IO流是阻塞式的,會一直監聽一個ServerSocket,在調用read等方法時,它會一直等到數據到來或者緩沖區已滿時才返回。調用accept也是一直阻塞到有客戶端連接才會返回。每個客戶端連接過來后,服務端都會啟動一個線程去處理該客戶端的請求。并且多線程處理多個連接。每個線程擁有自己的棧空間并且占用一些CPU時間。每個線程遇到外部未準備好的時候,都會阻塞掉。阻塞的結果就是會帶來大量的進程上下文切換。 而對于NIO,它是非阻塞式,核心類: 1. Buffer為所有的原始類型提供 (Buffer)緩存支持。 2. Charset字符集編碼解碼解決方案 3. Channel一個新的原始I/O抽象,用于讀寫Buffer類型,通道可以認為是一種連接,可以是到特定設備,程序或者是網絡的連接。 #### IO與NIO的主要區別 | IO | NIO | | :----- | :------- | | 面向流 | 面向緩沖 | | 阻塞IO | 非阻塞IO | | 無 | 選擇器 | **面向流與面向緩沖** Java IO和NIO之間第一個最大的區別是,IO是面向流的,NIO是面向緩沖區的。JavaIO面向流意味著每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方。此外,它不能前后移動流中的數據。如果需要前后移動從流中讀取的數據,需要先將它緩存到一個緩沖區。JavaNIO的緩沖導向方法略有不同。數據讀取到一個它稍后處理的緩沖區,需要時可在緩沖區中前后移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩沖區中包含所有您需要處理的數據。而且,需確保當更多的數據讀入緩沖區時,不要覆蓋緩沖區里尚未處理的數據。 **阻塞與非阻塞IO** Java IO的各種流是阻塞的。這意味著,當一個線程調用read()或write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再干任何事情了。JavaNIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什么都不會獲取,而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。線程通常將非阻塞IO的空閑時間用于在其它通道上執行IO操作,所以一個單獨的線程現在可以管理多個輸入和輸出通道(channel) **選擇器** Java NIO的選擇器允許一個單獨的線程來監視多個輸入通道,你可以注冊多個通道使用一個選擇器,然后使用一個單獨的線程來“選擇”通道:這些通道里已經有可以處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道。 ### NIO如何實現多路復用 36講-11 ### Java有幾種文件拷貝方式?那一種最高效 36講-12 ## JVM ### 字節碼 #### 什么是字節碼 采用字節碼的最大好處是什么 先看下 java 中的編譯器和解釋器: Java 中引入了虛擬機的概念,即在機器和編譯程序之間加入了一層抽象的虛擬的機器。這臺虛擬的機器在任何平臺上都提供給編譯程序一個的共同的接口。 編譯程序只需要面向虛擬機,生成虛擬機能夠理解的代碼,然后由解釋器來將虛擬機代碼轉換為特定系統的機器碼執行。在 Java 中,這種供虛擬機理解的代碼叫做`字節碼`(即擴展名為`.class`的文件),它不面向任何特定的處理器,只面向虛擬機。 每一種平臺的解釋器是不同的,但是實現的虛擬機是相同的。Java 源程序經過編譯器編譯后變成字節碼,字節碼由虛擬機解釋執行,虛擬機將每一條要執行的字節碼送給解釋器,解釋器將其翻譯成特定機器上的機器碼,然后在特定的機器上運行。這也就是解釋了 Java 的編譯與解釋并存的特點。 Java 源代碼---->編譯器---->jvm 可執行的 Java 字節碼(即虛擬指令)---->jvm---->jvm 中解釋器----->機器可執行的二進制機器碼---->程序運行。 - 采用字節碼的好處 **Java 語言通過字節碼的方式,在一定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特點。所以 Java 程序運行時比較高效,而且,由于字節碼并不專對一種特定的機器,因此,Java程序無須重新編譯便可在多種不同的計算機上運行。** > 解釋型語言:解釋型語言,是在運行的時候將程序翻譯成機器語言。解釋型語言的程序不需要在運行前編譯,在運行程序的時候才翻譯,專門的解釋器負責在每個語句執行的時候解釋程序代碼。這樣解釋型語言每執行一次就要翻譯一次,效率比較低。——百度百科 #### Java對象模型 ### JVM內存結構、內存模型、對象模型簡介 這里先說幾篇文章 - [JVM內存結構 VS Java內存模型 VS Java對象模型](https://mp.weixin.qq.com/s/mJVkLn2I1O7V8jvxc_Z4zw) - [再有人問你Java內存模型是什么,就把這篇文章發給他](https://mp.weixin.qq.com/s/ME_rVwhstQ7FGLPVcfpugQ) - [再有人問你synchronized是什么,就把這篇文章發給他](https://mp.weixin.qq.com/s/tI_4nCIg1kkcf6_UW1aA5A) - [再有人問你volatile是什么,就把這篇文章發給他](https://mp.weixin.qq.com/s/jSDAHKHWogeNU41ZS-fUwA) - [再有人問你volatile是什么,把這篇文章也發給他](https://mp.weixin.qq.com/s/aOQUnuf2_V_XehOxi2FdSQ) JVM內存結構、Java內存模型、Java對象模型 Java作為一種面向對象的,跨平臺語言,其對象、內存等一直是比較難的知識點。而且很多概念的名稱看起來又那么相似,很多人會傻傻分不清楚。比如本文我們要討論的**JVM內存結構**、**Java內存模型**和**Java對象模型**,這就是三個截然不同的概念,但是很多人容易弄混。 我們先區分下JVM內存結構、 Java內存模型 以及 Java對象模型 三個概念。 - **JVM內存結構**,和Java虛擬機的運行時區域有關。 - **Java內存模型**,和Java的并發編程有關。 - **Java對象模型**,和Java對象在虛擬機中的表現形式有關。 #### JVM內存結構 我們都知道,Java代碼是要運行在虛擬機上的,而虛擬機在執行Java程序的過程中會把所管理的內存劃分為若干個不同的數據區域,這些區域都有各自的用途。 其中有些區域隨著虛擬機進程的啟動而存在,而有些區域則依賴用戶線程的啟動和結束而建立和銷毀。在《Java虛擬機規范(Java SE 8)》中描述了JVM運行時內存區域結構如下: ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0hukgabb0j30ir09qjs1.jpg) 各個區域的功能不是本文重點,就不在這里詳細介紹了。這里簡單提幾個需要特別注意的點: 1、以上是Java虛擬機規范,不同的虛擬機實現會各有不同,但是一般會遵守規范。 2、規范中定義的方法區,只是一種概念上的區域,并說明了其應該具有什么功能。但是并沒有規定這個區域到底應該處于何處。所以,對于不同的虛擬機實現來說,是有一定的自由度的。 3、不同版本的方法區所處位置不同,上圖中劃分的是邏輯區域,并不是絕對意義上的物理區域。因為某些版本的JDK中方法區其實是在堆中實現的。 4、運行時常量池用于存放編譯期生成的各種字面量和符號應用。但是,Java語言并不要求常量只有在編譯期才能產生。比如在運行期,String.intern也會把新的常量放入池中。 5、除了以上介紹的JVM運行時內存外,還有一塊內存區域可供使用,那就是直接內存。Java虛擬機規范并沒有定義這塊內存區域,所以他并不由JVM管理,是利用本地方法庫直接在堆外申請的內存區域。 6、堆和棧的數據劃分也不是絕對的,如HotSpot的JIT會針對對象分配做相應的優化。 如上,做個總結,JVM內存結構,由Java虛擬機規范定義。描述的是Java程序執行過程中,由JVM管理的不同數據區域。各個區域有其特定的功能。 #### Java內存模型 Java內存模型看上去和Java內存結構(JVM內存結構)差不多,很多人會誤以為兩者是一回事兒,這也就導致面試過程中經常答非所為。 在前面的關于JVM的內存結構的圖中,我們可以看到,其中Java堆和方法區的區域是多個線程共享的數據區域。也就是說,多個線程可能可以操作保存在堆或者方法區中的同一個數據。這也就是我們常說的“Java的線程間通過共享內存進行通信”。 Java內存模型是根據英文Java Memory Model(JMM)翻譯過來的。其實JMM并不像JVM內存結構一樣是真實存在的。他只是一個抽象的概念。JSR-133: Java Memory Model and Thread Specification 中描述了,JMM是和多線程相關的,他描述了一組規則或規范,這個規范定義了一個線程對共享變量的寫入時對另一個線程是可見的。 那么,簡單總結下,Java的多線程之間是通過共享內存進行通信的,而由于采用共享內存進行通信,在通信過程中會存在一系列如可見性、原子性、順序性等問題,而JMM就是圍繞著多線程通信以及與其相關的一系列特性而建立的模型。JMM定義了一些語法集,這些語法集映射到Java語言中就是volatile、synchronized等關鍵字。 在JMM中,我們把多個線程間通信的共享內存稱之為主內存,而在并發編程中多個線程都維護了一個自己的本地內存(這是個抽象概念),其中保存的數據是主內存中的數據拷貝。而JMM主要是控制本地內存和主內存之間的數據交互的。 ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0hummkjv6j30br0aha9y.jpg) 在Java中,JMM是一個非常重要的概念,正是由于有了JMM,Java的并發編程才能避免很多問題。這里就不對Java內存模型做更加詳細的介紹了,想了解更多的朋友可以參考《Java并發編程的藝術》。 #### Java對象模型 Java是一種面向對象的語言,而Java對象在JVM中的存儲也是有一定的結構的。而這個關于Java對象自身的存儲模型稱之為Java對象模型。 HotSpot虛擬機中,設計了一個OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通對象指針,而Klass用來描述對象實例的具體類型。 每一個Java類,在被JVM加載的時候,JVM會給這個類創建一個`instanceKlass`,保存在方法區,用來在JVM層表示該Java類。當我們在Java代碼中,使用new創建一個對象的時候,JVM會創建一個`instanceOopDesc`對象,這個對象中包含了對象頭以及實例數據。 ```java class Model{ public static int a = 1; public int b; public Model(int b) { this.b = b; } } public static void main(String[] args) { int c = 10; Model modelA = new Model(2); Model modelB = new Model(3); } ``` ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0huo0t3v0j30u00dg0u1.jpg) ### Java對象模型 > 關于java對象模型更詳細的描述,可以查看[2_Java的對象模型][ref_2_Java的對象模型] m #### 36講-29 ### 對象內存圖 關于java對象模型更詳細的資料可以查看[][] **創建對象過程** - (1):加載A.class文件進內存 - (2):在棧內存為s開辟空間 - (3):在堆內存為學生對象開辟空間 - (4):對學生對象的成員變量進行默認初始化 - (5):對學生對象的成員變量進行顯示初始化 - (6):通過構造方法對學生對象的成員變量賦值 - (7):學生對象初始化完畢,把對象地址賦值給s變量 **一個對象的內存圖** ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0gi55vtqpj30xp0fg74b.jpg) **兩個對象的內存圖** ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0gi5rtkasj30yg0fw3z5.jpg) **三個對象的內存圖** ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0gi5y2j34j30yg0fw3z0.jpg) #### 談談JVM內存區域的劃分,哪些區域可能發生OutOfMemoryError 36講-25 ### java中的引用 #### 引用區別:強引用、軟引用、弱引用、幻象引用 36講-4 ### 垃圾回收 #### GC機制淺談 #### 談談GC調優思路 36講-28 #### Java常見的垃圾收集器有哪些? 36講-27 ### 編譯優化 #### JVM優化java代碼時都做了什么 36講-35 #### 有人說“Lambda能讓java程序慢30倍”,你怎么看? 36講-34 ### 類加載 #### 請介紹類加載過程,什么是雙親委派模型? 36講-23 ## 多線程 ### 線程基礎 #### 進程與線程 **什么是進程?** 進程就是正在運行的程序,是系統進行資源分配和調用的獨立單位。每一個進程都有它自己的內存空間和系統資源。 **多進程的意義** 單進程計算機只能做一件事情。而我們現在的計算機都可以一邊玩游戲(游戲進程),一邊聽音樂(音樂進程),所以我們常見的操作系統都是多進程操作系統。比如:Windows,Mac和Linux等,能在同一個時間段內執行多個任務。 對于單核計算機來講,游戲進程和音樂進程是同時運行的嗎?不是。 因為CPU在某個時間點上只能做一件事情,計算機是在游戲進程和音樂進程間做著頻繁切換,且切換速度很快, 所以,我們感覺游戲和音樂在同時進行,其實并不是同時執行的。 多進程的作用不是提高執行速度,而是提高CPU的使用率(CPU的處理速度遠快于存儲的速度)。 **什么是線程** 線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。 線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程包含以下內容: 一個指向當前被執行指令的指令指針; 一個棧; 一個寄存器值的集合,定義了一部分描述正在執行線程的處理器狀態的值 一個私有的數據區 **多線程有什么意義** 多線程的作用不是提高執行速度,而是為了提高應用程序的使用率。 那么怎么理解這個問題呢? 我們程序在運行的使用,都是在搶CPU的時間片(執行權),如果是多線程的程序,那么在搶到CPU的執行權的概率應該比較單線程程序搶到的概率要大.那么也就是說,CPU在多線程程序中執行的時間要比單線程多,所以就提高了程序的使用率.但是即使是多線程程序,那么他們中的哪個線程能搶占到CPU的資源呢,這個是不確定的,所以多線程具有隨機性。 **并行和并發** **并行**是**邏輯**上同時發生,指在某一個時間**內**,同時運行多個程序。 **并發**是**物理**上同時發生,指在某一個時間**點**,同時運行多個程序。 #### 線程與JVM **Java程序運行原理** Java命令會啟動java虛擬機,啟動JVM,等于啟動了一個應用程序,也就是啟動了一個進程。 該進程會自動啟動一個 “主線程” ,然后主線程去調用某個類的 main 方法。所以 main方法運行在主線程中。 **JVM的啟動是多線程的嗎** JVM啟動至少啟動了垃圾回收線程和主線程,所以是多線程的。 #### 線程實現兩種方式 java提供了兩種方式來實現多線程Thread和Runnable。 **繼承Thread** 第一種方式 繼承Thread的步驟: 1: 定義一個類,讓該類去繼承Thread類 2: 重寫run方法 3: 創建該類的對象 4: 啟動線程 ```java public class ThreadDemo { public static void main(String[] args) { // 創建對象 MyThread t1 = new MyThread() ; MyThread t2 = new MyThread() ; // 啟動線程: 需要使用start方法啟動線程, 如果我們在這里調用的是run方法,那么我們只是把該方法作為普通方法進行執行 // t1.run() ; // t1.run() ; t1.start() ; // 告訴jvm開啟一個線程調用run方法 // t1.start() ; // 一個線程只能被啟動一次 t2.start() ; } } public class MyThread extends Thread { @Override public void run() { for(int x = 0 ; x < 1000 ; x++) { System.out.println(x); } } } ``` **實現接口Runnable** 實現多線程的第二中方式步驟: 1: 定義一個類,讓該類去實現Runnable接口 2: 重寫run方法 3: 創建定義的類的對象 4: 創建Thread的對象吧第三步創建的對象作為參數傳遞進來 5: 啟動線程 ```java public static void main(String[] args) { // 創建定義的類的對象 MyThread mt = new MyThread() ; // 創建Thread的對象吧第三步創建的對象作為參數傳遞進來 Thread t1 = new Thread(mt , "張三") ; Thread t2 = new Thread(mt , "李四") ; // 啟動線程 t1.start() ; t2.start() ; } public class MyThread implements Runnable { @Override public void run() { for(int x = 0 ; x < 1000 ; x++) { System.out.println(Thread.currentThread().getName() + "---" + x); } } } ``` #### 一些常見問題 **thread、Runnable有什么區別** todo:java不支持多繼承 **Runnable、callable有什么不同** todo Runnable和Callable都代表那些要在不同的線程中執行的任務。Runnable從JDK1.0開始就有了,Callable是在JDK1.5增加的。它們的主要區別是Callable的 call() 方法可以返回值和拋出異常,而Runnable的run()方法沒有這些功能。Callable可以返回裝載有計算結果的Future對象。 Runnable接口中的run()方法的返回值是void,它做的事情只是純粹地去執行run()方法中的代碼而已;Callable接口中的call()方法是有返回值的,是一個泛型,和Future、FutureTask配合可以用來獲取異步執行的結果。 這其實是很有用的一個特性,因為多線程相比單線程更難、更復雜的一個重要原因就是因為多線程充滿著未知性,某條線程是否執行了?某條線程執行了多久?某條線程執行的時候我們期望的數據是否已經賦值完畢?無法得知,我們能做的只是等待這條多線程的任務執行完畢而已。而Callable+Future/FutureTask卻可以獲取多線程運行的結果,可以在等待時間太長沒獲取到需要的數據的情況下取消該線程的任務,真的是非常有用。 **run()、start()兩種方式的區別** run()方法只是調用了Thread實例的run()方法而已,它仍然運行在主線程上;而start()方法會開辟一個新的線程,在新的線程上調用run()方法,此時它運行在新的線程上 **為什么要重寫run方法** 可以在定義的類中,定義多個方法,而方法中的代碼并不是所有的都需要線程來進行執行; 如果我們想讓某一個段代碼被線程執行,那么我們只需要將那一段代碼放在run方法中。那么也就是說run方法中封裝的都是要被線程執行的代碼 ; run方法中的代碼的特點: 封裝的都是一些比較耗時的代碼 **線程能不能多次啟動** 一個線程只能被啟動一次 todo:一個線程兩次調用start()方法會出現什么情況? 36講-17 **匿名內部類的方式實現多線程程序** - new Thread(){代碼…}.start(); - new Thread(new Runnable(){代碼…}).start(); **wait()、sleep()區別** * **所在類**。sleep來自Thread類,和wait來自Object類 * **釋放對象鎖**。調用sleep()方法的過程中,線程不會釋放對象鎖。而 調用 wait 方法線程會釋放對象鎖 * **出讓資源**。sleep睡眠后不出讓系統資源,wait讓出系統資源其他線程可以占用CPU sleep(milliseconds)需要指定一個睡眠時間,時間一到會自動喚醒 #### 線程調度 應用程序在執行的時候都需要依賴于線程去搶占CPU的時間片 , 誰搶占到了CPU的時間片,那么CPU就會執行誰 線程的執行:假如我們的計算機只有一個 CPU,那么 CPU在某一個時刻只能執行一條指令,線程只有得到CPU時間片,也就是使用權,才可以執行指令。 **分時調度模型** 所有線程輪流使用CPU的使用權,平均分配每個線程占用 CPU 的時間片。 **搶占式調度模型** 優先讓優先級高的線程使用 CPU,如果線程的優先級相同,那么會隨機選擇一個,優先級高的線程獲取的 CPU 時間片相對多一些。Java使用的是搶占式調度模型。 #### 線程控制 **1、休眠線程** public static void sleep(long time) ; time表達的意思是休眠的時間 , 單位是毫秒 **2、加入線程** public final void join() 等待該線程執行完畢了以后,其他線程才能再次執行 注意事項: 在線程啟動之后,在調用方法 **3、禮讓線程** public static void yield(): 暫停當前正在執行的線程對象,并執行其他線程。 **線程禮讓的原理**: 暫定當前的線程,然后CPU去執行其他的線程, 這個暫定的時間是相當短暫的; 當我某一個線程暫定完畢以后,其他的線程還沒有搶占到cpu的執行權 ; 那么這個是時候當前的線程會和其他的線程再次搶占cpu的執行權; **4、守護線程** public final void setDaemon(boolean on) 將該線程標記為守護線程或用戶線程。當正在運行的線程都是守護線程時,Java 虛擬機退出。 該方法必須在啟動線程前調用。 jvm會線程程序中存在的線程類型,如果線程全部是守護線程,那么jvm就停止。 **5、中斷線程** public final void stop(): 停止線程的運行 public void interrupt(): 中斷線程(這個翻譯不太好),查看API可得,當線程調用wait(),sleep(long time)方法的時候處于阻塞狀態,可以通過這個方法清除阻塞 #### 線程狀態 ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0m83uwg9ej30i20cct9b.jpg) #### 同步、阻塞 同步、非同步,阻塞非阻塞 ### 線程池 有哪幾種?分別有什么特點? 如果提交任務時,線程池隊列已滿,這時會發生什么?線程調度算法是什么? 36講-21 #### ThreadPoolExecutor類介紹 ExecutorService是最初的線程池接口,ThreadPoolExecutor類是對線程池的具體實現,它通過構造方法來配置線程池的參數。 ```java public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } ``` **參數說明**: * **corePoolSize**:線程池中核心線程的數量,默認情況下,即使核心線程沒有任務在執行它也存在的,我們固定一定數量的核心線程且它一直存活這樣就避免了一般情況下CPU創建和銷毀線程帶來的開銷。我們如果將ThreadPoolExecutor的allowCoreThreadTimeOut屬性設置為true,那么閑置的核心線程就會有超時策略,這個時間由keepAliveTime來設定,即keepAliveTime時間內如果核心線程沒有回應則該線程就會被終止。allowCoreThreadTimeOut默認為false,核心線程沒有超時時間。 * **maximumPoolSize**,線程池中的最大線程數,當任務數量超過最大線程數時其它任務可能就會被阻塞。最大線程數=核心線程+非核心線程。非核心線程只有當核心線程不夠用且線程池有空余時才會被創建,執行完任務后非核心線程會被銷毀。 * **keepAliveTime**,非核心線程的超時時長,當執行時間超過這個時間時,非核心線程就會被回收。當allowCoreThreadTimeOut設置為true時,此屬性也作用在核心線程上。unit,枚舉時間單位,TimeUnit。 * **workQueue**,線程池中的任務隊列,我們提交給線程池的runnable會被存儲在這個對象上。 **遵循的規則** - 當線程池中的核心線程數量未達到最大線程數時,啟動一個核心線程去執行任務; - 如果線程池中的核心線程數量達到最大線程數時,那么任務會被插入到任務隊列中排隊等待執行; - 如果在上一步驟中任務隊列已滿但是線程池中線程數量未達到限定線程總數,那么啟動一個非核心線程來處理任務; - 如果上一步驟中線程數量達到了限定線程總量,那么線程池則拒絕執行該任務,且ThreadPoolExecutor會調用RejectedtionHandler的rejectedExecution方法來通知調用者。 #### 線程池的優點 再說線程池的有點前,我們先說說目前遇到的一些問題: 線程的創建和銷毀都需要時間,當有大量的線程創建和銷毀時,是非常耗cpu和內存的,這樣將直接影響系統的吞吐量,導致性能急劇下降,如果內存資源占用的比較多,還很可能造成OOM;此外,也很容易導致GC頻繁的執行,從而發生內存抖動現象,而發生了內存抖動,對于移動端來說,最大的影響就是造成界面卡頓 這里是優點: - 1、線程的創建和銷毀由線程池維護,一個線程在完成任務后并不會立即銷毀,而是由后續的任務復用這個線程,從而減少線程的創建和銷毀,節約系統的開銷 - 2、線程池旨在線程的復用,這就可以節約我們用以往的方式創建線程和銷毀所消耗的時間,減少線程頻繁調度的開銷,從而節約系統資源,提高系統吞吐量 - 3、在執行大量異步任務時提高了性能 - 4、Java內置的一套ExecutorService線程池相關的api,可以更方便的控制線程的最大并發數、線程的定時任務、單線程的順序執行等 #### 線程池的分類 目前線程池分為四類: **FixedThreadPool** 通過Executors的newFixedThreadPool()方法創建,它是個線程數量固定的線程池,該線程池的線程全部為核心線程,它們沒有超時機制且排隊任務隊列無限制,因為全都是核心線程,所以響應較快,且不用擔心線程會被回收。 **CachedThreadPool** 通過Executors的newCachedThreadPool()方法來創建,它是一個數量無限多的線程池,它所有的線程都是非核心線程,當有新任務來時如果沒有空閑的線程則直接創建新的線程,不會去排隊而直接執行,并且超時時間都是60s,所以此線程池適合執行大量耗時小的任務。由于設置了超時時間為60s,所以當線程空閑一定時間時就會被系統回收,所以理論上該線程池不會有占用系統資源的無用線程。 **ScheduledThreadPool** 通過Executors的newScheduledThreadPool()方法來創建,ScheduledThreadPool線程池像是上兩種的合體,它有數量固定的核心線程,且有數量無限多的非核心線程,但是它的非核心線程超時時間是0s,所以非核心線程一旦空閑立馬就會被回收。這類線程池適合用于執行定時任務和固定周期的重復任務。 **SingleThreadExecutor** 通過Executors的newSingleThreadExecutor()方法來創建,它內部只有一個核心線程,它確保所有任務進來都要排隊按順序執行。它的意義在于,統一所有的外界任務到同一線程中,讓調用者可以忽略線程同步問題。 #### 線程池一般用法 **一般方法介紹** shutDown(),關閉線程池,需要執行完已提交的任務; shutDownNow(),關閉線程池,并嘗試結束已提交的任務; allowCoreThreadTimeOut(boolen),允許核心線程閑置超時回收; execute(),提交任務無返回值; submit(),提交任務有返回值; **線程創建規則** ThreadPoolExecutor對象初始化時,不創建任何執行線程,當有新任務進來時,才會創建執行線程。構造ThreadPoolExecutor對象時,需要配置該對象的核心線程池大小和最大線程池大小: * 當 目前執行線程的總數小于核心線程大小時,所有新加入的任務,都在新線程中處理。 * 當 目前執行線程的總數大于或等于核心線程時,所有新加入的任務,都放入任務緩存隊列中。 * 當 目前執行線程的總數大于或等于核心線程,并且緩存隊列已滿,同時此時線程總數小于線程池的最大大小,那么創建新線程,加入線程池中,協助處理新的任務。 * 當 所有線程都在執行,線程池大小已經達到上限,并且緩存隊列已滿時,就rejectHandler拒絕新的任務。 ### 同步 參考文章: * [Java虛擬機是如何執行線程同步的][ref_Java虛擬機是如何執行線程同步的] * [1_Synchronized的實現原理][ref_1_Synchronized的實現原理] * [2_Java的對象模型][ref_2_Java的對象模型] * [3_Java的對象頭][ref_3_Java的對象頭] * [4_Moniter的實現原理][ref_4_Moniter的實現原理] * [5_Java虛擬機的鎖優化技術][ref_5_Java虛擬機的鎖優化技術] #### 對象鎖和類鎖的區別 JVM中有兩塊內存區域可以被所有線程共享: - 堆。上面存放著所有對象 - 方法區。上面存放著靜態變量 那么,如果有多個線程想要同時訪問同一個對象或者靜態變量,就需要被管控,否則可能出現不可預期的結果。 為了協調多個線程之間的共享數據訪問,虛擬機給每個對象和類都分配了一個鎖。這個鎖就像一個特權,在同一時刻,只有一個線程可以“擁有”這個類或者對象。如果一個線程想要獲得某個類或者對象的鎖,需要詢問虛擬機。當一個線程向虛擬機申請某個類或者對象的鎖之后,也許很快或者也許很慢虛擬機可以把鎖分配給這個線程,同時這個線程也許永遠也無法獲得鎖。當線程不再需要鎖的時候,他再把鎖還給虛擬機。這時虛擬機就可以再把鎖分配給其他申請鎖的線程。 **類鎖**其實通過對象鎖實現的。因為當虛擬機加載一個類的時候,會會為這個類實例化一個 `java.lang.Class` 對象,當你鎖住一個類的時候,其實鎖住的是其對應的Class 對象。 #### 同步方法和同步代碼塊區別 對于同步方法,JVM采用ACC_SYNCHRONIZED標記符來實現同步。 對于同步代碼塊。JVM采用monitorenter、monitorexit兩個指令來實現同步。 在java中,synchronized有兩種使用形式,同步方法和同步代碼塊。 ```java public class SynchronizedTest { public synchronized void doSth(){ System.out.println("Hello World"); } public void doSth1(){ synchronized (SynchronizedTest.class){ System.out.println("Hello World"); } } } ``` 反編譯以上代碼,結果如下(部分無用信息過濾掉了): ``` public synchronized void doSth(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello World 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return public void doSth1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: ldc #5 // class com/hollis/SynchronizedTest 2: dup 3: astore_1 4: monitorenter 5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 8: ldc #3 // String Hello World 10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: aload_1 14: monitorexit 15: goto 23 18: astore_2 19: aload_1 20: monitorexit 21: aload_2 22: athrow 23: return ``` 我們可以看到Java編譯器為我們生成的字節碼。在對于doSth和doSth1的處理上稍有不同。也就是說。JVM對于同步方法和同步代碼塊的處理方式不同。 對于同步方法,JVM采用ACC_SYNCHRONIZED標記符來實現同步。 對于同步代碼塊。JVM采用monitorenter、monitorexit兩個指令來實現同步。 **同步方法** 方法級的同步是隱式的。同步方法的常量池中會有一個`ACC_SYNCHRONIZED`標志。當某個線程要訪問某個方法的時候,會檢查是否有`ACC_SYNCHRONIZED`,如果有設置,則需要先獲得監視器鎖,然后開始執行方法,方法執行之后再釋放監視器鎖。這時如果其他線程來請求執行方法,會因為無法獲得監視器鎖而被阻斷住。值得注意的是,如果在方法執行過程中,發生了異常,并且方法內部并沒有處理該異常,那么在異常被拋到方法外面之前監視器鎖會被自動釋放。 **同步代碼塊** 可以把執行`monitorenter`指令理解為加鎖,執行`monitorexit`理解為釋放鎖。 每個對象維護著一個記錄著被鎖次數的計數器。未被鎖定的對象的該計數器為0,當一個線程獲得鎖(執行`monitorenter`)后,該計數器自增變為 1 ,當同一個線程再次獲得該對象的鎖的時候,計數器再次自增。當同一個線程釋放鎖(執行`monitorexit`指令)的時候,計數器再自減。當計數器為0的時候。鎖將被釋放,其他線程便可以獲得鎖。 #### Moniter原理 關于`Synchronize`的實現原理,無論是同步方法還是同步代碼塊,無論是`ACC_SYNCHRONIZED`還是`monitorenter`、`monitorexit`都是基于`Monitor`實現的。 在多線程訪問共享資源的時候,經常會帶來可見性和原子性的安全問題。為了解決這類線程安全的問題,Java提供了同步機制、互斥鎖機制,這個機制保證了在同一時刻只有一個線程能訪問共享資源。這個機制的保障來源于監視鎖Monitor,每個對象都擁有自己的監視鎖Monitor。 在Java虛擬機(HotSpot)中,Monitor是基于C++實現的,由ObjectMonitor實現的,其主要數據結構如下: ```c++ ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; } ``` 源碼地址:objectMonitor.hpp ObjectMonitor中有幾個關鍵屬性: * _owner:指向持有ObjectMonitor對象的線程 * _WaitSet:存放處于wait狀態的線程隊列 * _EntryList:存放處于等待鎖block狀態的線程隊列 * _recursions:鎖的重入次數 * _count:用來記錄該線程獲取鎖的次數 當多個線程同時訪問一段同步代碼時,首先會進入`_EntryList`隊列中,當某個線程獲取到對象的monitor后進入`_Owner`區域并把monitor中的`_owner`變量設置為當前線程,同時monitor中的計數器`_count`加1。即獲得對象鎖。 若持有monitor的線程調用`wait()`方法,將釋放當前持有的monitor,`_owner`變量恢復為`null`,`_count`自減1,同時該線程進入`_WaitSet`集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)并復位變量的值,以便其他線程進入獲取monitor(鎖)。如下圖所示 ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0m7u92c33j30et08vwf1.jpg) 這里附帶一張 線程狀態圖,便于理解: ![](http://ww1.sinaimg.cn/large/005Ogmrtly1g0m83uwg9ej30i20cct9b.jpg) #### 虛擬機中常見的鎖優化 常見的鎖優化技術:自旋鎖、鎖消除、鎖粗化。但這些優化僅在Java虛擬機server模式會生效,這里對這些優化原理進行簡述,各位在開發的過程中,稍加留意即可。 下面會簡要的介紹這幾種優化技術。 **自旋鎖** synchronize的實現方式中使用Monitor進行加鎖,這是一種互斥鎖,這種互斥鎖在互斥同步上對性能的影響很大,Java的線程是映射到操作系統原生線程之上的,如果要阻塞或喚醒一個線程就需要操作系統的幫忙,這就要從用戶態轉換到內核態,因此狀態轉換需要花費很多的處理器時間。 Java虛擬機的開發工程師們在分析過大量數據后發現:共享數據的鎖定狀態一般只會持續很短的一段時間,為了這段時間去掛起和恢復線程其實并不值得。 如果物理機上有多個處理器,可以讓多個線程同時執行的話。我們就可以讓后面來的線程“稍微等一下”,但是并不放棄處理器的執行時間,看看持有鎖的線程會不會很快釋放鎖。這個“稍微等一下”的過程就是自旋。 由于自旋鎖只是將當前線程不停地執行循環體,放棄處理器的執行時間,不進行線程狀態的改變,所以響應速度更快。但當線程數不停增加時,性能下降明顯,因為每個線程都需要執行,占用CPU時間。**如果線程競爭不激烈,并且保持鎖的時間段。適合使用自旋鎖**。 **鎖消除** 如果同步塊所使用的鎖對象通過這種分析被證實只能夠被一個線程訪問,那么JIT編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步。 如下代碼: ```java public void f() { Object hollis = new Object(); synchronized(hollis) { System.out.println(hollis); } } ``` 這里,可能有讀者會質疑了,代碼是程序員自己寫的,程序員難道沒有能力判斷要不要加鎖嗎?就像以上代碼,完全沒必要加鎖,有經驗的開發者一眼就能看的出來的。代碼中對`hollis`這個對象進行加鎖,但是`hollis`對象的生命周期只在`f()`方法中,并不會被其他線程所訪問到,所以在JIT編譯階段就會被優化掉。優化成: ```java public void f() { Object hollis = new Object(); System.out.println(hollis); } ``` 其實道理是這樣,但是還是有可能有疏忽,比如我們經常在代碼中使用StringBuffer作為局部變量,而StringBuffer中的append是線程安全的,有synchronized修飾的,這種情況開發者可能會忽略。這時候,JIT就可以幫忙優化,進行鎖消除。 **鎖粗化** 很多人都知道,在代碼中,需要加鎖的時候,我們提倡盡量減小鎖的粒度,這樣可以避免不必要的阻塞。 這也是很多人原因是用同步代碼塊來代替同步方法的原因,因為往往他的粒度會更小一些,這其實是很有道理的。 但是。也要分情況。 如果在一段代碼中連續的對同一個對象反復加鎖解鎖,其實是相對耗費資源的,這種情況可以適當放寬加鎖的范圍,減少性能消耗。 當JIT發現一系列連續的操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作出現在循環體中的時候,會將加鎖同步的范圍擴散(粗化)到整個操作序列的外部。 如下列: ```java for(int i=0;i<100000;i++){ synchronized(this){ do(); } ``` 會被粗化: ```java synchronized(this){ for(int i=0;i<100000;i++){ do(); } ``` 這其實和我們要求的減小鎖粒度并不沖突。減小鎖粒度強調的是不要在銀行柜臺前做準備工作以及和辦理業務無關的事情。而鎖粗化建議的是,同一個人,要辦理多個業務的時候,可以在同一個窗口一次性辦完,而不是多次取號多次辦理。 #### synchronized和ReentrantLock有什么區別 36講-15 #### synchronized底層如何實現?什么是鎖的升級、降級? 36講-16 #### synchonized(this)和synchonized(object)區別?Synchronize作用于方法和靜態方法區別? ### 內存模型 #### java內存模型中的happer-before是什么 #### AtomicInteger內存模型 ### volatile ### ThreadLocal ### 生產者消費者模型 ### 死鎖 什么情況下Java程序會產生死鎖?如何定位、修復 36講-18 也 可以參考這里[死鎖的四個必要條件和解決辦法](https://blog.csdn.net/guaiguaihenguai/article/details/80303835) 線程死鎖的 4 個條件 當兩個線程彼此占有對方需要的資源,同時彼此又無法釋放自己占有的資源的時候就發生了死鎖。發生死鎖需要滿足下面四個條件, 1. **互斥**:某種資源一次只允許一個進程訪問,即該資源一旦分配給某個進程,其他進程就不能再訪問,直到該進程訪問結束。(一個筷子只能被一個人拿) 2. **占有且等待**:一個進程本身占有資源(一種或多種),同時還有資源未得到滿足,正在等待其他進程釋放該資源。(每個人拿了一個筷子還要等其他人放棄筷子) 3. **不可搶占**:別人已經占有了某項資源,你不能因為自己也需要該資源,就去把別人的資源搶過來。(別人手里的筷子你不能去搶) 4. **循環等待**:存在一個進程鏈,使得每個進程都占有下一個進程所需的至少一種資源。(每個人都在等相鄰的下一個人放棄自己的筷子) #### 請手寫一個死鎖 ### 其他問題 #### 為什么wait()方法和notify()/notifyAll()方法要在同步塊中被調用 這是JDK強制的,wait()方法和notify()/notifyAll()方法在調用前都必須先獲得對象的鎖 #### wait()方法和notify()/notifyAll()方法在放棄對象監視器時有什么區別 wait()方法和notify()/notifyAll()方法在放棄對象監視器的時候的區別在于:wait()方法立即釋放對象監視器,notify()/notifyAll()方法則會等待線程剩余代碼執行完畢才會放棄對象監視器。 #### 你是如何調用 wait()方法的?使用 if 塊還是循環 todo: wait() 方法應該在循環調用,因為當線程獲取到 CPU 開始執行的時候,其他條件可能還沒有滿足,所以在處理前,循環檢測條件是否滿足會更好。 ```java public void doSothing(){ synchronized (obj) { while (condition does not hold) obj.wait(); // (Releases lock, and reacquires on wakeup) ... // Perform action appropriate to condition } } ``` 參見 Effective Java 第 69 條,獲取更多關于為什么應該在循環中來調用 wait 方法的內容。 #### Java中如何停止一個線程? Java提供了很豐富的API但沒有為停止線程提供API。JDK 1.0本來有一些像stop(), suspend() 和 resume()的控制方法但是由于潛在的死鎖威脅因此在后續的JDK版本中他們被棄用了,之后Java API的設計者就沒有提供一個兼容且線程安全的方法來停止一個線程。當run() 或者 call() 方法執行完的時候線程會自動結束,如果要手動結束一個線程,你可以用volatile 布爾變量來退出run()方法的循環或者是取消任務來中斷線程 #### 一個線程運行時發生異常會怎樣 這是我在一次面試中遇到的一個[很刁鉆的Java面試題](https://link.juejin.im/?target=http%3A%2F%2Fjava67.blogspot.sg%2F2012%2F09%2Ftop-10-tricky-java-interview-questions-answers.html), 簡單的說,如果異常沒有被捕獲該線程將會停止執行。Thread.UncaughtExceptionHandler是用于處理未捕獲異常造成線程突然中斷情況的一個內嵌接口。當一個未捕獲異常將造成線程中斷的時候JVM會使用Thread.getUncaughtExceptionHandler()來查詢線程的UncaughtExceptionHandler并將線程和異常作為參數傳遞給handler的uncaughtException()方法進行處理。 #### 如何在兩個線程間共享數據? 你可以通過共享對象來實現這個目的,或者是使用像阻塞隊列這樣并發的數據結構。這篇教程[《Java線程間通信》](https://link.juejin.im/?target=http%3A%2F%2Fjavarevisited.blogspot.sg%2F2013%2F12%2Finter-thread-communication-in-java-wait-notify-example.html)(涉及到在兩個線程間共享對象)用wait和notify方法實現了生產者消費者模型。 #### Java中notify 和 notifyAll有什么區別? #### 如何寫代碼來解決生產者消費者問題? #### 怎么檢測一個線程是否擁有鎖? 我一直不知道我們竟然可以檢測一個線程是否擁有鎖,直到我參加了一次電話面試。在java.lang.Thread中有一個方法叫holdsLock(),它返回true如果當且僅當當前線程擁有某個具體對象的鎖。你可以查看[這篇文章](https://link.juejin.im/?target=http%3A%2F%2Fjavarevisited.blogspot.com%2F2010%2F10%2Fhow-to-check-if-thread-has-lock-on.html)了解更多。 #### Java中synchronized 和 ReentrantLock 有什么不同? Java在過去很長一段時間只能通過synchronized關鍵字來實現互斥,它有一些缺點。比如你不能擴展鎖之外的方法或者塊邊界,嘗試獲取鎖時不能中途取消等。Java 5 通過Lock接口提供了更復雜的控制來解決這些問題。 ReentrantLock 類實現了 Lock,它擁有與 synchronized 相同的并發性和內存語義且它還具有可擴展性。 實現原理是synchronized是通過Monitor,ReentrantLock底層調用的是Unsafe的park方法加鎖。 #### Java中Lock接口比synchronized塊的優勢是什么? #### 有三個線程T1,T2,T3,怎么確保它們按順序執行? 目的是檢測你對”join”方法是否熟悉。這個多線程問題比較簡單,可以用join方法實現。 #### 如果你提交任務時,線程池隊列已滿。會時發會生什么? 這個問題問得很狡猾,許多程序員會認為該任務會阻塞直到線程池隊列有空位。事實上如果一個任務不能被調度執行那么ThreadPoolExecutor’s submit()方法將會拋出一個RejectedExecutionException異常 #### 如果同步塊內的線程拋出異常會發生什么? 這個問題坑了很多Java程序員,若你能想到鎖是否釋放這條線索來回答還有點希望答對。無論你的同步塊是正常還是異常退出的,里面的線程都會釋放鎖,所以對比鎖接口我更喜歡同步塊,因為它不用我花費精力去釋放鎖,該功能可以在[finally block](https://link.juejin.im/?target=http%3A%2F%2Fjavarevisited.blogspot.com%2F2012%2F11%2Fdifference-between-final-finally-and-finalize-java.html)里釋放鎖實現 #### 單例模式的雙檢鎖是什么? #### 如何在Java中創建線程安全的Singleton? #### java并發包提供了那些并發工具類? 36講-19 #### 并發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區別? 36講-20 ## 雜談 ### 動態代理 靜態代理、動態代理 動態代理和反射的關系 動態代理的幾種實現方式 AOP #### 有哪些方法可以在運行時銅帶生成一個Java類? 36講-24 ### 如何寫出安全的Java代碼? 36講-32 ### 注解 元注解、自定義注解、Java 中常用注解使用、注解與反射的結合 ## 參考 * [JavaGuide](https://github.com/Snailclimb/JavaGuide) [ref_Java虛擬機是如何執行線程同步的]: https://mp.weixin.qq.com/s/DpXS_v-qmUa-bkcVHu9ASQ [ref_1_Synchronized的實現原理]: https://mp.weixin.qq.com/s/637zy26W_fdeopX5dG8OKQ [ref_2_Java的對象模型]: https://mp.weixin.qq.com/s/mWWey3zngiqi-E40PR9U3A [ref_3_Java的對象頭]: https://mp.weixin.qq.com/s/3bfUtmhtRvXMGB04aPAIFA [ref_4_Moniter的實現原理]: https://mp.weixin.qq.com/s/uz4RnLG2Na9SvSBdkPflUg [ref_5_Java虛擬機的鎖優化技術]: https://mp.weixin.qq.com/s/VDdsKp0uzmh_7vpD9lpLaA
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看