人臉檢測和人臉識別原理,微調(Fine-tune)原理

一、MTCNN的原理

  搭建人臉識別系統的第一步是人臉檢測,也就是在圖片中找到人臉的位置。在這個過程中,系統的輸入是一張可能含有人臉的圖片,輸出是人臉位置的矩形框,如下圖所示。一般來說,人臉檢測應該可以正確檢測出圖片中存在的所有人臉,不能用遺漏,也不能有錯檢。  

   

  獲得包含人臉的矩形框后,第二步要做的就是人臉對齊(Face Alignment)。原始圖片中人臉的姿態、位置可能較大的區別,為了之後統一處理,要把人臉“擺正”。為此,需要檢測人臉中的關鍵點(Landmark),如眼睛的位置、鼻子的位置、嘴巴的位置、臉的輪廓點等。根據這些關鍵點可以使用仿射變換將人臉統一校準,以盡量消除姿勢不同帶來的誤差,人臉對齊的過程如下圖所示。

   

  這裏介紹一種基於深度卷積神經網絡的人臉檢測和人臉對齊方法—-MTCNN,它是基於卷積神經網絡的一種高精度的實時人臉檢測和對齊技術。MT是英文單詞Multi-task的縮寫,意思就是這種方法可以同時完成人臉檢測的人臉對齊兩項任務。相比於傳統方法,MTCNN的性能更好,可以更精確的定位人臉,此外,MTCNN也可以做到實時的檢測。

  MTCNN由三個神經網絡組成,分別是P-Net、R-Net、O-Net。在使用這些網絡之前,首先要將原始圖片縮放到不同尺度,形成一個“圖像金字塔”,如下圖所示。

   

  接着會對每個尺度的圖片通過神經網絡計算一遍。這樣做的原因在於:原始圖片中的人臉存在不同的尺度,如有的人臉比較大,有的人臉比較小。對於比較小的人臉,可以在放大后的圖片上檢測;對於比較大的人臉,可以在縮小后的圖片上進行檢測。這樣,就可以在統一的尺度下檢測人臉了。

  現在再來討論第一個網絡P-Net的結構,如下圖所示

   

  P-Net的輸入是一個寬和高皆為12像素,同時是3通道的RGB圖像,該網絡要判斷這個12×12的圖像中是否含有人臉,並且給出人臉框和關鍵點的位置。因此對應的輸出應該由3部分組成:

  (1)第一個部分要判斷該圖像是否是人臉(上圖中的face classification),輸出向量的形狀為1x1x2,也就是兩個值,分別為該圖像是人臉的概率,以及該圖像不是人臉的概率。這兩個值加起來應該嚴格等1。之所以使用兩個值來表示,是為了方便定義交叉熵損失。
  (2)第二個部分給出框的精確位置(上圖中的bounding box regression),一般稱之為框回歸。P-Net輸入的12×12的圖像塊可能並不是完美的人臉框的位置,如有的時候人臉並不正好為方形,有的時候12×12的圖像塊可能偏左或偏右,因此需要輸出當前框位置相對於完美的人臉框位置的偏移。這個偏移由四個變量組成。一般地,對於圖像中的框,可以用四個數來表示它的位置:框左上角的橫坐標、框左上角的縱坐標、框的寬度、框的高度。因此,框回歸輸出的值是:框左上角的橫坐標的相對偏移、框左上角的縱坐標的相對偏移、框的寬度的誤差、框的 高度的誤差。輸出向量的形狀就是上圖中的1x1x4。
  (3)第三個部分給出人臉的5個關鍵點的位置。5個關鍵點分別為:左眼的位置、右眼的位置、鼻子的位置、左嘴角的位置、右嘴角的位置。每個關鍵點又需要橫坐標和縱坐標來表示,因此輸出一共是10維(即1x1x10)

  上面的介紹大致就是P-Net的結構了。在實際計算中,通過P-Net中第一層卷積的移動,會對圖像中每一個12×12的區域做一次人臉檢測,得到的結構如下圖所示:

   

  圖中框的大小各有不同,除了框回歸的影響外,主要是因為將圖片金字塔的各個尺度都使用P-Net計算了一遍,因此形成了大小不同的人臉框。P-Net的結果還是比較粗糙的,所以接下來又使用R-Net進一步調優。R-Net的網絡結構如下圖所示。

   

  這個結構與之前的P-Net非常類似,P-Net的輸入是12x12x3的圖像,R-Net是24x24x3的圖像,也就是說,R-Net判斷24x24x3的圖像中是否含有人臉,以及預測關鍵點的位置。R-Net的輸出和P-Net完全一樣,同樣有人臉判別、框回歸、關鍵點位置預測三部分組成。

  在實際應用中,對每個P-Net輸出可能為人臉的區域都放縮到24×24的大小,在輸入到R-Net中,進行進一步的判定。得到的結果如下圖所示:

   

  顯然R-Net消除了P-Net中很多誤判的情況。

  進一步把所有得到的區域縮放成48×48的大小,輸入到最後的O-Net中,O-Net的結構同樣與P-Net類似,不同點在於它的輸入是48x48x3的圖像,網絡的通道數和層數也更多了。O-Net的網絡的結構如下圖所示:

   

  檢測結果如下圖所示:

   

  從P-Net到R-Net,最後再到O-Net,網絡輸入的圖片越來越大,卷積層的通道數越來越多,內部的層數也越來越多,因此它們識別人臉的準確率應該是越來越高的。同時,P-Net的運行速度是最快的,R-Net的速度其次,O-Net的運行速度最慢。之所以要使用三個網絡,是因為如果一開始直接對圖中的每個區域使用O-Net,速度會非常慢慢。實際上P-Net先做了一遍過濾,將過濾后的結果再交給R-Net進行過濾,最後將過濾后的結果交給效果最好但速度較慢的O-Net進行判別。這樣在每一步都提前減少了需要判別的數量,有效降低了處理時間。

  最後介紹MTCNN的損失定義和訓練過程。MTCNN中每個網絡都有三部分輸出,因此損失也由三部分組成。針對人臉判別部分,直接使用交叉熵損失,針對框回歸和關鍵點判定,直接使用L2損失。最後這三部分損失各自乘以自身的權重再加起來,就形成最後的總損失了。在訓練P-Net和R-Net時,更關心框位置的準確性,而較少關注關鍵點判定的損失,因此關鍵點判定損失的權重很小。對於O-Net,關鍵點判定損失的權重較大。

二、使用深度卷積網絡提取特徵

  經過人臉檢測和人臉對齊兩個步驟,就獲得了包含人臉的區域圖像,接下來就要進行人臉識別了。這一步一般是使用深度卷積網絡,將輸入的人臉圖像轉換為一個向量的表示,也就是所謂的“特徵”。

  如何針對人臉來提取特徵?可以先來回憶VGG16的網絡結構(見),輸入神經網絡的是圖像,經過一系列卷積計算后,全連接分類得到類別概率。

  在通常的圖像應用中,可以去掉全連接層,使用卷積層的最後一層當作圖像的“特徵”。但如果對人臉識別問題同樣採用這種方法,即使用卷積層最後一層做為人臉的“向量表示”,效果其實是不好的。這其中的原因和改進方法是什麼?在後面會談到,這裏先談談希望這種人臉的“向量表示”應該具有哪些性質。

  在理想的狀況下,希望“向量表示”之間的距離可以直接反映人臉的相似度

  對於同一個人的兩張人臉圖像,對應的向量之間的歐幾里得距離應該比較小。對於不同人的兩張人臉圖像,對應的向量之間的歐幾里得距離應該比較大。

  例如,設人臉圖像為$x_{1}$,$x_{2}$,對應的特徵為$f(x_{1})$,$f(x_{2})$,當$x_{1}$,$x_{2}$對應是同一個人的人臉時,$f(x_{1})$,$f(x_{2})$的距離$\left \| f(x_{1}),f(x_{2}) \right \|$2應該很小,而當$x_{1}$,$x_{2}$是不同人的人臉時,$f(x_{1})$,$f(x_{2})$的距離$\left \| f(x_{1}),f(x_{2}) \right \|$2應該很大。

  在原始的CNN模型中,使用的是Softmax損失。Softmax是類別間的損失,對於人臉來說,每一類就是一個人。儘管使用Softmax損失可以區別出每個人,但其本質上沒有對每一類的向量表示之間的距離做出要求。

  舉個例子,使用CNN對MNIST進行分類,設計一個特殊的卷積網絡,讓其最後一層的向量變為2維,此時可以畫出每一類對應的2維向量(圖中一種顏色對應一種類別),如下圖所示:

   

  上圖是我們直接使用softmax訓練得到的結果,它就不符合我們希望特徵具有的特點:

  (1)我們希望同一類對應的向量表示盡可能接近。但這裏同一類(如紫色),可能具有很大的類間距離;
  (2)我們希望不同類對應的向量應該盡可能遠。但在圖中靠中心的位置,各個類別的距離都很近;

  對於人臉圖像同樣會出現類似的情況,對此,有很改進方法。這裏介紹其中兩種:一種是三元組損失函數(Triplet Loss),一種是中心損失函數。 

三、三元組損失的定義

  三元組損失函數的原理:既然目標是特徵之間的距離應該具備某些性質,那麼我們就圍繞這個距離來設計損失。具體的,我們每次都在訓練數據中抽出三張人臉圖像,第一張圖像記為$x_{i}^{a}$,第二張圖像記為$x_{i}^{p}$,第三張圖像記為$x_{i}^{n}$。在這樣的一個“三元組”中,$x_{i}^{a}$和$x_{i}^{p}$對應的是同一個人的圖像,而$x_{i}^{n}$是另外一個不同的人的人臉圖像。因此,距離$\left \| f(x_{i}^{a})-f(x_{i}^{p}) \right \|_{2}$應該較小,而距離$\left \| f(x_{i}^{a})-f(x_{i}^{n}) \right \|_{2}$應該較大。嚴格來說,三元組損失要求下面的式子成立:

   $\left \| f(x_{i}^{a})- f(x_{i}^{p})\right \|_{2}^{2}+\alpha <\left \| f(x_{i}^{a})- f(x_{i}^{p})\right \|_{2}^{2}$

  然後計算相同人臉之間與不同人臉之間距離的平方

   $\left [ \left \| f(x_{i}^{a})-f(x_{i}^{p}) \right \|_{2}^{2}+\alpha -\left \| f(x_{i}^{a})-f(x_{i}^{n}) \right \|_{2}^{2} \right ]_{+}$

  上式表達相同人臉間的距離平方至少要比不同人臉間的距離平方小α(取平方主要是為了方便求導),據此,上式實際上就是相當於一個損失函數。這樣的話,當三元組的距離滿足 $\left \| f(x_{i}^{a})- f(x_{i}^{p})\right \|_{2}^{2}+\alpha <\left \| f(x_{i}^{a})- f(x_{i}^{p})\right \|_{2}^{2}$時,不產生任何損失,此時$L_{i}=0$。當距離不滿足上述等式時,就會有值為$\left \| f(x_{i}^{a})-f(x_{i}^{p}) \right \|_{2}^{2}+\alpha -\left \| f(x_{i}^{a})-f(x_{i}^{n}) \right \|_{2}^{2}$的損失。此外,在訓練時會固定$\left \| f(x) \right \|_{2}=1$,以保證特徵不會無限地“遠離”。

  三元組損失直接對距離進行優化,因此可以解決人臉的特徵表示問題。但是在訓練過程中,三元組的選擇非常地有技巧性。如果每次都是隨機選擇三元組,雖然模型可以正確的收斂,但是並不能達到最好的性能。如果加入”難例挖掘”,即每次都選擇最難分辨率的三元組進行訓練,模型又往往不能正確的收斂。對此,又提出每次都選擇那些“半難”(Semi-hard)的數據進行訓練,讓模型在可以收斂的同時也保持良好的性能。此外,使用三元組損失訓練人臉模型通常還需要非常大的人臉數據集,才能取得較好的效果。

四、中心損失的定義

  與三元組損失不同,中心損失(Center Loss)不直接對距離進行優化,它保留了原有的分類模型,但又為每個類(在人臉模型中,一個類就對應一個人)指定了一個類別中心。同一類的圖像對應的特徵都應該盡量靠近自己的類別中心,不同類的類別中心盡量遠離。與三元組損失函數相比,使用中心損失訓練人臉模型不需要使用特別的採樣方法,而且利用較少的圖像就可以達到與單元組損失相似的效果。下面我們一起來學習中心損失的定義:

   還是設輸入的人臉圖像為$x_{i}$,該人臉對應的類別為$y_{i}$,對每個類別都規定一個類別中心,記作$c_{yi}$。希望每個人臉圖像對應的特徵$f(x_{i})$都盡可能接近其中心$c_{yi}$。因此定義中心損失為:

    $L_{i}=\frac{1}{2}\left \| f(x_{i})-c_{yi}\right \|_{2}^{2}$

  多張圖像的中心損失就是將它們的值加在一起:

   $L_{center}=\sum\limits_{i}L_i$

  這是一個非常簡單的定義。不過還有一個問題沒有解決,那就是如何確定每個類別的中心$c_{yi}$呢?從理論上來說,類別$y_{i}$的最佳中心應該是它對應的所有圖片的特徵的平均值。但如果採取這樣的定義,那麼在每一次梯度下降時,都要對所有圖片計算一次$c_{yi}$,計算複雜度就太高了。針對這種情況,不妨近似一處理下,在初始階段,先隨機確定$c_{yi}$,接着在每個batch內,使用$L_i=\|f(x_i)-c_{yi}\|_2^2$對當前batch內的$c_{yi}$ 也計算梯度,並使用該梯度更新$c_{yi}$ 。此外,不能只使用中心損失來訓練分類模型,還需要加入Softmax損失,也就是說,最終的損失由兩部分構成,即$L = L_{softmax}+\lambda L_{center}$,其中$\lambda $是一個超參數。

  最後來總結使用中心損失來訓練人臉模型的過程。首先隨機初始化各个中心$c_{yi}$,接着不斷地取出batch進行訓練,在每個batch中,使用總的損失$L$,除了使用神經網絡模型的參數對模型進行更新外,也對$c_{yi}$進行計算梯度,並更新中心的位置。

  中心損失可以讓訓練處的特徵具有“內聚性”。還是以MNIST的例子來說,在未加入中心損失時,訓練的結果不具有內聚性。再加入中心損失后,得到的特徵如下圖所示。 

   

從圖中可以看出,當中心損失的權重λ越大時,生成的特徵就會具有越明顯的“內聚性” 。

五、使用特徵設計應用

當提取出特徵后,剩下的問題就非常簡單了。因為這種特徵已經具有了相同人對應的向量的距離小,不同人對應的向量距離大的特點,接下來,一般的應用有以下幾類:

  • 人臉驗證(Face Identification)。就是檢測A、B是否屬於同一個人。只需要計算向量之間的距離,設定合適的報警閾值(threshold)即可。
  • 人臉識別(Face Recognition)。這個應用是最多的,給定一張圖片,檢測數據庫中與之最相似的人臉。顯然可以被轉換為一個求距離的最近鄰問題。
  • 人臉聚類(Face Clustering)。在數據庫中對人臉進行聚類,直接用K-means即可。

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

Android DecorView 與 Activity 綁定原理分析

一年多以前,曾經以為自己對 View 的添加显示邏輯已經有所了解了,事後發現也只是懂了些皮毛而已。經過一年多的實戰,Android 和 Java 基礎都有了提升,是時候該去看看 DecorView 的添加显示。

概論

Android 中 Activity 是作為應用程序的載體存在,代表着一個完整的用戶界面,提供了一個窗口來繪製各種視圖,當 Activity 啟動時,我們會通過 setContentView 方法來設置一個內容視圖,這個內容視圖就是用戶看到的界面。那麼 View 和 activity 是如何關聯在一起的呢 ?

 上圖是 View 和 Activity 之間的關係。先解釋圖中一些類的作用以及相關關係:

  • Activity : 對於每一個 activity 都會有擁有一個 PhoneWindow。

  • PhoneWindow :該類繼承於 Window 類,是 Window 類的具體實現,即我們可以通過該類具體去繪製窗口。並且,該類內部包含了一個 DecorView 對象,該 DectorView 對象是所有應用窗口的根 View。
  • DecorView 是一個應用窗口的根容器,它本質上是一個 FrameLayout。DecorView 有唯一一個子 View,它是一個垂直 LinearLayout,包含兩個子元素,一個是 TitleView( ActionBar 的容器),另一個是 ContentView(窗口內容的容器)。

  • ContentView :是一個 FrameLayout(android.R.id.content),我們平常用的 setContentView 就是設置它的子 View 。

  • WindowManager : 是一個接口,裏面常用的方法有:添加View,更新View和刪除View。主要是用來管理 Window 的。WindowManager 具體的實現類是WindowManagerImpl。最終,WindowManagerImpl 會將業務交給 WindowManagerGlobal 來處理。
  • WindowManagerService (WMS) : 負責管理各 app 窗口的創建,更新,刪除, 显示順序。運行在 system_server 進程。

ViewRootImpl :擁有 DecorView 的實例,通過該實例來控制 DecorView 繪製。ViewRootImpl 的一個內部類 W,實現了 IWindow 接口,IWindow 接口是供 WMS 使用的,WSM 通過調用 IWindow 一些方法,通過 Binder 通信的方式,最後執行到了 W 中對應的方法中。同樣的,ViewRootImpl 通過 IWindowSession 來調用 WMS 的 Session 一些方法。Session 類繼承自 IWindowSession.Stub,每一個應用進程都有一個唯一的 Session 對象與 WMS 通信。

DecorView 的創建 

先從 Mainactivity 中的代碼看起,首先是調用了 setContentView;

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

該方法是父類 AppCompatActivity 的方法,最終會調用 AppCompatDelegateImpl 的 setContentView 方法:

// AppCompatDelegateImpl  
public void setContentView(int resId) { this.ensureSubDecor(); ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290); contentParent.removeAllViews(); LayoutInflater.from(this.mContext).inflate(resId, contentParent); this.mOriginalWindowCallback.onContentChanged(); }

ensureSubDecor 從字面理解就是創建 subDecorView,這個是根據主題來創建的,下文也會講到。創建完以後,從中獲取 contentParent,再將從 activity 傳入的 id xml 布局添加到裏面。不過大家注意的是,在添加之前先調用 removeAllViews() 方法,確保沒有其他子 View 的干擾。

    private void ensureSubDecor() {
        if (!this.mSubDecorInstalled) {
            this.mSubDecor = this.createSubDecor(); 
            ......
        }
        ......
    }        

 最終會調用 createSubDecor() ,來看看裏面的具體代碼邏輯:

 private ViewGroup createSubDecor() {
        // 1、獲取主題參數,進行一些設置,包括標題,actionbar 等 
        TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);
        if (!a.hasValue(styleable.AppCompatTheme_windowActionBar)) {
            a.recycle();
            throw new IllegalStateException("You need to use a Theme.AppCompat theme (or descendant) with this activity.");
        } else {
            if (a.getBoolean(styleable.AppCompatTheme_windowNoTitle, false)) {
                this.requestWindowFeature(1);
            } else if (a.getBoolean(styleable.AppCompatTheme_windowActionBar, false)) {
                this.requestWindowFeature(108);
            }

            if (a.getBoolean(styleable.AppCompatTheme_windowActionBarOverlay, false)) {
                this.requestWindowFeature(109);
            }

            if (a.getBoolean(styleable.AppCompatTheme_windowActionModeOverlay, false)) {
                this.requestWindowFeature(10);
            }

            this.mIsFloating = a.getBoolean(styleable.AppCompatTheme_android_windowIsFloating, false);
            a.recycle();
            // 2、確保優先初始化 DecorView
            this.mWindow.getDecorView();
            LayoutInflater inflater = LayoutInflater.from(this.mContext);
            ViewGroup subDecor = null;
            // 3、根據不同的設置來對 subDecor 進行初始化
            if (!this.mWindowNoTitle) {
                if (this.mIsFloating) {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_dialog_title_material, (ViewGroup)null);
                    this.mHasActionBar = this.mOverlayActionBar = false;
                } else if (this.mHasActionBar) {
                    TypedValue outValue = new TypedValue();
                    this.mContext.getTheme().resolveAttribute(attr.actionBarTheme, outValue, true);
                    Object themedContext;
                    if (outValue.resourceId != 0) {
                        themedContext = new ContextThemeWrapper(this.mContext, outValue.resourceId);
                    } else {
                        themedContext = this.mContext;
                    }

                    subDecor = (ViewGroup)LayoutInflater.from((Context)themedContext).inflate(layout.abc_screen_toolbar, (ViewGroup)null);
                    this.mDecorContentParent = (DecorContentParent)subDecor.findViewById(id.decor_content_parent);
                    this.mDecorContentParent.setWindowCallback(this.getWindowCallback());
                    if (this.mOverlayActionBar) {
                        this.mDecorContentParent.initFeature(109);
                    }

                    if (this.mFeatureProgress) {
                        this.mDecorContentParent.initFeature(2);
                    }

                    if (this.mFeatureIndeterminateProgress) {
                        this.mDecorContentParent.initFeature(5);
                    }
                }
            } else {
                if (this.mOverlayActionMode) {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple_overlay_action_mode, (ViewGroup)null);
                } else {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple, (ViewGroup)null);
                }

                if (VERSION.SDK_INT >= 21) {
                    ViewCompat.setOnApplyWindowInsetsListener(subDecor, new OnApplyWindowInsetsListener() {
                        public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
                            int top = insets.getSystemWindowInsetTop();
                            int newTop = AppCompatDelegateImpl.this.updateStatusGuard(top);
                            if (top != newTop) {
                                insets = insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), newTop, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
                            }

                            return ViewCompat.onApplyWindowInsets(v, insets);
                        }
                    });
                } else {
                    ((FitWindowsViewGroup)subDecor).setOnFitSystemWindowsListener(new OnFitSystemWindowsListener() {
                        public void onFitSystemWindows(Rect insets) {
                            insets.top = AppCompatDelegateImpl.this.updateStatusGuard(insets.top);
                        }
                    });
                }
            }

            if (subDecor == null) {
                throw new IllegalArgumentException("AppCompat does not support the current theme features: { windowActionBar: " + this.mHasActionBar + ", windowActionBarOverlay: " + this.mOverlayActionBar + ", android:windowIsFloating: " + this.mIsFloating + ", windowActionModeOverlay: " + this.mOverlayActionMode + ", windowNoTitle: " + this.mWindowNoTitle + " }");
            } else {
                if (this.mDecorContentParent == null) {
                    this.mTitleView = (TextView)subDecor.findViewById(id.title);
                }

                ViewUtils.makeOptionalFitsSystemWindows(subDecor);
                ContentFrameLayout contentView = (ContentFrameLayout)subDecor.findViewById(id.action_bar_activity_content);
                ViewGroup windowContentView = (ViewGroup)this.mWindow.findViewById(16908290);
                if (windowContentView != null) {
                    while(windowContentView.getChildCount() > 0) {
                        View child = windowContentView.getChildAt(0);
                        windowContentView.removeViewAt(0);
                        contentView.addView(child);
                    }

                    windowContentView.setId(-1);
                    contentView.setId(16908290);
                    if (windowContentView instanceof FrameLayout) {
                        ((FrameLayout)windowContentView).setForeground((Drawable)null);
                    }
                }
                // 將 subDecor 添加到 DecorView 中
                this.mWindow.setContentView(subDecor);
                contentView.setAttachListener(new OnAttachListener() {
                    public void onAttachedFromWindow() {
                    }

                    public void onDetachedFromWindow() {
                        AppCompatDelegateImpl.this.dismissPopups();
                    }
                });
                return subDecor;
            }
        }
    }
                    

上面的代碼總結來說就是在做一件事,就是創建 subDecor。攤開來說具體如下:

1、根據用戶選擇的主題來設置一些显示特性,包括標題,actionbar 等。

2、根據不同特性來初始化 subDecor;對 subDecor 內部的子 View 進行初始化。

3、最後添加到 DecorView中。

添加的具體代碼如下:此處是通過調用 

 // AppCompatDelegateImpl   this.mWindow.getDecorView();

 // phoneWindow    public final View getDecorView() {
        if (mDecor == null || mForceDecorInstall) {
            installDecor();
        }
        return mDecor;
    }
 

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
 // 生成 DecorView             mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
 // 這樣 DecorView 就持有了window             mDecor.setWindow(this);
        }
      ......
}


   protected DecorView generateDecor(int featureId) {
        // System process doesn't have application context and in that case we need to directly use // the context we have. Otherwise we want the application context, so we don't cling to the // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
   }

到此,DecorView 的創建就講完了。可是我們似乎並沒有看到 DecorView 是被添加的,什麼時候對用戶可見的。

 WindowManager

View 創建完以後,那 Decorview 是怎麼添加到屏幕中去的呢?當然是 WindowManager 呢,那麼是如何將 View 傳到 WindowManager 中呢。

看 ActivityThread 中的 handleResumeActivity 方法:

// ActivityThread
public
void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) { ...... final int forwardBit = isForward ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0; // If the window hasn't yet been added to the window manager, // and this guy didn't finish itself or start another activity, // then go ahead and add the window. boolean willBeVisible = !a.mStartedActivity; if (!willBeVisible) { try { willBeVisible = ActivityManager.getService().willActivityBeVisible( a.getActivityToken()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; ...... if (a.mVisibleFromClient) { if (!a.mWindowAdded) { a.mWindowAdded = true; wm.addView(decor, l); } else { // The activity will get a callback for this {@link LayoutParams} change // earlier. However, at that time the decor will not be set (this is set // in this method), so no action will be taken. This call ensures the // callback occurs with the decor set. a.onWindowAttributesChanged(l); } } // If the window has already been added, but during resume // we started another activity, then don't yet make the // window visible. } else if (!willBeVisible) { if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set"); r.hideForNow = true; } // Get rid of anything left hanging around. cleanUpPendingRemoveWindows(r, false /* force */); // The window is now visible if it has been added, we are not // simply finishing, and we are not starting another activity. if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) { if (r.newConfig != null) { performConfigurationChangedForActivity(r, r.newConfig); if (DEBUG_CONFIGURATION) { Slog.v(TAG, "Resuming activity " + r.activityInfo.name + " with newConfig " + r.activity.mCurrentConfig); } r.newConfig = null; } if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" + isForward); WindowManager.LayoutParams l = r.window.getAttributes(); if ((l.softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != forwardBit) { l.softInputMode = (l.softInputMode & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)) | forwardBit; if (r.activity.mVisibleFromClient) { ViewManager wm = a.getWindowManager(); View decor = r.window.getDecorView(); wm.updateViewLayout(decor, l); } } r.activity.mVisibleFromServer = true; mNumVisibleActivities++; if (r.activity.mVisibleFromClient) {           // 這裏也會調用addview r.activity.makeVisible(); } } r.nextIdle = mNewActivities; mNewActivities = r; if (localLOGV) Slog.v(TAG, "Scheduling idle handler for " + r); Looper.myQueue().addIdleHandler(new Idler()); }

上面的代碼主要做了以下幾件事:

1、獲取到 DecorView,設置不可見,然後通過 wm.addView(decor, l) 將 view 添加到 WindowManager;

2、在某些情況下,比如此時點擊了輸入框調起了鍵盤,就會調用 wm.updateViewLayout(decor, l) 來更新 View 的布局。

3、這些做完以後,會調用 activity 的  makeVisible ,讓視圖可見。如果此時 DecorView 沒有添加到 WindowManager,那麼會添加。 

// Activity
void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); }

 接下來,看下 addview 的邏輯。 WindowManager 的實現類是 WindowManagerImpl,而它則是通過 WindowManagerGlobal 代理實現 addView 的,我們看下 addView 的方法:

// WindowManagerGlobal  
 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
           // ......
    
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
           // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            } 
}

在這裏,實例化了 ViewRootImpl 。同時調用 ViewRootImpl 的 setView 方法來持有了 DecorView。此外這裏還保存了 DecorView ,Params,以及 ViewRootImpl 的實例。

現在我們終於知道為啥 View 是在 OnResume 的時候可見的呢。

 ViewRootImpl

實際上,View 的繪製是由 ViewRootImpl 來負責的。每個應用程序窗口的 DecorView 都有一個與之關聯的 ViewRootImpl 對象,這種關聯關係是由 WindowManager 來維護的。

先看 ViewRootImpl 的 setView 方法,該方法很長,我們將一些不重要的點註釋掉:

   /**
     * We have one child
     */
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                ......
               
                mAdded = true;
                int res; /* = WindowManagerImpl.ADD_OKAY; */

                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.

                requestLayout();
                ......
            }
        }
    }

這裏先將 mView 保存了 DecorView 的實例,然後調用 requestLayout() 方法,以完成應用程序用戶界面的初次布局。

 public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

因為是 UI 繪製,所以一定要確保是在主線程進行的,checkThread 主要是做一個校驗。接着調用 scheduleTraversals 開始計劃繪製了。

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

這裏主要關注兩點:

mTraversalBarrier : Handler 的同步屏障。它的作用是可以攔截 Looper 對同步消息的獲取和分發,加入同步屏障之後,Looper 只會獲取和處理異步消息,如果沒有異步消息那麼就會進入阻塞狀態。也就是說,對 View 繪製渲染的處理操作可以優先處理(設置為異步消息)。

mChoreographer: 編舞者。統一動畫、輸入和繪製時機。也是這章需要重點分析的內容。

mTraversalRunnable :TraversalRunnable 的實例,是一個Runnable,最終肯定會調用其 run 方法:

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

doTraversal,如其名,開始繪製了,該方法內部最終會調用 performTraversals 進行繪製。

  void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

到此,DecorView 與 activity 之間的綁定關係就講完了,下一章,將會介紹 performTraversals 所做的事情,也就是 View 繪製流程。 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Java——內部類詳解

說起內部類,大家肯定感覺熟悉又陌生,因為一定在很多框架源碼中有看到別人使用過,但又感覺自己使用的比較少,今天我就帶你具體來看看內部類。

內部類基礎

所謂內部類就是在類的內部繼續定義其他內部結構類。

在 Java 中,廣泛意義上的內部類一般來說包括這四種:成員內部類、局部內部類、匿名內部類和靜態內部類。下面就先來了解一下這四種內部類的用法。

成員內部類

成員內部類是最普通的內部類,它的定義為位於另一個類的內部,具體使用如下:

class Circle {
    double radius = 0;

    public Circle(double radius) {
        this.radius = radius;
    }

    /**
     * 內部類
     */
    class Draw {
        public void drawSahpe() {
            System.out.println("drawshape");
        }
    }
}

這樣看起來,類 Draw 像是類 Circle 的一個成員, Circle 稱為外部類。成員內部類可以無條件訪問外部類的所有成員屬性和成員方法(包括 private 成員和靜態成員),例如:

class Circle {
    private double radius = 0;
    public static int count =1;
    public Circle(double radius) {
        this.radius = radius;
    }

    /**
     * 內部類
     */
    class Draw {
        public void drawSahpe() {
            // 外部類的private成員
            System.out.println(radius);
            // 外部類的靜態成員
            System.out.println(count);
        }
    }
}

不過要注意的是,當成員內部類擁有和外部類同名的成員變量或者方法時,會發生隱藏現象,即默認情況下訪問的是成員內部類的成員。如果要訪問外部類的同名成員,需要採取以下形式進行訪問:

外部類.this.成員變量
外部類.this.成員方法

雖然成員內部類可以無條件地訪問外部類的成員,而外部類想訪問成員內部類的成員卻不是這麼隨心所欲了。在外部類中如果要訪問成員內部類的成員,必須先創建一個成員內部類的對象,再通過指向這個對象的引用來訪問,其具體形式為:

class Circle {
    private double radius = 0;

    public Circle(double radius) {
        this.radius = radius;
        // 必須先創建成員內部類的對象,再進行訪問
        getDrawInstance().drawSahpe();
    }

    private Draw getDrawInstance() {
        return new Draw();
    }

    /**
     * 內部類
     */
    class Draw {
        public void drawSahpe() {
            // 外部類的private成員
            System.out.println(radius);
        }
    }
}

成員內部類是依附外部類而存在的,也就是說,如果要創建成員內部類的對象,前提是必須存在一個外部類的對象。創建成員內部類對象的一般方式如下:

public class Test {
    public static void main(String[] args)  {
        // 第一種方式
        Outter outter = new Outter();
        // 必須通過Outter對象來創建
        Outter.Inner inner = outter.new Inner();

        // 第二種方式
        Outter.Inner inner1 = outter.getInnerInstance();
    }
}

class Outter {
    private Inner inner = null;
    public Outter() {
    }

    public Inner getInnerInstance() {
        if(inner == null)
            inner = new Inner();
        return inner;
    }

    class Inner {
        public Inner() {
        }
    }
}

內部類可以擁有 private 訪問權限、 protected 訪問權限、 public 訪問權限及包訪問權限。

比如上面的例子,如果成員內部類 Inner 用 private 修飾,則只能在外部類的內部訪問;如果用 public 修飾,則任何地方都能訪問;如果用 protected 修飾,則只能在同一個包下或者繼承外部類的情況下訪問;如果是默認訪問權限,則只能在同一個包下訪問。

這一點和外部類有一點不一樣,外部類只能被 public 和包訪問兩種權限修飾。

我個人是這麼理解的,由於成員內部類看起來像是外部類的一個成員,所以可以像類的成員一樣擁有多種權限修飾。

局部內部類

局部內部類是定義在一個方法或者一個作用域裏面的類,它和成員內部類的區別在於局部內部類的訪問僅限於方法內或者該作用域內。

class People{
    public People() {
    }
}

class Man{
    public Man(){
    }

    public People getWoman(){
        /**
         * 局部內部類
         */
        class Woman extends People{
            int age =0;
        }
        return new Woman();
    }
}

注意,局部內部類就像是方法裏面的一個局部變量一樣,是不能用 public 、 protected 、 private 以及 static 修飾的。

匿名內部類

匿名內部類應該是平時我們編寫代碼時用得最多的,比如創建一個線程的時候:

class Test {

    public static void main(String[] args) {
        Thread thread = new Thread(
                // 匿名內部類
                new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("Thread run");
                    }
                }
        );
    }
}

同樣的,匿名內部類也是不能有訪問修飾符和 static 修飾符的。

匿名內部類是唯一一種沒有構造器的類。正因為其沒有構造器,所以匿名內部類的使用範圍非常有限,大部分匿名內部類用於接口回調。

匿名內部類在編譯的時候由系統自動起名為Outter$1.class。一般來說,匿名內部類用於繼承其他類或是實現接口,並不需要增加額外的方法,只是對繼承方法的實現或是重寫。

靜態內部類

靜態內部類也是定義在另一個類裏面的類,只不過在類的前面多了一個關鍵字 static 。

靜態內部類是不需要依賴於外部類的,這點和類的靜態成員屬性有點類似,並且它不能使用外部類的非 static 成員變量或者方法,這點很好理解,因為在沒有外部類的對象的情況下,可以創建靜態內部類的對象,如果允許訪問外部類的非 static 成員就會產生矛盾,因為外部類的非 static 成員必須依附於具體的對象。

例如:

public class Test {
    public static void main(String[] args)  {
        Outter.Inner inner = new Outter.Inner();
    }
}

class Outter {
    public Outter() {
    }

    /**
     * 靜態
     */
    static class Inner {
        public Inner() {
        }
    }
}

深入理解內部類

通過上面的介紹,相比你已經大致了解的內部類的使用,那麼你的心裏想必會有一個疑惑:

為什麼成員內部類可以無條件訪問外部類的成員?

首先我們先定義一個內部類:

public class Outter {
    private Inner inner = null;

    public Outter() {
    }

    public Inner getInnerInstance() {
        if (inner == null)
            inner = new Inner();
        return inner;
    }

    protected class Inner {
        public Inner() {
        }
    }
}

先用 javac 進行編譯,你可以發現會生成兩個文件: Outter$Inner.class 和 Outter.class 。接下來利用javap -p反編譯 Outter$Inner.class ,其結果如下:

Classfile /D:/project/Test/src/test/java/test/Outter$Inner.class
  Last modified 2019-11-25; size 408 bytes
  MD5 checksum b936e37bc77059b83951429e28f3f225
  Compiled from "Outter.java"
public class Outter$Inner
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Fieldref           #3.#13         // test/Outter$Inner.this$0:Ltest/Outter;
   #2 = Methodref          #4.#14         // java/lang/Object."<init>":()V
   #3 = Class              #16            // test/Outter$Inner
   #4 = Class              #19            // java/lang/Object
   #5 = Utf8               this$0
   #6 = Utf8               Ltest/Outter;
   #7 = Utf8               <init>
   #8 = Utf8               (Ltest/Outter;)V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               SourceFile
  #12 = Utf8               Outter.java
  #13 = NameAndType        #5:#6          // this$0:Ltest/Outter;
  #14 = NameAndType        #7:#20         // "<init>":()V
  #15 = Class              #21            // test/Outter
  #16 = Utf8               test/Outter$Inner
  #17 = Utf8               Inner
  #18 = Utf8               InnerClasses
  #19 = Utf8               java/lang/Object
  #20 = Utf8               ()V
  #21 = Utf8               test/Outter
{
  final Outter this$0;
    descriptor: Ltest/Outter;
    flags: ACC_FINAL, ACC_SYNTHETIC

  public Outter$Inner(Outter);
    descriptor: (Ltest/Outter;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:Ltest/Outter;
         5: aload_0
         6: invokespecial #2                  // Method java/lang/Object."<init>":()V
         9: return
      LineNumberTable:
        line 16: 0
        line 17: 9
}
SourceFile: "Outter.java"
InnerClasses:
     protected #17= #3 of #15; //Inner=class test/Outter$Inner of class test/Outter

32行的內容為:final Outter this$0;

學過 C 的朋友應該能知道,這是一個指向外部類 Outter 對象的指針,也就是說編譯器會默認為成員內部類添加一個指向外部類對象的引用,這樣也就解釋了為什麼成員內部類能夠無條件訪問外部類了。

那麼這個引用是如何賦初值的呢?下面接着看內部類的構造器:public Outter$Inner(Outter);

從這裏可以看出,雖然我們在定義的內部類的構造器是無參構造器,但編譯器還是會默認添加一個參數,該參數的類型為指向外部類對象的一個引用,所以成員內部類中的 Outter this&0 指針便指向了外部類對象,因此可以在成員內部類中隨意訪問外部類的成員。

從這裏也間接說明了成員內部類是依賴於外部類的,如果沒有創建外部類的對象,則無法對 Outter this&0 引用進行初始化賦值,也就無法創建成員內部類的對象了。

為什麼局部內部類和匿名內部類只能訪問局部final變量?

我們還是採用和之前一樣的解答方式,先定義一個類:

public class Outter {

    public static void main(String[] args)  {
        Outter outter = new Outter();
        int b = 10;
        outter.test(b);
    }

    public void test(final int b) {
        final int a = 10;
        new Thread(){
            public void run() {
                System.out.println(a);
                System.out.println(b);
            };
        }.start();
    }
}

通過 javac 編譯 Outter,也會生成兩個文件: Outter.class 和 Outter1.class。默認情況下,編譯器會為匿名內部類和局部內部類起名為 Outter$x.class( x 為正整數)。

根據我提供的類,可以思考一個問題:

當 test 方法執行完畢之後,變量 a 的生命周期就結束了,而此時 Thread 對象的生命周期很可能還沒有結束,那麼在 Thread 的 run 方法中繼續訪問變量 a 就變成不可能了,但是又要實現這樣的效果,怎麼辦呢?

Java 採用了複製的手段來解決這個問題。將 Outter$1.class 反編譯可以得到下面的內容:

Classfile /D:/project/Test/src/test/java/test/Outter$1.class
  Last modified 2019-11-25; size 653 bytes
  MD5 checksum 2e238dafbd73356eba22d473c6469082
  Compiled from "Outter.java"
class test.Outter$1 extends java.lang.Thread
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Fieldref           #6.#23         // test/Outter$1.this$0:Ltest/Outter;
   #2 = Fieldref           #6.#24         // test/Outter$1.val$b:I
   #3 = Methodref          #7.#25         // java/lang/Thread."<init>":()V
   #4 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #28.#29        // java/io/PrintStream.println:(I)V
   #6 = Class              #30            // test/Outter$1
   #7 = Class              #32            // java/lang/Thread
   #8 = Utf8               val$b
   #9 = Utf8               I
  #10 = Utf8               this$0
  #11 = Utf8               Ltest/Outter;
  #12 = Utf8               <init>
  #13 = Utf8               (Ltest/Outter;I)V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               run
  #17 = Utf8               ()V
  #18 = Utf8               SourceFile
  #19 = Utf8               Outter.java
  #20 = Utf8               EnclosingMethod
  #21 = Class              #33            // test/Outter
  #22 = NameAndType        #34:#35        // test:(I)V
  #23 = NameAndType        #10:#11        // this$0:Ltest/Outter;
  #24 = NameAndType        #8:#9          // val$b:I
  #25 = NameAndType        #12:#17        // "<init>":()V
  #26 = Class              #36            // java/lang/System
  #27 = NameAndType        #37:#38        // out:Ljava/io/PrintStream;
  #28 = Class              #39            // java/io/PrintStream
  #29 = NameAndType        #40:#35        // println:(I)V
  #30 = Utf8               test/Outter$1
  #31 = Utf8               InnerClasses
  #32 = Utf8               java/lang/Thread
  #33 = Utf8               test/Outter
  #34 = Utf8               test
  #35 = Utf8               (I)V
  #36 = Utf8               java/lang/System
  #37 = Utf8               out
  #38 = Utf8               Ljava/io/PrintStream;
  #39 = Utf8               java/io/PrintStream
  #40 = Utf8               println
{
  final int val$b;
    descriptor: I
    flags: ACC_FINAL, ACC_SYNTHETIC

  final test.Outter this$0;
    descriptor: Ltest/Outter;
    flags: ACC_FINAL, ACC_SYNTHETIC

  test.Outter$1(test.Outter, int);
    descriptor: (Ltest/Outter;I)V
    flags:
    Code:
      stack=2, locals=3, args_size=3
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:Ltest/Outter;
         5: aload_0
         6: iload_2
         7: putfield      #2                  // Field val$b:I
        10: aload_0
        11: invokespecial #3                  // Method java/lang/Thread."<init>":()V
        14: return
      LineNumberTable:
        line 10: 0

  public void run();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: bipush        10
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
         8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_0
        12: getfield      #2                  // Field val$b:I
        15: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        18: return
      LineNumberTable:
        line 12: 0
        line 13: 8
        line 14: 18
}
SourceFile: "Outter.java"
EnclosingMethod: #21.#22                // test.Outter.test
InnerClasses:
     #6; //class test/Outter$1

我們看到在 run 方法中有一條指令:bipush 10

這條指令表示將操作數10壓棧,表示使用的是一個本地局部變量。

這個過程是在編譯期間由編譯器默認進行,如果這個變量的值在編譯期間可以確定,則編譯器默認會在匿名內部類(局部內部類)的常量池中添加一個內容相等的字面量或直接將相應的字節碼嵌入到執行字節碼中。

這樣一來,匿名內部類使用的變量是另一個局部變量,只不過值和方法中局部變量的值相等,因此和方法中的局部變量完全獨立開。

接下來也來看一下 test.Outter$1 的構造方法:test.Outter$1(test.Outter, int);

我們看到匿名內部類 Outter$1 的構造器含有兩個參數,一個是指向外部類對象的引用,一個是 int 型變量,很顯然,這裡是將變量 test 方法中的形參 b 以參數的形式傳進來對匿名內部類中的拷貝(變量 b 的拷貝)進行賦值初始化。

也就說如果局部變量的值在編譯期間就可以確定,則直接在匿名內部裏面創建一個拷貝。如果局部變量的值無法在編譯期間確定,則通過構造器傳參的方式來對拷貝進行初始化賦值。

從上面可以看出,在 run 方法中訪問的變量 b 根本就不是test方法中的局部變量 b 。這樣一來就解決了前面所說的 生命周期不一致的問題。但是新的問題又來了,既然在 run 方法中訪問的變量 b 和test方法中的變量 b 不是同一個變量,那麼當在 run 方法中改變變量 b 的值的話,會出現什麼情況?

會造成數據不一致性,這樣就達不到原本的意圖和要求。為了解決這個問題, Java 編譯器就限定必須將變量 b 限製為 final ,不允許對變量 b 進行更改(對於引用類型的變量,是不允許指向新的對象),這樣數據不一致性的問題就得以解決了。

到這裏,想必大家應該清楚為何 方法中的局部變量和形參都必須用 final 進行限定了。

靜態內部類有特殊的地方嗎?

從前面可以知道,靜態內部類是不依賴於外部類的,也就說可以在不創建外部類對象的情況下創建內部類的對象。

另外,靜態內部類是不持有指向外部類對象的引用的,這個讀者可以自己嘗試反編譯 class 文件看一下就知道了,是沒有 Outter this&0 引用的。

總結

今天介紹了內部類相關的知識,包括其一般的用法以及內部類和外部類的依賴關係,通過對字節碼進行反編譯詳細了解了其實現模式,最後留給大家一個任務自己去實際探索一下靜態內部類的實現。希望通過這篇介紹可以幫大家更加深刻了解內部類。

有興趣的話可以訪問我的博客或者關注我的公眾號、頭條號,說不定會有意外的驚喜。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

08 決策樹與隨機森林

08 決策樹與隨機森林

決策樹之信息論基礎

認識決策樹

  1. 來源: 決策樹的思想來源非常樸素,程序設計中的條件分支結構就是if – then 結構,最早的決策樹就是利用這類結構分割數據的一種分類學習方法。

  2. 舉例:是否見相親對象

信息的度量和作用

  1. 克勞德 .艾爾伍德 .香農:信息論創始人,密西根大學學士,麻省理工學院博士。 1948年發表了划時代論文 – 通信的數學原理,奠定了現代信息論的基礎。
  2. 信息的單位: 比特 (bit)

  3. 舉例: 以32支球隊爭奪世界杯冠軍
  • 如果不知道任何球隊的信息,每支球隊得冠概率相等。
    以二分法預測,最少需要使用5次才能預測到準確結果。 5 = log32 (以2為底)
    5 = -(1/32log1/32 + 1/32log1/32 + ……)

  • 開放一些信息,則小於5bit, 如1/6 德國,1/6 巴西, 1/10 中國
    5 > -(1/6log1/4 + 1/6log1/4 + ….)

  1. 信息熵:
  • “誰是世界杯冠軍”的信息量應該比5 bit少, 它的準確信息量應該是:
  • H = -(p1logp1 + p2logp2 + p3logp3 +……p32logp32 ) Pi 為第i支球隊獲勝的概率
  • H 的專業術語就是信息熵,單位為比特

決策樹的劃分以及案例

信息增益

  1. 定義: 特徵A對訓練數據集D的信息增益g(D,A), 定義為集合D的信息熵H(D)與特徵A給定條件下D的信息條件熵H(D|A) 之差,即:
    g(D,A) = H(D) – H(D | A)
    注: 信息增益表示得知特徵 X 的信息而使得類 Y的信息的不確定性減少的程度。

  2. 以不同特徵下的信貸成功率為例

  • H(D) = -(9/15log(9/15) + 6/15log(6/15)) = 0.971 # 以類別進行判斷,只有是否兩種類別
  • gD,年紀) = H(D) – H(D’|年紀) = 0.971 – [1/3H(青年)+ 1/3H(中年)+ 1/3H(老年)] # 三種年紀對應的目標值均佔1/3
    – H(青年) = -(2/5log(2/5) + 3/5log(3/5)) # 青年類型中,類別的目標值特徵為(2/5, 3/5)
    – H(中年) = -(2/5log(2/5) + 3/5log(3/5))
    – H(老年) = -(4/5log(2/5) + 1/5log(3/5))

令A1, A2, A3, A4 分別表示年齡,有工作,有房子和信貸情況4個特徵,則對應的信息增益為:
g(D,A1) = H(D) – H(D|A1)
其中,g(D,A2) = 0.324 , g(D,A3) = 0.420 , g(D,A4) = 0.363
相比而言,A3特徵(有房子)的信息增益最大,為最有用特徵。
所以決策樹的實際劃分為:

常見決策樹使用的算法

  1. ID3
  • 信息增益,最大原則
  1. C4.5
  • 信息增益比最大原則 (信息增益占原始信息量的比值)
  1. CART
  • 回歸樹: 平方誤差最小
  • 分類樹: 基尼係數最小原則 (劃分的細緻),sklearn默認的劃分原則

Sklearn決策樹API

  1. sklearn.tree.DecisionTreeClassifier(criterion=’gini’, max_depth=None, random_state=None)
  • criterion (標準): 默認基尼係數,也可以選用信息增益的熵‘entropy’
  • max_depth: 樹的深度大小
  • random_state: 隨機數種子
  1. 決策樹結構
    sklearn.tree.export_graphviz() 導出DOT文件格式
  • estimator: 估算器
  • out_file = “tree.dot” 導出路徑
  • feature_name = [,] 決策樹特徵名

決策樹預測泰坦尼克號案例

import pandas as pd
from sklearn.feature_extraction import DictVectorizer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, export_graphviz

"""
泰坦尼克數據描述事故后乘客的生存狀態,該數據集包括了很多自建旅客名單,提取的數據集中的特徵包括:
票的類別,存貨,等級,年齡,登錄,目的地,房間,票,船,性別。
乘坐等級(1,2,3)是社會經濟階層的代表,其中age數據存在缺失。
"""


def decision():
    """
    決策樹對泰坦尼克號進行預測生死
    :return: None
    """
    # 1.獲取數據
    titan = pd.read_csv('./titanic_train.csv')

    # 2.處理數據,找出特徵值和目標值
    x = titan[['Pclass', 'Age', 'Sex']]
    y = titan[['Survived']]
    # print(x)

    # 缺失值處理 (使用平均值填充)
    x['Age'].fillna(x['Age'].mean(), inplace=True)
    print(x)
    # 3.分割數據集到訓練集和測試集
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.25)

    # 4. 進行處理(特徵工程) 特徵,類別 --> one_hot編碼
    dict = DictVectorizer(sparse=False)
    x_train = dict.fit_transform(x_train.to_dict(orient='records'))
    print(dict.get_feature_names())
    x_test = dict.transform(x_test.to_dict(orient='records'))  # 默認一行一行轉換成字典
    print(x_train)

    # 5. 用決策樹進行預測
    dec = DecisionTreeClassifier()
    dec.fit(x_train, y_train)

    # 預測準確率
    print("預測的準確率:", dec.score(x_test, y_test))

    # 導出決策樹
    export_graphviz(dec, out_file='./tree.dot', feature_names=['Pclass', 'Age', 'Sex'])
    return None


if __name__ == '__main__':
    decision()

隨機森林

集成學習方法

  1. 定義:集成學習通過建立幾個模型組合,來解決單一預測問題。其工作原理是生成多個分類器 / 模型,各組獨立地學習和作出預測。這些預測最後結合成單預測,因此優於任何一個單分類的租出預測。

隨機森林

  1. 定義:在機器學習中,隨機森林是一個包含多個決策樹的分類器,並且其輸出的類別是由個別樹輸出的類別的眾數而定。
    例如: 訓練了5棵樹,其中4棵樹的結果是True, 1棵樹為False, 那麼最終的結果就是True. (投票)

  2. 問題: 如果每棵樹使用相同的特徵,相同的分類器,參數也相同,建立的每棵樹不就是相同的么?

隨機森林建立多個決策樹的過程:

單個樹的建立:(N個樣本,M個特徵)

  1. 隨機在N個樣本中選擇一個樣本,重複N次, 樣本有可能重複
  2. 隨機在M個特徵當中選出m個特徵 m << M
  3. 建立10棵決策樹,樣本,特徵大多不一樣 隨機有放回的抽樣 (bootstrap抽樣)

為什麼要隨機抽樣訓練集?

如果不隨機抽樣,每棵樹的訓練集都一樣,那麼最終訓練處的樹分類結果也是完全一樣的

為什麼要有放回的抽樣?

如果不是有放回的抽樣,那麼每棵樹的訓練樣本都是不同的,都是沒有交集的,這樣的每棵樹都是“有偏的”,“片面的”。即,每棵樹訓練出來都是有很大的差異,而隨機森鈴最後分類取決於多棵樹(弱分類器)的投票表決。

隨機森林 API

  • 分類器:sklearn.ensemble.RandomForestClassifier
    • n_estimators:integer(整數),option, default=10 (森林里數目的數量)
    • criteria: string (default =’gini’) 分割特徵的測量方法
    • max_depth 樹的最大深度
    • max_feature = ‘auto’ 每個決策樹的最大特徵數量
    • bootstrap: default = True 是否放回抽樣

隨機森林的優點

  1. 在當前的所有算法中,具有極好的準確率
  2. 能有有效地運行在大數據集上 (樣本,特徵)
  3. 能夠處理具有高維特徵的輸入樣本,不需要降維
  4. 能夠評估各個特徵在分類問題上的重要性

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

EF Core For MySql查詢中使用DateTime.Now作為查詢條件的一個小問題

背景

最近一直忙於手上澳洲線上項目的整體遷移和升級的準備工作,導致博客和公眾號停更。本周終於艱難的完成了任務,藉此機會,總結一下項目中遇到的一些問題。

EF Core一直是我們團隊中中小型項目常用的ORM框架,在使用SQL Server作為持久化倉儲的場景一下,一直表現還中規中矩。但是在本次項目中,項目使用了MySql作為持久化倉儲。為了與EF Core集成,團隊使用了Pomelo.EntityFrameworkCore.MySql作為EF Core For MySql的擴展。在開發過程中,團隊遇到了各種各樣在SQL Server場景下沒有遇到過的問題,其中最奇怪的,也是隱藏最深的問題,就是將DateTime.Now作為查詢條件,產生了非預期的結果。

問題場景

本周在項目升級的過程中,客戶反饋了一個問題。

在當前系統的Dashboard頁面,有一個消息提醒功能,客戶可以自定義一些消息,並且指定提醒的日期。客戶遇到的問題是通常添加的消息提醒,在指定日期的上午時間段是不會显示,只有在下午時間段才能看到,比如說客戶指定2019年10月26號看到一個的消息提醒,但是在10月26日這天早上8:00-12:00這個時間段,系統總是看不到提醒,只有到了下午的時間段才能看到提醒。

PS:這裏客戶表達的只是個籠統的問題,但問題確實是上午的大部分時間是看不到消息提醒的,但並不是精確到中午12:00點這個時間, 所以此處不必過於糾結於具體的時間。

查看問題代碼

看到這個問題的時候,我自己也很奇怪,難道代碼或者數據庫使用了時區,導致查詢出現了偏差?

於是我就Review了一下此處的查詢, 代碼如下。

var query = DbContext.CRM_Note_Reminders
    .Include(x => x.CRM_Note)
    .Where(x => !x.CRM_Note.Is_Deleted 
             && !x.Is_Deleted
             && x.Reminder_Date.Date <= DateTime.Now.Date)
     .ToList();

PS: 這裏可能有同學會有疑問,為啥不用DbFunctions.DiffDays? 原因是DbFunctions.DiffDays是 EF Core for SQLServer的擴展方法,針對MySql還沒有官方的實現方案。

從這個查詢中,我沒有看出任何問題,於是我直接藉助一些日誌工具,將EF Core生成的查詢語句的輸出了出來。

其中WHERE條件部分如下:

WHERE (((`x.CRM_Note`.`Is_Deleted` = FALSE) 
AND (`x`.`Is_Deleted` = FALSE))
AND (CONVERT(`x`.`Reminder_Date`, date) 
  <= CONVERT(CURRENT_TIMESTAMP(), date)))

這裏CURRENT_TIMESTAMP()是MySql的內置函數,與SQLServer的內置函數GETDATE()不同,CURRENT_TIMESTAMP()默認返回的是UTC時間。因此我們大概能知道,為什麼澳洲客戶會遇到上面的場景了。

PS: 根據7樓兄弟的反饋,我試了一下,改動Mysql的時區配置之後,果然CURRENT_TIMESTAMP()就改為了對應時區的時間。這裏使用UTC時間的原因應該是我在AWS RDS上創建Mysql實例的時候,忽略了時區配置。

由於澳洲處於東10區,與UTC時間有+10個小時的時差,所以當澳洲上午的10點之前,UTC時間都是在當前澳洲日期的前一天,所以系統中出現了當天的消息提醒在上午時間段不能正常显示的問題。

PS: 由於澳洲是分冬令時和夏令時的,夏令時時間要加一個小時,所以實際上客戶在每天的11點之前都無法看到正確的消息提醒。

深入思考

你這可能會非常奇怪,為什麼DateTime.Now會被轉化成內置函數CURRENT_TIMESTAMP(),而沒有使用我們傳入的值DateTime.Now.Date呢?

其實EF/EF Core在查詢是時候是分2個階段的,一個是組合查詢表達式樹的階段,一個是真正的查詢階段。

在組合查詢表達式樹的階段,EF/EF Core只會去組合表達式,而不會去嘗試計算表達式的值,所以這個階段DateTime.Now.Date的值並沒有被計算出來, 在進入正常查詢階段的時候, EF/EF Core會嘗試將查詢表達式樹翻譯成SQL腳本,這時候由於我們的EF ProviderMySql Provider, 恰巧DateTime.Now可以翻譯成Mysql的內置函數CURRENT_TIMESTAMP(), 所以這裏EF/EF Core就跳過了表達式值的計算,直接將其翻譯成了對應的內置函數,所以導致生成的SQL查詢和我們的預期有偏差。

那麼我們該如何解決這個問題呢?

解決方案

經過了以上的思考,其實解決這個問題也就很簡單了,我們可以將DateTime.Now.Date先計算出來,保存在一個變量中,然後將這個變量傳入查詢中。

var today = DateTime.Now.Date;

var query = DbContext.CRM_Note_Reminders
     .Include(x => x.CRM_Note)
     .Where(x => !x.CRM_Note.Is_Deleted 
             && !x.Is_Deleted
             && x.Reminder_Date.Date <= today)
     .ToList();

由此生成的MySQL腳本如下:

WHERE (((`x.CRM_Note`.`Is_Deleted` = FALSE) 
AND (`x`.`Is_Deleted` = FALSE)) 
AND (CONVERT(`x`.`Reminder_Date`, date) <= @__date_0)) 

這樣我們就得到了一個正確的結果,澳洲客戶也就收到了正確的消息。

是不是有種差之毫厘,謬以千里的感覺呢?

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

政府重視電動機車電池電芯國產化,2020 持續補助購買電動機車

行政院副院長陳其邁在 7 月 15 日參訪 Gogoro 智能工廠,強調政府對電動機車電池電芯國產化的重視,並表示明年還是會持續補助消費者購買電動機車。

行政院副院長陳其邁與政務委員龔明鑫、經濟部次長林全能和環保署副署長沈志修等人前往桃園市龜山區,參訪 Gogoro 的智能工廠。除了現場試乘 5 月發表的新車種 Gogoro 3 車款並體驗交換電池外,也與 Gogoro 執行長陸學森針對電池電芯國產化進程與傳統機車產業合作等議題進行交流。

陳其邁表示,政府非常重視電動機車電池電芯國產化,目前除了經濟部的科專與 A+ 計畫外,對於國產電池的模組設計、儲能技術與電池的智慧管理也提供計畫類型的補助,希望加速國產電芯量產,並建立相關驗證機制,為國內電動機車產業鏈提供幫助。陳其邁強調,政府也相當重視電動機車產業與傳統機車行合作的問題,希望傳統機車行未來不論在營運、銷售與後續維修,都能與電動機車業者有更多合作機會,協助推動傳統機車行升級和轉型。

根據陳其邁的說法,政府在 2019 年對購買電動機車的補助維持不變,2020 年的補助還會持續下去。政府也研議鼓勵民眾淘汰排放污染的老舊機車,補助購買更環保的燃油機車,讓民眾使用的機車兼顧便捷與環保。

Gogoro 執行長陸學森表示,截至 2019 年 6 月底,Gogoro 生產銷售的電動機車已近 18 萬輛,全台已布建 1,283 個電池交換站,在 6 大都會區更是每 5 分鐘車程就有可即時換電的電池充電站。Gogoro 的每一部車子都是在桃園市龜山區的工廠生產製造,而且機車上的所有零組件,均來自全台 192 家供應商的供應鏈,做到電動機車的國產化。陸學森也向陳其邁建言,希望在台灣目前重型電動機車技術仍領先全球 3 到 5 年的優勢下,政府能夠支持電動機車產業,讓台灣的電動機車產業團結一致,一起為台灣打贏「世界盃」。

(合作媒體:。首圖來源: CC BY 2.0)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

eMOVING 極速 100 公里電動機車 iE125 上市,快充 10 分鐘可騎 78 公里

中華汽車在 7 月 19 日發表新款電動機車 eMOVING iE125,極速可達到時速 100 公里,成為市場關注的焦點。

iE 125 的命名除了代表 intelligent electricity,也有 industrial engineering 的含義,125 則象徵 125cc 等級燃油機車的性能與操控。iE125 安全極速達到時速 100 公里,靜止加速到時速 50 公里僅需 3.9 秒,30% 坡度爬坡最高速為時速 32 公里。iE125 在充滿電之後,時速 30 公里之下續航里程為 155 公里,TES 變速續航里程為 82 公里。支援超級快充功能,充電 10 分鐘就能充滿 50% 的電力,可以行駛 78 公里。

iE125 含電池車重為 124 公斤,配備 CBS 連動煞車和 IP67 防水等級。提供 ECO、SPEED 和 BOOST 三種行車模式,讓消費者在不同情境下使用。特別的電動駐車功能只要按下按鈕就能直接立起中柱,車主不再需要為立中柱而困擾。配置 QC 3.0 USB 充電座,方便騎乘時進行充電。

eMOVING iE125 藍色版。

eMOVING iE125 白色版。

eMOVING iE125 橘色版。

eMOVING iE125 灰色版。

iE125 精緻型儀錶板螢幕為彩色液晶螢幕,豪華型和旗艦型則為汽車級 TFT。豪華型和旗艦型搭載車輛診斷系統,能在儀表板上顯示車身、動力、電池和胎壓等資訊,並提供異常提示與保養提醒。儀表板還可以進行個人化設定,自由更換儀表板主題與桌布。

iE125 豪華型和旗艦型具有遙控防盜中控鎖,能夠連結手機進行上鎖、解鎖和座墊開啟。iOS 版本的 App 支援即時來電提醒,來電與訊息通知會即時顯示。旗艦型還特別內建前方行車記錄器和胎壓偵測器,進一步確保行車安全。

儀錶板能顯示各種車身資訊。

eMOVING 將充電分為 3 種類型,分別在不同需求時使用。家用滿足平時充電需求,約 160 分鐘可以充滿 50% 的電力。快速充電站則供在外逛街或用餐時補充電力,約 30 分鐘可以充滿 50% 的電力。超級充電站供臨時路途中繼充電,約 10 分鐘可以充滿 50% 的電力。

消費者可以自行選擇電池租賃方案,eMOVING 提供電池永久保固。基礎型在家充電每月 399 元,輕量型每月 599 元額外提供 100 分鐘的超級充電分鐘數,進階型每月 799 元可以不限時數進行超級充電。為了推廣超級充電,12 月以前輕量型和進階型方案皆以每月 499 元計價,而且享受不限時數的超級充電。

中華汽車預計在 12 月佈建 70 座以上的快速充電站,2020 年 6 月更要佈建超過 150 座快速充電站,早期合作夥伴包括肯德基、家樂福、順益汽車和滙豐汽車。中華汽車也宣布捐贈 5 座超級充電站給桃園市政府,未來讓符合快充共通規格電動機車車主都能免費充電。

中華汽車捐贈 5 座超級充電站給桃園市政府。

eMOVING 推出了 10 月底前購車,就贈送 5,000 元購車金的優惠,可以全額折抵車價或購買配件。iE125 提供藍色、白色、灰色和橘色 4 種顏色讓消費者選擇,精緻型定價為台幣 73,800 元,豪華型定價為台幣 79,800 元,旗艦型定價為台幣 85,800 元。補助最高的桃園市汰換二行程機車換購電動機車補助最高 29,000 元,再加上 10 月底前購車贈送的購車金 5,000 元,精緻型最低台幣 39,800 元起,豪華型最低台幣 45,800 元起,旗艦型最低台幣 51,800 元起。

(合作媒體:。圖片來源:)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

新發現一種抗體有望開發為通用型流感疫苗

  新華社華盛頓 10 月 26 日電(記者周舟)美國科研團隊發現一種能“嵌入”流感病毒表面蛋白的抗體,可保護小鼠免遭多種流感病毒毒株的感染,未來有望開發為通用型流感疫苗。

  血凝素(H蛋白)和神經氨酸酶(N蛋白)是流感病毒表面的兩種蛋白,它們將流感病毒分為不同的亞型。目前開發的流感疫苗主要靶向血凝素。2017 年冬,美國華盛頓大學病理學和免疫學助理教授阿里·艾利貝迪發現一個流感患者的血樣不僅含有靶向血凝素的抗體,還含有可靶向其他蛋白的抗體。

  艾利貝迪將其中三種靶向不明的抗體送至芒特西奈伊坎醫學院進行檢測,該院微生物學教授弗洛里安·克拉默發現其中一種被稱為“1G01”的抗體,可阻斷多種流感病毒毒株上幾乎所有已知的神經氨酸酶的活動。

  克拉默團隊讓實驗小鼠感染致命性劑量的流感病毒,發現這種抗體可以對抗 12 種被測試的流感毒株,其中包括三類人類流感病毒毒株、禽流感和其他不在人際間傳播的病毒毒株。實驗發現,所有小鼠都生存了下來,即便在感染 72 小時以後給葯。相比而言,達菲必須在癥狀出現 24 小時內給葯。

  美國斯克里普斯研究所的結構生物學家伊安·威爾遜分析了這種抗體的結構,發現這種抗體將一個環狀結構嵌入神經氨酸酶的活性部位,阻止了神經氨酸酶從細胞表面釋放新的病毒顆粒。

  研究显示,這種抗體只阻斷神經氨酸酶的活性部位,而不同流感毒株間的活性部位幾乎不發生變異,因此它對多種流感病毒均有效。目前研究人員正在以抗體 1G01 為基礎設計新的流感藥物和疫苗。

  這一研究成果日前發表在美國《科學》雜誌上。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

Springboot 系列(十六)你真的了解 Swagger 文檔嗎?

前言

目前來說,在 Java 領域使用 Springboot 構建微服務是比較流行的,在構建微服務時,我們大多數會選擇暴漏一個 REST API 以供調用。又或者公司採用前後端分離的開發模式,讓前端和後端的工作由完全不同的工程師進行開發完成。不管是微服務還是這種前後端分離開發,維持一份完整的及時更新的 REST API 文檔,會極大的提高我們的工作效率。而傳統的文檔更新方式(如手動編寫),很難保證文檔的及時性,經常會年久失修,失去應有的意義。因此選擇一種新的 API 文檔維護方式很有必要,這也是這篇文章要介紹的內容。

1. OpenAPI 規範介紹

OpenAPI Specification 簡稱 OAS,中文也稱 OpenAPI 描述規範,使用 OpenAPI 文件可以描述整個 API,它制定了一套的適合通用的與語言無關的 REST API 描述規範,如 API 路徑規範、請求方法規範、請求參數規範、返回格式規範等各種相關信息,使人類和計算機都可以不需要訪問源代碼就可以理解和使用服務的功能。

下面是 OpenAPI 規範中建議的 API 設計規範,基本路徑設計規範。

https://api.example.com/v1/users?role=admin&status=active
\________________________/\____/ \______________________/
         server URL       endpoint    query parameters
                            path

對於傳參的設計也有規範,可以像下面這樣:

  • , 例如 /users/{id}
  • , 例如 /users?role=未讀代碼
  • , 例如 X-MyHeader: Value
  • , 例如 Cookie: debug=0; csrftoken=BUSe35dohU3O1MZvDCU

OpenAPI 規範的東西遠遠不止這些,目前 OpenAPI 規範最新版本是 3.0.2,如果你想了解更多的 OpenAPI 規範,可以訪問下面的鏈接。

2. Swagger 介紹

很多人都以為 Swagger 只是一個接口文檔生成框架,其實並不是。 Swagger 是一個圍繞着 OpenAPI Specification(OAS,中文也稱 OpenAPI規範)構建的一組開源工具。可以幫助你從 API 的設計到 API 文檔的輸出再到 API 的測試,直至最後的 API 部署等整個 API 的開發周期提供相應的解決方案,是一個龐大的項目。 Swagger 不僅免費,而且開源,不管你是企業用戶還是個人玩家,都可以使用 Swagger 提供的方案構建令人驚艷的 REST API

Swagger 有幾個主要的產品。

  • – 一個基於瀏覽器的 Open API 規範編輯器。
  • – 一個將 OpenAPI 規範呈現為可交互在線文檔的工具。
  • – 一個根據 OpenAPI 生成調用代碼的工具。

如果你想了解更多信息,可以訪問 Swagger 官方網站 。

3. Springfox 介紹

源於 Java 中 Spring 框架的流行,讓一個叫做 Marrty Pitt 的老外有了為 SpringMVC 添加接口描述的想法,因此他創建了一個遵守 OpenAPI 規範(OAS)的項目,取名為 swagger-springmvc,這個項目可以讓 Spring 項目自動生成 JSON 格式的 OpenAPI 文檔。這個框架也仿照了 Spring 項目的開發習慣,使用註解來進行信息配置。

後來這個項目發展成為 Springfox,再後來擴展出 springfox-swagger2 ,為了讓 JSON 格式的 API 文檔更好的呈現,又出現了 springfox-swagger-ui 用來展示和測試生成的 OpenAPI 。這裏的 springfox-swagger-ui 其實就是上面介紹的 Swagger-ui,只是它被通過 webjar 的方式打包到 jar 包內,並通過 maven 的方式引入進來。

上面提到了 Springfox-swagger2 也是通過註解進行信息配置的,那麼是怎麼使用的呢?下面列舉常用的一些註解,這些註解在下面的 Springboot 整合 Swagger 中會用到。

註解 示例 描述
@ApiModel @ApiModel(value = “用戶對象”) 描述一個實體對象
@ApiModelProperty @ApiModelProperty(value = “用戶ID”, required = true, example = “1000”) 描述屬性信息,執行描述,是否必須,給出示例
@Api @Api(value = “用戶操作 API(v1)”, tags = “用戶操作接口”) 用在接口類上,為接口類添加描述
@ApiOperation @ApiOperation(value = “新增用戶”) 描述類的一個方法或者說一個接口
@ApiParam @ApiParam(value = “用戶名”, required = true) 描述單個參數

更多的 Springfox 介紹,可以訪問 Springfox 官方網站。

4. Springboot 整合 Swagger

就目前來說 ,Springboot 框架是非常流行的微服務框架,在微服務框架下,很多時候我們都是直接提供 REST API 的。REST API 如果沒有文檔的話,使用者就很頭疼了。不過不用擔心,上面說了有一位叫 Marrty Pitt 的老外已經創建了一個發展成為 Springfox 的項目,可以方便的提供 JSON 格式的 OpenAPI 規範和文檔支持。且擴展出了 springfox-swagger-ui 用於頁面的展示。

需要注意的是,這裏使用的所謂的 Swagger 其實和真正的 Swagger 並不是一個東西,這裏使用的是 Springfox 提供的 Swagger 實現。它們都是基於 OpenAPI 規範進行 API 構建。所以也都可以 Swagger-ui 進行 API 的頁面呈現。

4.1. 創建項目

如何創建一個 Springboot 項目這裏不提,你可以直接從 下載一個標準項目,也可以使用 idea 快速創建一個 Springboot 項目,也可以順便拷貝一個 Springboot 項目過來測試,總之,方式多種多樣,任你選擇。

下面演示如何在 Springboot 項目中使用 swagger2。

4.2. 引入依賴

這裏主要是引入了 springfox-swagger2,可以通過註解生成 JSON 格式的 OpenAPI 接口文檔,然後由於 Springfox 需要依賴 jackson,所以引入之。springfox-swagger-ui 可以把生成的 OpenAPI 接口文檔显示為頁面。Lombok 的引入可以通過註解為實體類生成 get/set 方法。

<dependencies> 
    <!-- Spring Boot web 開發整合 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <artifactId>spring-boot-starter-json</artifactId>
                <groupId>org.springframework.boot</groupId>
            </exclusion>
        </exclusions>
    </dependency>

    <!-- 引入swagger2的依賴-->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
    
    <!-- jackson相關依賴 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.5.4</version>
    </dependency>

    <!-- Lombok 工具 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

4.3. 配置 Springfox-swagger

Springfox-swagger 的配置通過一個 Docket 來包裝,Docket 里的 apiInfo 方法可以傳入關於接口總體的描述信息。而 apis 方法可以指定要掃描的包的具體路徑。在類上添加 @Configuration 聲明這是一個配置類,最後使用 @EnableSwagger2 開啟 Springfox-swagger2。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * <p>
 * Springfox-swagger2 配置
 *
 * @Author niujinpeng
 * @Date 2019/11/19 23:17
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("net.codingme.boot.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("未讀代碼 API")
                .description("公眾號:未讀代碼(weidudaima) springboot-swagger2 在線借口文檔")
                .termsOfServiceUrl("https://www.codingme.net")
                .contact("達西呀")
                .version("1.0")
                .build();
    }
}

4.4. 代碼編寫

文章不會把所有代碼一一列出來,這沒有太大意義,所以只貼出主要代碼,完整代碼會上傳到 Github,並在文章底部附上 Github 鏈接。

參數實體類 User.java,使用 @ApiModel@ApiModelProperty 描述參數對象,使用 @NotNull 進行數據校驗,使用 @Data 為參數實體類自動生成 get/set 方法。

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;

import javax.validation.constraints.NotNull;
import java.util.Date;

/**
 * <p>
 * 用戶實體類
 *
 * @Author niujinpeng
 * @Date 2018/12/19 17:13
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "用戶對象")
public class User {

    /**
     * 用戶ID
     *
     * @Id 主鍵
     * @GeneratedValue 自增主鍵
     */
    @NotNull(message = "用戶 ID 不能為空")
    @ApiModelProperty(value = "用戶ID", required = true, example = "1000")
    private Integer id;

    /**
     * 用戶名
     */
    @NotNull(message = "用戶名不能為空")
    @ApiModelProperty(value = "用戶名", required = true)
    private String username;
    /**
     * 密碼
     */
    @NotNull(message = "密碼不能為空")
    @ApiModelProperty(value = "用戶密碼", required = true)
    private String password;
    /**
     * 年齡
     */
    @ApiModelProperty(value = "用戶年齡", example = "18")
    private Integer age;
    /**
     * 生日
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd hh:mm:ss")
    @ApiModelProperty(value = "用戶生日")
    private Date birthday;
    /**
     * 技能
     */
    @ApiModelProperty(value = "用戶技能")
    private String skills;
}

編寫 Controller 層,使用 @Api 描述接口類,使用 @ApiOperation 描述接口,使用 @ApiParam 描述接口參數。代碼中在查詢用戶信息的兩個接口上都添加了 tags = "用戶查詢" 標記,這樣這兩個方法在生成 Swagger 接口文檔時候會分到一個共同的標籤組裡。

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import net.codingme.boot.domain.Response;
import net.codingme.boot.domain.User;
import net.codingme.boot.enums.ResponseEnum;
import net.codingme.boot.utils.ResponseUtill;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;

/**
 * <p>
 * 用戶操作
 *
 * @Author niujinpeng
 * @Date 2019/11/19 23:17
 */

@Slf4j
@RestController(value = "/v1")
@Api(value = "用戶操作 API(v1)", tags = "用戶操作接口")
public class UserController {

    @ApiOperation(value = "新增用戶")
    @PostMapping(value = "/user")
    public Response create(@Valid User user, BindingResult bindingResult) throws Exception {
        if (bindingResult.hasErrors()) {
            String message = bindingResult.getFieldError().getDefaultMessage();
            log.info(message);
            return ResponseUtill.error(ResponseEnum.ERROR.getCode(), message);
        } else {
            // 新增用戶信息 do something
            return ResponseUtill.success("用戶[" + user.getUsername() + "]信息已新增");
        }
    }

    @ApiOperation(value = "刪除用戶")
    @DeleteMapping(value = "/user/{username}")
    public Response delete(@PathVariable("username")
                           @ApiParam(value = "用戶名", required = true) String name) throws Exception {
        // 刪除用戶信息 do something
        return ResponseUtill.success("用戶[" + name + "]信息已刪除");
    }

    @ApiOperation(value = "修改用戶")
    @PutMapping(value = "/user")
    public Response update(@Valid User user, BindingResult bindingResult) throws Exception {
        if (bindingResult.hasErrors()) {
            String message = bindingResult.getFieldError().getDefaultMessage();
            log.info(message);
            return ResponseUtill.error(ResponseEnum.ERROR.getCode(), message);
        } else {
            String username = user.getUsername();
            return ResponseUtill.success("用戶[" + username + "]信息已修改");
        }
    }

    @ApiOperation(value = "獲取單個用戶信息", tags = "用戶查詢")
    @GetMapping(value = "/user/{username}")
    public Response get(@PathVariable("username")
                        @NotNull(message = "用戶名稱不能為空")
                        @ApiParam(value = "用戶名", required = true) String username) throws Exception {
        // 查詢用戶信息 do something
        User user = new User();
        user.setId(10000);
        user.setUsername(username);
        user.setAge(99);
        user.setSkills("cnp");
        return ResponseUtill.success(user);
    }

    @ApiOperation(value = "獲取用戶列表", tags = "用戶查詢")
    @GetMapping(value = "/user")
    public Response selectAll() throws Exception {
        // 查詢用戶信息列表 do something
        User user = new User();
        user.setId(10000);
        user.setUsername("未讀代碼");
        user.setAge(99);
        user.setSkills("cnp");
        List<User> userList = new ArrayList<>();
        userList.add(user);
        return ResponseUtill.success(userList);
    }
}

最後,為了讓代碼變得更加符合規範和好用,使用一個統一的類進行接口響應。

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "響應信息")
public class Response {
    /**
     * 響應碼
     */
    @ApiModelProperty(value = "響應碼")
    private String code;
    /**
     * 響應信息
     */
    @ApiModelProperty(value = "響應信息")
    private String message;

    /**
     * 響應數據
     */
    @ApiModelProperty(value = "響應數據")
    private Collection content;
}

4.5. 運行訪問

直接啟動 Springboog 項目,可以看到控制台輸出掃描到的各個接口的訪問路徑,其中就有 /2/api-docs

這個也就是生成的 OpenAPI 規範的描述 JSON 訪問路徑,訪問可以看到。

因為上面我們在引入依賴時,也引入了 springfox-swagger-ui 包,所以還可以訪問 API 的頁面文檔。訪問路徑是 /swagger-ui.html,訪問看到的效果可以看下圖。

也可以看到用戶查詢的兩個方法會歸到了一起,原因就是這兩個方法的註解上使用相同的 tag 屬性。

4.7. 調用測試

springfox-swagger-ui 不僅是生成了 API 文檔,還提供了調用測試功能。下面是在頁面上測試獲取單個用戶信息的過程。

  1. 點擊接口 [/user/{username}] 獲取單個用戶信息。
  2. 點擊 **Try it out** 進入測試傳參頁面。
  3. 輸入參數,點擊 Execute 藍色按鈕執行調用。
  4. 查看返回信息。

下面是測試時的響應截圖。

5. 常見報錯

如果你在程序運行中經常發現像下面這樣的報錯。

java.lang.NumberFormatException: For input string: ""
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) ~[na:1.8.0_111]
    at java.lang.Long.parseLong(Long.java:601) ~[na:1.8.0_111]
    at java.lang.Long.valueOf(Long.java:803) ~[na:1.8.0_111]
    at io.swagger.models.parameters.AbstractSerializableParameter.getExample(AbstractSerializableParameter.java:412) ~[swagger-models-1.5.20.jar:1.5.20]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_111]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_111]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_111]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_111]
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:536) [jackson-databind-2.5.4.jar:2.5.4]
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:666) [jackson-databind-2.5.4.jar:2.5.4]
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:156) [jackson-databind-2.5.4.jar:2.5.4]
    at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:113) [jackson-databind-2.5.4.jar:2.5.4]

那麼你需要檢查使用了 @ApiModelProperty 註解且字段類型為数字類型的屬性上,@ApiModelProperty 註解是否設置了 example 值,如果沒有,那就需要設置一下,像下面這樣。

@NotNull(message = "用戶 ID 不能為空")
@ApiModelProperty(value = "用戶ID", required = true, example = "1000")
private Integer id;

文中代碼都已經上傳到

參考文檔

個人網站:
如果你喜歡這篇文章,可以關注公眾號,一起成長。
關注公眾號回復資源可以沒有套路的獲取全網最火的的 Java 核心知識整理&面試核心資料。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

【設計模式】單例模式的八種姿態寫法分析

目錄

前言
網上泛濫流傳單例模式的寫法種類,有說7種的,也有說6種的,當然也不排除說5種的,他們說的有錯嗎?其實沒有對與錯,刨根問底,寫法終究是寫法,其本質精髓大體一致!因此完全沒必要去追究寫法的多少,有這個時間還不如跟着宜春去網吧偷耳機、去田裡抓青蛙得了,一天天的….

言歸正傳…單例模式是最常用到的設計模式之一,熟悉設計模式的朋友對單例模式絕對不會陌生。同時單例模式也是比較簡單易理解的一種設計模式。

@

何謂單例模式?

專業術語

單例模式是一種常用的軟件設計模式,其定義是單例對象的類只能允許一個實例存在。許多時候整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行為。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然後服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在複雜環境下的配置管理。

單例模式,簡單的說就是 一個類只能有一個實例,並且在整個項目中都能訪問到這個實例。

單例模式的優點

1、在內存中只有一個對象,節省內存空間。
2、避免頻繁的創建銷毀對象,可以提高性能。
3、避免對共享資源的多重佔用。
4、可以全局訪問。

單例模式實現整體思路流程

首先我們要清楚單例模式要求類能夠有返回對象一個引用(永遠是同一個)和一個獲得該實例的方法(必須是靜態方法,通常使用getInstance這個名稱)。

單例模式的常規實現思路大致相同為以下三個步驟:

1、私有構造方法
2、指向自己實例的私有靜態引用
3、以自己實例為返回值的靜態的公有的方法

當然也可以理解為
1、私有化構造方法,讓外部不能new。
2、本類內部創建對象實例【靜態變量目的是為了類加載的時候創建實例】
3、提供一個公有的static靜態方法(一般該方法使用getInstance這個名稱),返回實例對象。

將該類的構造方法定義為私有方法,這樣其他處的代碼就無法通過調用該類的構造方法來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例;
在該類內提供一個靜態方法,當我們調用這個方法時,如果類持有的引用不為空就返回這個引用,如果類保持的引用為空就創建該類的實例並將實例的引用賦予該類保持的引用。

單例模式的適用場景

由於單例模式有很多獨特的優點,所以是編程中用的比較多的一種設計模式。我總結了一下我所知道的適合使用單例模式的場景:

1、需要頻繁實例化然後銷毀的對象。
2、創建對象時耗時過多或者耗資源過多,但又經常用到的對象。
3、有狀態的工具類對象。
4、頻繁訪問數據庫或文件的對象。

在後面我將會講到JDK中的Runtime類就是使用的餓漢式單例!在Spring MVC框架中的controller 默認是單例模式的!

單例模式的八種姿態寫法

宜春強烈建議:如果是沒有接觸單例模式的讀者朋友強烈建議你們動手敲一遍,不要複製,不然沒效果!

還有一點就是,要真正輕而易舉的理解單例模式,JVM的類加載知識是不能少的,不然你只是會敲的層次,啥?不懂類加載?放心,宜春就是要你會,要你理解透徹。

其實上面的這篇文章特別重要,上面這篇文章的重要性懂的自然懂,不懂的希望能理解宜春的一片好意,去看一下吧,實在看不懂看不下去在回來看這篇文章就好了,再大不了就把博主一起按在馬桶蓋蓋上….

是不是心裏暖暖的?宜春也不多嗶嗶了,直接擼碼走起….

姿態一:餓漢式1(靜態變量)

package singletonPattern;
//餓漢式(靜態變量)

class Singleton{
    //1、私有化構造方法,讓外部不能new
    private Singleton(){

    }
    //2、本類內部創建對象實例【靜態變量目的是為了類加載的時候創建實例】
    private final static Singleton instance=new Singleton();

    //3、提供一個公有的static靜態方法,返回實例對象
    public static Singleton getInstance(){
        return instance;
    }
}
//以下是測試代碼=====================

public class SingletenDemo1 {
    public static void main(String[] args) {
        Singleton singleton=Singleton.getInstance();
        Singleton singleton2=Singleton.getInstance();
//驗證一:
        System.out.println(singleton==singleton2);
//驗證二:
        System.out.println(singleton.hashCode());
        System.out.println(singleton2.hashCode());
    }
}

//運行結果:
//        true
//        460141958
//        460141958

/*
餓漢式(靜態變量)方法

優點:寫法簡單,在類加載的時候就完成了實例化,同時也就避免了線程同步問題,因此線程安全
缺點:由於是在類加載時就完成了實例化,沒有達到懶加載的效果。如果一直沒有使用過這個實例,就造成了內存的浪費!

總結:這種方式基於ClassLoader類加載機制避免了多線程的同步問題,只不過instance屬性在類加載就實例化,在單例模式中大多數都是調用getInstance方法,
     由於getInstance方法是static靜態的,調用它肯定會觸發類加載!但是觸發類加載的原因有很多,我們不能保證這個類會通過其他的方式觸發類加載(比如調用了其他的static方法)
     這個時候初始化instance就沒有達到lazy loading 懶加載的效果,可能造成內存的浪費!

     餓漢式(靜態變量)這種方式可以使用但是會造成內存的浪費!

     */

姿態二:餓漢式2(static靜態代碼塊)

package singletonPattern;
//餓漢式2(static靜態代碼塊)

class Singleton2{
    private Singleton2(){

    }

    private static Singleton2 instance;

    static{ //把創建單例對象的操作放進了static靜態代碼塊中==============
        instance = new Singleton2();
    }

    public static Singleton2 getInstance(){
        return instance;
    }
}
//餓漢式2(static靜態代碼塊)其實和第一種餓漢式(靜態變量)方法差不多,其優缺點一致!
//唯一不同的就是把創建單例對象的操作放進了static靜態代碼塊中

姿態三:懶漢式1(線程不安全)

package singletonPattern;
//懶漢式1(線程不安全)
class Singleton3{
    private Singleton3(){

    }

    private static Singleton3 instance;

    public static Singleton3 getInstance(){
        if(instance == null){
            instance=new Singleton3();
        }
        return instance;
    }
}
/*
懶漢式(線程不安全)的這種方式起到了懶加載的效果,但只能在單線程下使用。
如果在多線程下,一個線程進入了if(singleton==null)判斷語句塊,還沒執行產生實例的句子,另一個線程
又進來了,這時會產生多個實例,所以不安全。

結語:懶漢式(線程不安全)在實際開發中,不要使用這種方式!!存在潛在危險
*/

姿態四:懶漢式2(線程安全)

package singletonPattern;
//懶漢式2(線程安全)
class Singleton4{
    private Singleton4(){

    }

    private static Singleton4 instance;

    public static synchronized Singleton4 getInstance(){
        if(instance == null){
            instance=new Singleton4();
        }
        return instance;
    }
}

/*
懶漢式2(線程安全)方式

優點:線程安全
缺點:效率太低,每次調用getInstance方法都要進行同步

結語:懶漢式2(線程安全)方式在開發中不推薦使用,主要是效率太低了*/

姿態五:餓漢式2(static靜態代碼塊)

package singletonPattern;
//懶漢式3 同步代碼塊(線程安全) 但是不滿足單例,在多線程下依舊會有多個實例
class Singleton5{
    private Singleton5(){

    }

    private static Singleton5 instance;

    public static  Singleton5 getInstance(){
        if(instance == null){   //多線程情況下可能多個線程進入這個if塊
            synchronized (Singleton5.class){  //到這裏只會一個一個創建實例,雖然安全,但是就不再是單例了
                instance=new Singleton5();
            }
        }
        return instance;
    }
}
/*
懶漢式3 同步代碼塊(線程安全) 但是不滿足單例,依舊會有多個實例

結語:懶漢式3 同步代碼塊(線程安全)方式在開發中不使用 ,實際上這個單例設計的有點搞笑*/

姿態六:雙重檢查單例

package singletonPattern;
//雙重檢查應用實例方式
class Singleton6{
    private Singleton6(){}

    private static volatile Singleton6 singleton;

    public static Singleton6 getInstance(){
        if(singleton==null){
            synchronized(Singleton6.class){
                if(singleton == null){
                    singleton= new Singleton6();
                }
            }
        }
        return singleton;
    }
}
/*
雙重檢查應用實例方式:

線程安全、延遲加載、效率較高

結語:開發中推薦使用!
*/

這個時候博主就得嗶嗶幾句了,細心的童鞋會發現有一個Volatile關鍵字,完了,沒見過,小白童鞋慌了!

Volatile 變量具有 synchronized 的可見性特性,但是不具備原子特性。這就是說線程能夠自動發現 volatile 變量的最新值。

這種實現方式既可以實現線程安全地創建實例,而又不會對性能造成太大的影響。它只是第一次創建實例的時候同步,以後就不需要同步了,從而加快了運行速度。

姿態七:靜態內部類單例

package singletonPattern;
//static靜態內部類單例

class Singleton7{
    private Singleton7(){}

    private static volatile Singleton7 instance;

    //寫一個static靜態內部類,給該類添加一個static靜態instance屬性
    private static class SingletonInstance{
        private static final Singleton7 SINGLETON_7=new Singleton7();
    }

    //
    public static synchronized Singleton7 getInstence(){
        return SingletonInstance.SINGLETON_7;
    }
}
/*
靜態內部類單例方式
        1、這種方式採用了類加載機制來保證初始化實例時只有一個線程
        2、巧妙的將實例化Singleton操作放進getInstance方法中,getInstance方法返回靜態內部類中實例化好的Singleton
        3、類的靜態屬性只會在第一次加載類的時候初始化,也就是只會初始化一次,在這裏,JVM幫我們保證了線程的安全,類在初始化時,別的線程無法進入。
       
        優點:線程安全、利用靜態內部類特點實現延遲加載、效率高
        開發中推薦使用這種靜態內部類單例方式!

static靜態內部特點:
1、外部類加載不會導致內部類加載,保證了其懶加載
*/

這個單例,宜春就不得不嗶嗶兩句了,要清楚這個單例,必須要明白static靜態內部特點,也就是外部類加載不會導致內部類加載!

姿態八:餓漢式2(static靜態代碼塊)

package singletonPattern;
//使用枚舉

import com.sun.xml.internal.bind.v2.runtime.unmarshaller.XsiNilLoader;

enum Singleton8{
    INSTANCE;
    public void methodName(){
        System.out.println("測試數據");
    }
}
/*

枚舉方式的枚舉:
推薦寫法,簡單高效。充分利用枚舉類的特性,只定義了一個實例,且枚舉類是天然支持多線程的。
藉助JDK1.5中添加的枚舉來實現單例模式優點:
         1、不僅能避免多線程同步問題 
         2、還能防止反序列化重新創建新的對象

枚舉方式單例是由Effective java作者Josh Bloch提倡的,結語:推薦使用!
*/

當然也可以測試一下

public class SingletonDemo8 {
    public static void main(String[] args) {
        Singleton8 instance = Singleton8.INSTANCE;
        Singleton8 instance2 = Singleton8.INSTANCE;
        System.out.println(instance==instance2);

        System.out.println(instance.hashCode());
        System.out.println(instance2.hashCode());

        instance.methodName();
    }
}

運行結果:

true
460141958
460141958
測試數據

屬實沒毛病!

JDK源碼中單例模式的應用

先來看一段Runtime 的源碼吧,並分析一下其使用的是種單例模式!

/**
 * Every Java application has a single instance of class
 * <code>Runtime</code> that allows the application to interface with
 * the environment in which the application is running. The current
 * runtime can be obtained from the <code>getRuntime</code> method.
 * <p>
 * An application cannot create its own instance of this class.
 *
 * @author  unascribed
 * @see     java.lang.Runtime#getRuntime()
 * @since   JDK1.0
 */
public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

這應該不難看出吧!如果看不出的話只能說明你真的還沒有理解單例模式,我其實想說單例模式其實是23種設計模式中最簡單的一個,只是寫法比較多而已!同時面試官一般都會問單例模式,它已經是很基礎的了,問的稍微有點水平就是問你單例模式在JDK中哪裡運用到了,顯然JDK中的Runtime其實它使用的就是餓漢式單例!正如註釋所說,每一個java應用程序都有一個Runtime實例。Runtime的單例模式是採用餓漢模式創建的,意思是當你加載這個類文件時,這個實例就已經存在了。

Runtime類可以取得JVM系統信息,或者使用gc()方法釋放掉垃圾空間,還可以使用此類運行本機的程序。

==還有就是spring Mvc 中的controller 默認是單例模式的,解析。==

單例模式總結

1、餓漢式(靜態變量)這種方式可以使用,但是沒有達到 lazy loading 懶加載的效果會造成內存的浪費!開發中不建議使用。
2、餓漢式(static靜態代碼塊)其實和第一種餓漢式(靜態變量)方法差不多,其優缺點一致!唯一不同的就是把創建單例對象的操作放進了static靜態代碼塊中
3、懶漢式(線程不安全)起到了懶加載的效果,但只能在單線程下使用。在實際開發中,不要使用這種方式!!!
4、懶漢式2(線程安全)方式線程安全但是效率太低,每次調用getInstance方法都要進行同步。所以在開發中不推薦使用。 5、懶漢式3
同步代碼塊(線程安全)方式在開發中不使用 ,實際上這個設計有點搞笑哈哈。
6、雙重檢查應用實例方式,線程安全、延遲加載、效率較高。因此開發中推薦使用!
7、靜態內部類單例方式線程安全、利用靜態內部類特點實現延遲加載、效率高。 開發中推薦使用這種靜態內部類單例方式!
8、藉助JDK1.5中添加的枚舉來實現單例模式不僅能避免多線程同步問題還能防止反序列化重新創建新的對象。枚舉方式單例是由Effective java作者Josh Bloch提倡的,開發中推薦使用!

單例模式必須考慮到在多線程的應用場合下的使用,畢竟現在的服務器基本上都是多核的了。

如果本文對你有一點點幫助,那麼請點個讚唄,謝謝~

最後,若有不足或者不正之處,歡迎指正批評,感激不盡!如果有疑問歡迎留言,絕對第一時間回復!

歡迎各位關注我的公眾號,一起探討技術,嚮往技術,追求技術,說好了來了就是盆友喔…

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!