11_條件隨機場

  今天是2020年6月14號星期日。從這篇開始,發布時間就正常了。前邊的文章是在寒假寫好的,後來因為趕進度和改小論文,沒有及時整理。3月到6月,兩個半月的時間做了些什麼?真的好怕浪費了時間~《統計學習方法》這本書(第二版),大概在四月底就看個差不多了,半生不熟的好歹是通篇過了一遍,當然不止一遍,除了潛在狄利克雷分配。期間夾雜着手撕代碼的過程,因為腦容量有限的原因,寫代碼的時候,又要把書面內容重新過一遍。就連發布,也要重新過一遍,把重要的地方加上不同顏色…於是這個假期,《統計學習方法》這本書的每個章節,不知道反反覆復的過了多少遍,肯定不超過十遍…也就是看的多,不代表學的多…但是看了總歸是看了,也比不看強…

  CRF條件隨機場~本節說難,挺難;說簡單,也簡單。為什麼我覺得CRF和LR很像呢?可能有很多人在應用場景上反駁我,LR做數據分類問題,CRF是針對序列標註問題。我覺得像,提供一下幾個觀點(遞進着看):

  1.LR和CRF都是判別模型。這句話要從計算方式上來理解:LR是通過線性函數,對特徵向量擬合不同的權值,最後加和的線性函數值作為sigmoid函數的輸入,轉換為概率值(對數幾率)。CRF在公式(11.10)中,不,在更明顯的公式(11.15)中,wkfk(y,x)1->K加和。個人膚淺的理解:公式(11.15)就是對各個特徵擬合不同的權值wk,加和的線性函數值作為某個“求概率公式”的輸入。

  2.從觀點1出發,思考一下“特徵”。在LR中,“特徵”很明顯,就是特徵向量的各個分量,相當於一個分量一個特徵。LR擬合的w,其實就是看看每個特徵分量對分類結果占什麼樣的貢獻。在CRF中,“特徵”是定義在最大團上的,拋去“最大團”“勢函數”“特徵函數”這些抽象概念,CRF的“特徵”其實不就是定義在相互聯繫的兩個節點上嘛?相當於“成對”構成一個特徵。轉移特徵也好,狀態特徵也罷,線性鏈條件隨機場不就限定了“特徵”要產生在“成對”的關聯關係上。然後,公式(11.15)告訴我,[權值×特徵==》加和],CRF和LR計算線性函數值的方式沒區別啊(區別在特徵的定義上)。

  3.從觀點1出發,思考一下“概率的計算”。不知道大家有沒有仔細思考LR中,第六章公式(6.6)分母位置的1是怎麼計算的,或者為什麼要在分母添加這一項。是不是可以理解為exp(0*wx)=1,這樣能夠把分母作為歸一化項Z(x)看待。分母的1+exp(wx)看作Z(x)=exp(0*wx)+exp(1*wx)P(Y=1|x)=exp(1*wx)/Z(x),另一個類別Y=0的概率P(Y=0|x)=exp(0*wx)/Z(x),怎麼樣,香不香?這裏不就是兩個項的線性函數值進行了指數歸一化。在這個角度上,看CRF的概率計算方式,公式(11.15)和公式(11.16)不就是多了幾項嘛…多個項(K項)進行了歸一化,把數值轉換成了概率值,這這這,和softmax()指數歸一化不就是一回事嘛…然後再考慮考慮為什麼CRF中勢函數是嚴格正的…

  暫時先這三點,實際上都是圍繞[判別模型]提出的。

GitHub:https://github.com/wangzycloud/statistical-learning-method

條件隨機場

引入

  書中對條件隨機場CRF的描述是:給定一組輸入隨機變量條件下另一組輸出隨機變量的條件概率分佈模型,其特點是假設輸出隨機變量構成馬爾可夫隨機場。先拋去“輸出隨機變量構成馬爾可夫隨機場”這個限定條件不看,該模型是條件概率模型,並且是一組隨機變量,在另外一組隨機變量條件下的條件概率模型。

  組這個概念,限定了模型中發生作用的因素要產生在多個隨機變量上。類似這樣,形象不(僅代表個人理解哈~不能保證正確):

  像示意圖中表示的,如果一組隨機變量之間,關係是散亂無序的,我們該怎樣求解呢?各個組上隨機變量的關係都發現不了,何談尋找A組在B組條件下的條件概率分佈?因此,考慮對輸出組的隨機變量施加限定條件。人類最常做的就是發現規律,總結經驗。比如說,讓輸出組的隨機變量兩兩之間產生聯繫,會不會簡化問題?

  於是,模型變成了這樣。又或者,讓輸入組隨機變量和輸出組隨機變量,都變成序列關係。這時,問題變成了由輸入序列對輸出序列預測的判別模型。具體的,讓輸出組隨機變量構成馬爾可夫隨機場。需要的知識包括圖與無向圖、馬爾可夫性、無向圖模型的因子分解、團與最大團…

概率無向圖

  概率無向圖模型,又稱為馬爾可夫隨機場,是一個可以由無向圖表示的聯合概率分佈。“馬爾可夫隨機場”正式出場了,簡單講它是一個聯合概率分佈,是由無向圖表示的。想象無向圖的樣子,由結點和邊構成,且邊沒有方向(無箭頭)。深入一點,一個結點表示一個隨機變量,如果兩個結點之間有邊關聯,就認為這兩個結點具有關聯關係,並且是“相互關聯”(無向邊),相互產生影響。

  可以看到,“有向變無向”這個操作,讓限定條件變的更寬泛了,也就是讓CRF的應用場景增多。當然,無向圖不只是上圖的線性序列關係。根據馬爾可夫性的不同,概率無向圖模型也有很多種類。接下來由圖開始,具體介紹一下模型的定義。

  看一下馬爾可夫性,我的理解是這樣:在無向圖G上給定Cs(與集合C有關的一堆結點),AsBs條件獨立。AsBs之間沒有邊關聯,就算利用Cs作為結點把AsBs連接了起來,兩者仍然保持獨立關係。

  有三種類型,分別是成對馬爾可夫性、局部馬爾可夫性、全局馬爾可夫性,具體定義較晦澀。如下:

  由以上馬爾可夫性,概率無向圖模型定義為:

  有了概率無向圖的定義,實際上我們更關心的是如何求解各個隨機變量的聯合概率分佈。那麼,對給定的概率無向圖模型,我們能不能找到將整體的聯合概率寫成若干個子聯合概率的乘積的方法?也就是將聯合概率進行因子分解(類似:6=2×3,整數6可以分解成因子2乘以因子3),就能方便模型的學習與計算了。事實上,概率無向圖模型最大的特點就是易於因子分解。這裏留幾個疑問,“整體的”變成若干個“子聯合概率的”這句話,子聯合概率我不是可以理解成“子集”的,這個子集要如何劃分?我們知道,圖結構動不動就會像漁網一樣,橫七豎八的全是連接邊。

  另外,聯合概率的因子分解,不僅要能解決無向圖上劃分小子集的問題,還要保證各個小子集乘積的聯合概率要等於整體的聯合概率。

  這裏劃分小子集的問題,由無向圖上的“最大團”解決的,定義如下:

  例子中描述的,應該是挺清楚的,最大團中的任何兩個結點均有邊連接。於是,概率無向圖模型的聯合概率分佈可以表示為其最大團上的隨機變量的函數的乘積,這種操作稱為概率無向圖模型的因子分解。接下來,考慮聯合概率如何計算。

  不管整體的聯合概率也好,子集的聯合概率也好,無非是表示一大撮和一小撮隨機變量的區別。問題在於,這個東西它是概率啊。怎麼在圖結構上體現概率啊…公式(11.15)和公式(11.16)給出了答案,對每個Yi(N個狀態序列中的一個序列Yi)求分值,將各個“得分”歸一化后的結果作為該無向圖Yi的聯合概率。實際上也就是每一個無向圖Yi是所有狀態序列中的一種情況,這個Yi的得分佔所有情況的比例,就認為是Yi的聯合概率。

  這裏Yi的得分,是如何反映的呢?可以從公式(11.15)看出,圖中所有最大團上各個最大團勢函數的乘積,作為Yi的得分。

  串到CRF特徵抓取的過程中,我認為這裏的勢函數,就是抓取最大團集合中的結點特徵,並且是各個結點之間的關聯特徵。由於訓練過程中擬合參數wk,所以這裏只需要將每個特徵作為正的“量”就可以了。勢函數僅作標示“特徵”的度量(metric,擬合的wk決定該特徵對“得分”影響的正負、大小程度(對應開頭的觀點3)。

  現在利用的這種方式,不是空穴來風,是有定理來保證的:

條件隨機場的定義與形式

  條件隨機場是給定隨機變量X條件下,隨機變量Y的馬爾可夫隨機場。我們要用到的CRF指的是定義在線性鏈上的特殊的條件隨機場,稱為線性鏈條件隨機場。一般情況下,在條件概率模型P(Y|X)中,Y是輸出變量,表示標記序列;X是輸入變量,表示觀測序列。這裏,我們也把標記序列稱為狀態序列。在學習時,利用訓練數據通過極大似然估計或正則化的極大似然估計得到條件概率模型;預測時,對於給定的輸入序列x,求出條件概率模型最大的輸出序列y。

  我認為在給定輸入序列X的條件下,計算輸出序列Y的條件概率(且序列Y構成線性鏈條件隨機場)的場景是與圖11.4相符合的。文中描述的,現實中一般假設X和Y有相同的圖結構,形如圖11.5這樣,這就好像對模型進行了簡化,讓模型更容易實現。換一個角度看,圖11.4中的X表達的是輸入序列整體對輸出序列Y的影響,具體到如何產生影響,如圖11.5表達的這樣,輸入序列X的每個分量Xi,與相同時刻的Yi相互影響。

  這裏留兩個疑問,(Xi,Yi)的相互影響關係,會不會傳遞到t時刻?與時刻t有沒有關係?

  看一下線性鏈條件隨機場的具體定義:

  圖示如下,公式(11.9)實際上表達了:馬爾可夫性規定Yi(Y3)只與相鄰的結點有關聯關係(藍框代表等式左側,紅框代表等式右側)

  根據定理11.1,可以給出線性鏈條件隨機場P(Y|X)的因子分解式,各因子是定義在相鄰兩個結點(最大團)上的勢函數。兩個疑問的解答,相信後面部分的描述中可以找到答案。接下來看一下條件隨機場的三種形式。

1)條件隨機場的參數化形式

  該基本形式,表示給定輸入序列x,對輸出序列y預測的條件概率。實際上現在的具體定義,就是把最大團、勢函數、概率求解等概念落實下來,變成可以量化的東西。公式(11.11)為歸一化項,相當於把Y的每種情況(各種Y序列)考慮到,作為求概率時的分母(每種情況下,各個得分加和)。在某個情況Y下,計算該Y的概率,需要先得到所有勢函數構成的特徵和,也就是“得分分值”。

  這裏的特徵來自兩部分,yi-1yi橫向上的轉移特徵tk(·)yix豎向上的狀態特徵sl(·)

  看一下具體定義:

  這裏tksl的取值,類似於最大熵模型中特徵函數的定義。我的理解,取1或者0,是標記這個特徵有還是沒有。至於此時該特徵的貢獻是大是小,是正是負,這取決於模型訓練時擬合的參數情況(λ、μ)。

  舉個例子,看下計算時的具體過程:

  該標記序列y=(y1,y2,y3)的非規範化概率,實際上就是通過存在的特徵×對應權重,然後加和。符合[權值×特徵==》加和]這個方式。

2)條件隨機場的簡化形式

  注意到條件隨機場公式(11.10)中同一個特徵在各個位置都有定義,可以對同一個特徵在各個位置求和,從而將局部特徵轉化為一個全局特徵函數。這樣就可以將條件隨機場寫成權值向量和特徵向量的內積形式,也就是現在要描述的簡化形式。實際上,這更進一步貼近了[權值×特徵==》加和]這種方式。

  如上圖所示,假設每個位置i都有這個局部特徵(沒有該特徵的話,特徵值為0),每個i都要針對這一個局部特徵求一個參數,這個工作量似乎有點大,並且重複。那麼,是不是可以把該局部變量上升到全局特徵,每個局部位置特徵值加和,讓這一個特徵在全局上學習一個權重參數。一來減少沒必要的參數估計,二來可以把重點放在“特徵”的增加和估計上。

  這裏公式(11.13)表明了全局特徵是通過各個位置i的局部特徵加和得到的。

  接下來,用向量形式進行表示:

  這樣就得到了條件隨機場的簡化形式。經過知識點細化,再抽象,看CRF公式(11.19)和公式(11.20)和LR中的公式(6.5)和公式(6.6)多像。各個特徵的線性函數值(得分),通過指數歸一化轉化成概率,學習的過程擬合各個參數。

3)條件隨機場的矩陣形式

  先看定義:

  這裏先記一個要點,對每個標記序列引進特殊的起點和終點狀態標記,這時標註序列的概率Pw(y|x)可以通過矩陣形式表示並有效計算。划重點:引進特殊起點和重點標記之後,才可以通過矩陣形式計算。

  不知道怎麼分析這個地方,現在從以下幾個問題開始:

  ①公式(11.21)中Mi的下標iyi的下標i是否表示同一個意思?

  我認為這兩個下標i指的不是同一個意思,yii表示該矩陣中狀態的取值;Mi中的i表示第i個矩陣,實際上每個位置都有對應的矩陣。同時矩陣的維度是M×M維的,因為yi可以取到M個狀態(yiM個狀態,yi-1的某個狀態可以轉移到M个中任意一個,yi-1M個狀態,所以轉移有M×M)。例M2(1,3)表示在狀態序列的第2個位置上(t=2),由t=1時刻的“狀態1”轉移到t=2的“狀態3”的非規範化概率。

  Mi中的元素是怎麼計算的,是什麼?

  計算方式見公式(11.22)和公式(11.23)。我先想到的是,Mi矩陣為yi-1yi的轉移矩陣,每個時刻yi都是M種狀態,轉移也都是yi-1yi這些轉移方式,計算方式都由公式(11.23)計算。那麼,不同的Mi之間的區別是什麼呢。我的理解是針對不同的時刻t、不同的x,特徵函數是否存在和相應的權值大小,決定了Mi不同。

  ③公式(11.24)的分子部分n+1個矩陣的適當元素的乘積,是什麼?

  仔細看一下CRF的簡化形式中公式(11.15)的分子部分,利用公式(11.13)對i進行展開,有:

  是不是就變成了公式(11.24)的分子部分n+1個矩陣的適當元素乘積的形式。也就是說,CRF的矩陣形式來源於簡化形式,至於為什麼會有這種方式,我覺得是便於計算吧,下邊前後向算法會用到這種形式。

  ④公式(11.25)規範化因子,是什麼?

  看上去是對所有序列的非規範化概率的總和。其實追根究底④這個問題是想知道矩陣運算在這裏計算的是什麼。

  綜上,條件隨機場矩陣形式的要點有:

  以2×2Mi矩陣為例(例11.2),具體表示如下:

  看一下例11.2:

  以狀態序列(1,2,1)為例:

  解析如下:

  接下來看一下求規範化因子的過程:

  上面提到的問題④,n+1個矩陣連乘后,得到的結果仍然是M×M維的。但第1行第1列的元素,正好是所有路徑上的非規範化概率之和。

  了解完概率無向圖、條件隨機場的定義和各種表示方法之後,與隱馬爾可夫模型類似的,接下來介紹條件隨機場的3個基本問題:概率計算問題、學習問題、預測問題。

條件隨機場的概率計算問題

  與隱馬爾可夫模型類似,引進前向-後向向量,遞歸的計算概率(遞歸的計算過程是非常不同的)。

  先看前向計算過程。注意CRF作為無向圖模型,拋去了HMM的方向性,我們要從矩陣乘法的角度進行分析。仔細看一下公式(11.28),轉換成矩陣語言如下(m=5為例,這裏將時刻的下標標記為t,用來區分yi的狀態和時刻tαi->αt,Mi->Mt):

  具體到轉換過程中(考慮矩陣乘法的過程):

  可以看到,α1t的得出,是結合了t-1時刻各個αi的結果。再來理解一下“αt(yi|x)表示在位置t的標記是yi並且從1t的前部分標記序列的非規範化概率”這句話,見下圖:

  實際上,t時刻α向量中的某一個分量,αi可以視作終點狀態取yi時的非規範化概率,並且這個概率是1->t時刻的整個過程中,所有可能序列的非規範化概率之和(從startstop所有路徑上的非規範化概率之和)。如圖中,α1t也就是從start=y1stop=y1過程中,所有可能序列的非規範化概率之和。因此,每個αistart=y1stop=yi的規範化因子Zi這樣就能看出與HMM特別不同了,一個應用矩陣乘法,一個應用條件概率公式。

  我理解的大概就是這個樣子,不知道能不能寫清楚,接下來看下後向算法的計算過程。從公式(11.31)開始:

  具體到轉換過程中(考慮矩陣乘法的過程):

  於是,βt(yi|x)可以表示從t時刻yt:start=y1yn+1:stop=y1,所有路徑上的非規範化概率之和(共T-t個結點狀態的序列)。前向算法也好,後向算法也好,這裏的箭頭指向僅表示乘的方向,不是有向圖結構。

  與HMM類似的,接下來看幾個概率計算。

  公式(11.32)和公式(11.33)還是好理解的,看一下示意圖:

  通過示意圖,先來看一下Z(X)Z(X)既可以通過前向向量,又可以通過後向向量來求。實際上,不管前向向量αn也好,後向向量β1也好,Z(X)的計算矩陣過程,實際上是把m個值加和,也就是得到所有狀態序列的規範化因子。

  分開看公式(11.32)分子部分,我覺得是兩個值(α、β)進行了相乘。第一個α值,代表了從0時刻start=y1t時刻stop=yi的非規範化概率;第二個β值,代表了從t時刻start=yin+1時刻stop=y1的非規範化概率。公式(11.33)是類似分析方法。

  再來看一下幾個期望的計算,就不具體分析了:

 

 

條件隨機場的學習問題

  學習問題實際上討論的是在給定訓練數據集上估計模型參數的問題。條件隨機場模型實際上定義在時序數據上的對數線性模型(是不是與LR像),學習方法包括極大似然估計等,具體的有改進的迭代尺度法IIS、梯度下降法及擬牛頓法。

  具體的方法目前先不弄了(有大概了解,但了解的程度,不足以寫出來),趕趕刷題的進度去了…等找着工作了,準備畢業的時候再把這些方法整理上~

  話說這章的代碼沒寫,因為不會…不知道從什麼地方下手。

條件隨機場的預測問題

  同HMM,大名鼎鼎的維特比算法。

PASS

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

Java 異常處理的十個建議

3{icon} {views}

前言

Java異常處理的十個建議,希望對大家有幫助~

本文已上傳github:

https://github.com/whx123/JavaHome

公眾號:撿田螺的小男孩

一、盡量不要使用e.printStackTrace(),而是使用log打印。

反例:

try{
  // do what you want  
}catch(Exception e){
  e.printStackTrace();
}

正例:

try{
  // do what you want  
}catch(Exception e){
  log.info("你的程序有異常啦,{}",e);
}

理由:

  • printStackTrace()打印出的堆棧日誌跟業務代碼日誌是交錯混合在一起的,通常排查異常日誌不太方便。
  • e.printStackTrace()語句產生的字符串記錄的是堆棧信息,如果信息太長太多,字符串常量池所在的內存塊沒有空間了,即內存滿了,那麼,用戶的請求就卡住啦~

二、catch了異常,但是沒有打印出具體的exception,無法更好定位問題

反例:

try{
  // do what you want  
}catch(Exception e){
  log.info("你的程序有異常啦");
}

正例:

try{
  // do what you want  
}catch(Exception e){
  log.info("你的程序有異常啦,{}",e);
}

理由:

  • 反例中,並沒有把exception出來,到時候排查問題就不好查了啦,到底是SQl寫錯的異常還是IO異常,還是其他呢?所以應該把exception打印到日誌中哦~

三、不要用一個Exception捕捉所有可能的異常

反例:

public void test(){
    try{
        //…拋出 IOException 的代碼調用
        //…拋出 SQLException 的代碼調用
    }catch(Exception e){
        //用基類 Exception 捕捉的所有可能的異常,如果多個層次都這樣捕捉,會丟失原始異常的有效信息哦
        log.info(“Exception in test,exception:{}”, e);
    }
}

正例:

public void test(){
    try{
        //…拋出 IOException 的代碼調用
        //…拋出 SQLException 的代碼調用
    }catch(IOException e){
        //僅僅捕捉 IOException
        log.info(“IOException in test,exception:{}”, e);
    }catch(SQLException e){
        //僅僅捕捉 SQLException
        log.info(“SQLException in test,exception:{}”, e);
    }
}

理由:

  • 用基類 Exception 捕捉的所有可能的異常,如果多個層次都這樣捕捉,會丟失原始異常的有效信息哦

四、記得使用finally關閉流資源或者直接使用try-with-resource

反例:

FileInputStream fdIn = null;
try {
    fdIn = new FileInputStream(new File("/jay.txt"));
    //在這裏關閉流資源?有沒有問題呢?如果發生異常了呢?
    fdIn.close();
} catch (FileNotFoundException e) {
    log.error(e);
} catch (IOException e) {
    log.error(e);
}

正例1:

需要使用finally關閉流資源,如下

FileInputStream fdIn = null;
try {
    fdIn = new FileInputStream(new File("/jay.txt"));
} catch (FileNotFoundException e) {
    log.error(e);
} catch (IOException e) {
    log.error(e);
}finally {
    try {
        if (fdIn != null) {
            fdIn.close();
        }
    } catch (IOException e) {
        log.error(e);
    }
}

正例2:

當然,也可以使用JDK7的新特性try-with-resource來處理,它是Java7提供的一個新功能,它用於自動資源管理。

  • 資源是指在程序用完了之後必須要關閉的對象。
  • try-with-resources保證了每個聲明了的資源在語句結束的時候會被關閉
  • 什麼樣的對象才能當做資源使用呢?只要實現了java.lang.AutoCloseable接口或者java.io.Closeable接口的對象,都OK。
try (FileInputStream inputStream = new FileInputStream(new File("jay.txt")) {
    // use resources   
} catch (FileNotFoundException e) {
    log.error(e);
} catch (IOException e) {
    log.error(e);
}

理由:

  • 如果不使用finally或者try-with-resource,當程序發生異常,IO資源流沒關閉,那麼這個IO資源就會被他一直佔著,這樣別人就沒有辦法用了,這就造成資源浪費。

五、捕獲異常與拋出異常必須是完全匹配,或者捕獲異常是拋異常的父類

反例:

//BizException 是 Exception 的子類
public class BizException extends Exception {}
//拋出父類Exception
public static void test() throws Exception {}

try {
    test(); //編譯錯誤
} catch (BizException e) { //捕獲異常子類是沒法匹配的哦
    log.error(e);
}

正例:

//拋齣子類Exception
public static void test() throws BizException {}

try {
    test();
} catch (Exception e) {
    log.error(e);
}

六、捕獲到的異常,不能忽略它,至少打點日誌吧

反例:

public static void testIgnoreException() throws Exception {
    try {       
        // 搞事情
    } catch (Exception e) {     //一般不會有這個異常
        
    }
}

正例:

public static void testIgnoreException() {
    try {
        // 搞事情
    } catch (Exception e) {     //一般不會有這個異常
        log.error("這個異常不應該在這裏出現的,{}",e); 
    }
}

理由:

  • 雖然一個正常情況都不會發生的異常,但是如果你捕獲到它,就不要忽略呀,至少打個日誌吧~

七、注意異常對你的代碼層次結構的侵染(早發現早處理)

反例:

public UserInfo queryUserInfoByUserId(Long userid) throw SQLException {
    //根據用戶Id查詢數據庫
}

正例:

public UserInfo queryUserInfoByUserId(Long userid) {
    try{
        //根據用戶Id查詢數據庫
    }catch(SQLException e){
        log.error("查詢數據庫異常啦,{}",e);
    }finally{
        //關閉連接,清理資源
    }
}

理由:

  • 我們的項目,一般都會把代碼分 Action、Service、Dao 等不同的層次結構,如果你是DAO層處理的異常,儘早處理吧,如果往上 throw SQLException,上層代碼就還是要try catch處理啦,這就污染了你的代碼~

八、自定義封裝異常,不要丟棄原始異常的信息Throwable cause

我們常常會想要在捕獲一個異常后拋出另一個異常,並且希望把原始異常的信息保存下來,這被稱為異常鏈。公司的框架提供統一異常處理就用到異常鏈,我們自定義封裝異常,不要丟棄原始異常的信息,否則排查問題就頭疼啦

反例:

public class TestChainException {
    public void readFile() throws MyException{
        try {
            InputStream is = new FileInputStream("jay.txt");
            Scanner in = new Scanner(is);
            while (in.hasNext()) {
                System.out.println(in.next());
            }
        } catch (FileNotFoundException e) {
            //e 保存異常信息
            throw new MyException("文件在哪裡呢");
        }
    }
    public void invokeReadFile() throws MyException{
        try {
            readFile();
        } catch (MyException e) {
            //e 保存異常信息
            throw new MyException("文件找不到");
        }
    }
    public static void main(String[] args) {
        TestChainException t = new TestChainException();
        try {
            t.invokeReadFile();
        } catch (MyException e) {
            e.printStackTrace();
        }
    }
}
//MyException 構造器
public MyException(String message) {
        super(message);
    }

運行結果如下,沒有了Throwable cause,不好排查是什麼異常了啦

正例:


public class TestChainException {
    public void readFile() throws MyException{
        try {
            InputStream is = new FileInputStream("jay.txt");
            Scanner in = new Scanner(is);
            while (in.hasNext()) {
                System.out.println(in.next());
            }
        } catch (FileNotFoundException e) {
            //e 保存異常信息
            throw new MyException("文件在哪裡呢", e);
        }
    }
    public void invokeReadFile() throws MyException{
        try {
            readFile();
        } catch (MyException e) {
            //e 保存異常信息
            throw new MyException("文件找不到", e);
        }
    }
    public static void main(String[] args) {
        TestChainException t = new TestChainException();
        try {
            t.invokeReadFile();
        } catch (MyException e) {
            e.printStackTrace();
        }
    }
}
//MyException 構造器
public MyException(String message, Throwable cause) {
        super(message, cause);
    }

九、運行時異常RuntimeException ,不應該通過catch 的方式來處理,而是先預檢查,比如:NullPointerException處理

反例:

try {
  obj.method() 
} catch (NullPointerException e) {
...
}

正例:

if (obj != null){
   ...
}

十、注意異常匹配的順序,優先捕獲具體的異常

注意異常的匹配順序,因為只有第一個匹配到異常的catch塊才會被執行。如果你希望看到,是NumberFormatException異常,就拋出NumberFormatException,如果是IllegalArgumentException就拋出IllegalArgumentException。

反例:

try {
    doSomething("test exception");
} catch (IllegalArgumentException e) {       
    log.error(e);
} catch (NumberFormatException e) {
    log.error(e);
}

正例:

try {
    doSomething("test exception");
} catch (NumberFormatException e) {       
    log.error(e);
} catch (IllegalArgumentException e) {
    log.error(e);
}

理由:

  • 因為NumberFormatException是IllegalArgumentException 的子類,反例中,不管是哪個異常,都會匹配到IllegalArgumentException,就不會再往下執行啦,因此不知道是否是NumberFormatException。所以需要優先捕獲具體的異常,把NumberFormatException放前面~

公眾號

  • 歡迎關注我個人公眾號,交個朋友,一起學習哈~
  • 如果答案整理有錯,歡迎指出哈,感激不盡~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

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

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

Spring源碼系列(一)–詳細介紹bean組件

2{icon} {views}

簡介

spring-bean 組件是 Spring IoC 的核心,我們可以使用它的 beanFactory 來獲取所需的對象,對象的實例化、屬性裝配和初始化等都可以交給 spring 來管理。

針對 spring-bean 組件,我計劃分成兩篇博客來講解。本文會詳細介紹這個組件,包括以下內容。下一篇再具體分析它的源碼。

  1. spring-bean 組件的相關概念:實例化、屬性裝配、初始化、bean、beanDefinition、beanFactory。
  2. bean 組件的使用:註冊bean、獲取bean、屬性裝配、處理器等。

項目環境說明

正文開始前,先介紹下示例代碼使用的環境等。

工程環境

JDK:1.8.0_231

maven:3.6.1

IDE:Spring Tool Suites4 for Eclipse 4.12

Spring:5.2.6.RELEASE

依賴引入

除了引入 spring,這裏還額外引入了日誌和單元測試。

    <properties>
        <spring.version>5.2.6.RELEASE</spring.version>
    </properties>
    
    <dependencies>
        <!-- spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>
		<!-- junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- logback -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.28</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.3</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
            <type>jar</type>
        </dependency>
    </dependencies>

幾個重要概念

實例化、屬性裝配和初始化

在 spring-bean 組件的設計中,這三個詞完整、有序地描述了生成一個新對象的整個流程,是非常重要的理論基礎。它們的具體含義如下:

  1. 實例化:創建出一個新對象。
  2. 屬性裝配:給對象的成員屬性賦值。
  3. 初始化:調用對象的初始化方法。

下面使用一段代碼來簡單演示下這個流程。

public class UserService implements IUserService {
    
    private UserDao userDao;
    
    
    public UserService() {
        super();
        System.err.println("UserService構造方法被調用");
        System.err.println("        ||");
        System.err.println("        \\/");
    }
    
    public void init() {
        System.err.println("UserService的init方法被調用");
        System.err.println("        ||");
        System.err.println("        \\/");
    }
    
    
    public UserDao getUserDao() {
        return userDao;
    }
    
    public void setUserDao(UserDao userDao) {
        System.err.println("UserService的屬性裝配中");
        System.err.println("        ||");
        System.err.println("        \\/");
        this.userDao = userDao;
    }
    
}

如果我們將這個 bean 交給 spring 管理,獲取 bean 時會在控制台打印以下內容:

什麼是bean

按照官方的說法, bean 是一個由 Spring IoC 容器實例化、組裝和管理的對象。我認為,這種表述是錯誤的,通過registerSingleton方式註冊的 bean,它就不是由 Spring IoC 容器實例化、組裝,所以,更準確的表述應該是這樣:

某個類的對象、FactoryBean 對象、描述對象或 FactoryBean 描述對象,被註冊到了 Spring IoC 容器,這時通過 Spring IoC 容器獲取的這個類的對象就是 bean。

舉個例子,使用了 Spring 的項目中, Controller 對象、Service 對象、DAO 對象等都屬於 bean。

至於什麼是 IoC 容器,在 spring-bean 組件中,我認為,beanFactory 就屬於 IoC 容器。

什麼是beanFactory

從客戶端來看,一個完整的 beanFactory 工廠包含以下基本功能:

  1. 註冊別名。對應下圖的AliasRegistry接口。
  2. 註冊單例對象。對應下圖的SingletonBeanRegistry接口。
  3. 註冊BeanDefinition對象。對應下圖的BeanDefinitionRegistry接口。
  4. 獲取 bean。對應下圖的BeanFactory接口。

在 spring-bean 組件中,DefaultListableBeanFactory就是一個完整的 beanFactory 工廠,也可以說是一個 IoC 容器。接下來的例子將直接使用它來作為 beanFactory。

至於其他的接口,這裏也補充說明下。HierarchicalBeanFactory用於提供父子工廠的支持,ConfigurableBeanFactory用於提供配置 beanFactory 的支持,ListableBeanFactory用於提供批量獲取 bean 的支持(不包含父工廠的 bean),AutowireCapableBeanFactory用於提供實例化、屬性裝配、初始化等一系列管理 bean 生命周期的支持。

什麼是beanDefinition

beanDefinaition 是一個描述對象,用來描述 bean 的實例化、初始化等信息。

在 spring-bean 組件中,beanDefinaition主要包含以下四種:

  1. RootBeanDefinition:beanFactory 中最終用於 createBean 的 beanDefinaition,不允許添加 parentName。在 BeanFactory 中以下三種實現類都會被包裝成RootBeanDefinition用於 createBean。
  2. ChildBeanDefinition必須設置 parentName 的 beanDefinaition。當某個 Bean 的描述對象和另外一個的差不多時,我們可以直接定義一個ChildBeanDefinition,並設置它的 parentName 為另外一個的 beanName,這樣就不用重新設置一份。
  3. GenericBeanDefinition:通用的 beanDefinaition,可以設置 parentName,也可以不用設置
  4. AnnotatedGenericBeanDefinition:在GenericBeanDefinition基礎上增加暴露註解數據的方法。

spring-bean 組件提供了BeanDefinitionBuilder用於創建 beanDefinaition,下面的例子會頻繁使用到。

使用例子

入門–簡單地註冊和獲取bean

下面通過一個入門例子來介紹註冊和獲取 bean 的過程。

    @Test
    public void testBase() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        
        // 創建BeanDefinition對象
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        
        // 註冊Bean
        beanFactory.registerBeanDefinition("userService", rootBeanDefinition);
        
        // 獲取Bean
        IUserService userService = (IUserService)beanFactory.getBean("userService");
        System.err.println(userService.get("userId"));
    }

兩種註冊bean的方式

beanFactory 除了支持註冊 beanDefinition,還允許直接註冊 bean 實例,如下。和前者相比,後者的實例化、屬性裝配和初始化都沒有交給 spring 管理。

    @Test
    public void testRegisterWays() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        
        // 註冊Bean-- BeanDefinition方式
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        beanFactory.registerBeanDefinition("userService", rootBeanDefinition);
        
        // 註冊Bean-- Bean實例方式
        beanFactory.registerSingleton("userService2", new UserService());
        
        // 獲取Bean
        IUserService userService = (IUserService)beanFactory.getBean("userService");
        System.err.println(userService.get("userId"));
        IUserService userService2 = (IUserService)beanFactory.getBean("userService2");
        System.err.println(userService2.get("userId"));
    }

當然,這種方式僅支持單例 bean 的註冊,多例的就沒辦法了。

註冊多例bean

默認情況下,我們從 beanFactory 獲取到的 bean 都是單例的,即每次 getBean 獲取到的都是同一個對象,實際項目中,有時我們需要獲取到多例的 bean,這個時候就可以通過設置 beanDefinition 的 scope 來處理。如下:

    @Test
    public void testScope() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        
        // 註冊Bean-- BeanDefinition方式
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        rootBeanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
        beanFactory.registerBeanDefinition("userService", rootBeanDefinition);
        
        // 獲取Bean--通過BeanType
        IUserService userService1 = beanFactory.getBean(IUserService.class);
        IUserService userService2 = beanFactory.getBean(IUserService.class);
        assertNotEquals(userService1, userService2);
    }

多種獲取bean的方式

beanFactory 提供了多種方式來獲取 bean 實例,如下。如果同時使用 beanName 和 beanType,獲取到指定 beanName 的 bean 後會進行類型檢查和類型類型,如果都不通過,將會報錯。

    @Test
    public void testGetBeanWays() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        
        // 創建BeanDefinition對象
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        
        // 註冊Bean
        beanFactory.registerBeanDefinition("userService", rootBeanDefinition);
        
        // 獲取Bean--通過BeanName
        IUserService userService = (IUserService)beanFactory.getBean("userService");
        System.err.println(userService.get("userId"));
        // 獲取Bean--通過BeanType
        IUserService userService2 = beanFactory.getBean(IUserService.class);
        System.err.println(userService2.get("userId"));
        // 獲取Bean--通過BeanName+BeanType的方式
        IUserService userService3 = beanFactory.getBean("userService", IUserService.class);
        System.err.println(userService3.get("userId"));
    }

不同形式的beanName

通過 name 獲取 bean,這個 name 包含以下三種形式:

  1. beanName,即註冊 bean 時用的 beanName。這是使用最多的形式,需要注意一點,如果 beanName 對應的 bean 是FactoryBean,並不會返回FactoryBean的實例,而是會返回FactoryBean.getObject方法的返回結果。
  2. alias,即我們通過SimpleAliasRegistry.registerAlias(name, alias)方法註冊到 beanFactory 的別名。這時,需要將 name 解析為 alias 對應的 beanName 來獲取 bean。
  3. ‘&’ + factorybeanName,這時為了獲取FactoryBean的一種特殊格式。
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

        // 註冊Bean--註冊的是一個 FactoryBean
        UserServiceFactoryBean userServiceFactoryBean = new UserServiceFactoryBean();
        beanFactory.registerSingleton("userServiceFactoryBean", userServiceFactoryBean);

        // 註冊BeanName的別名
        beanFactory.registerAlias("userServiceFactoryBean", "userServiceAlias01");

        // 通過BeanName獲取
        assertEquals(userServiceFactoryBean.getObject(), beanFactory.getBean("userServiceFactoryBean"));

        // 通過別名獲取
        assertEquals(userServiceFactoryBean.getObject(), beanFactory.getBean("userServiceAlias01"));

        // 通過&+FactoryBeanName的方式
        assertEquals(userServiceFactoryBean, beanFactory.getBean("&userServiceFactoryBean"));

bean衝突的處理

通過 beanType 的方式獲取 bean,如果存在多個同類型的 bean且無法確定最優先的那一個,就會報錯。

    @Test
    public void testPrimary() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); 
        
        // 創建BeanDefinition對象
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
        
        // 註冊Bean
        beanFactory.registerBeanDefinition("UserRegisterBeanDefinition", rootBeanDefinition);
        beanFactory.registerSingleton("UserRegisterSingleton", new User("zzs002", 19));
        beanFactory.registerSingleton("UserRegisterSingleton2", new User("zzs002", 18));
        
        // 獲取Bean--通過BeanType
        User user = beanFactory.getBean(User.class);
        System.err.println(user);
    }

運行以上方法,將出現 NoUniqueBeanDefinitionException 的異常。

針對上面的這種問題,spring 的處理方法如下:

  1. 檢查是否存在唯一一個通過registerBeanDefinitionisPrimary = true的(存在多個會報錯),存在的話將它作為匹配到的唯一 beanName;
  2. 通過我們註冊的OrderComparator來確定優先值最小的作為唯一 beanName。注意,通過registerSingleton註冊的和通過registerBeanDefinition註冊的,比較的對象是不一樣的,前者比較的對象是 bean 實例,後者比較的對象是 bean 類型,另外,這種方法最好不要存在相同優先級的 bean。

所以,為了解決這種衝突,可以設置BeanDefinition對象的 isPrimary = true,或者為 beanFactory 設置OrderComparator,代碼如下:

    @Test
    public void testPrimary() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

        // 為BeanFactory設置比較器
        beanFactory.setDependencyComparator(new OrderComparator() {

            @Override
            public Integer getPriority(Object obj) {
                return obj.hashCode();
            }
        });

        // 創建BeanDefinition對象
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
        // rootBeanDefinition.setPrimary(true); // 設置BeanDefinition對象為isPrimary

        // 註冊Bean
        beanFactory.registerBeanDefinition("userRegisterBeanDefinition", rootBeanDefinition);
        beanFactory.registerSingleton("userRegisterSingleton", new User("zzs002", 19));
        beanFactory.registerSingleton("userRegisterSingleton2", new User("zzs003", 18));

        // 獲取Bean--通過BeanType
        User user = beanFactory.getBean(User.class);
        System.err.println(user);
    }

使用TypeConverter獲取自定義類型的對象

當我們使用 beanType 來獲取 bean 時,如果獲取到的 bean 不是指定的類型,這時,不會立即報錯,beanFactory 會嘗試使用我們註冊的TypeConverter來強制轉換。而這個類型轉換器我們可以自定義設置,如下。

    @Test
    public void testTypeConverter() {
        
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        // 註冊類型轉換器
        beanFactory.setTypeConverter(new TypeConverterSupport() {
            @SuppressWarnings("unchecked")
            @Override
            public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
                    @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {
                // 將User轉換為UserVO
                if(UserVO.class.equals(requiredType) && User.class.isInstance(value)) {
                    User user = (User)value;
                    return (T)new UserVO(user);
                }
                return null;
            }
        });

        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
        beanFactory.registerBeanDefinition("User", rootBeanDefinition);

        UserVO bean = beanFactory.getBean("User", UserVO.class);
        Assert.assertTrue(UserVO.class.isInstance(bean));
    }

屬性裝配

beanFactory 在進行屬性裝配時,會讀取 beanDefinition 對象中的PropertyValues中的propertyName=propertyValue,所以,我們想要對 bean 注入什麼參數,只要在定義 beanDefinition 時指定就行。

    @Test
    public void testPopulate() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

        // 定義userService的beanDefinition
        AbstractBeanDefinition userServiceBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        // 定義userDao的beanDefinition
        AbstractBeanDefinition userDaoBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserDao.class).getBeanDefinition();
        // 給userService設置裝配屬性
        userServiceBeanDefinition.getPropertyValues().add("userDao", userDaoBeanDefinition);
        // userServiceBeanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        // userServiceBeanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_NAME);

        // 註冊Bean
        beanFactory.registerBeanDefinition("userService", userServiceBeanDefinition);
        beanFactory.registerBeanDefinition("userDao", userDaoBeanDefinition);

        // 獲取Bean
        IUserService userService = (IUserService)beanFactory.getBean("userService");
        userService.save(null);
    }

運行以上方法,發現 userDao 對象被成功注入到了 userService 對象中!

這裏補充一點,beanFactory 除了通過 beanDefinition 中的PropertyValues獲取 propertyName=propertyValue,還可以讀取 bean 中的屬性來自動定義 propertyName=propertyValue,只要設置 beanDefinition 的 autowireMode 就可以了。

bean 實例化、屬性裝配和初始化的處理器

前面講到,我們將 bean 的實例化、屬性裝配和初始化都交給了 spring 處理,然而,有時我們需要在這些節點對 bean 進行自定義的處理,這時就需要用到 beanPostProcessor。

這裏我簡單演示下如何添加處理器,以及處理器的執行時機,至於處理器的具體實現,我就不多擴展了。

    @Test
    public void testPostProcessor() {
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

        // 添加實例化處理器
        beanFactory.addBeanPostProcessor(new InstantiationAwareBeanPostProcessor() {
            // 如果這裏我們返回了對象,則beanFactory會將它作為bean直接返回,不再進行bean的實例化、屬性裝配和初始化等操作
            public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
                if(UserService.class.equals(beanClass)) {
                    System.err.println("bean實例化之前的處理。。 --> ");
                }
                return null;
            }

            // 這裏通過返回的布爾值判斷是否需要繼續對bean進行屬性裝配和初始化等操作
            public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
                if(UserService.class.isInstance(bean)) {
                    System.err.println("bean實例化之後的處理。。 --> ");
                }
                return true;
            }
        });

        // 添加裝配處理器
        beanFactory.addBeanPostProcessor(new InstantiationAwareBeanPostProcessor() {

            // 這裏可以在屬性裝配前對參數列表進行調整
            public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
                if(UserService.class.isInstance(bean)) {
                    System.err.println("屬性裝配前對參數列表進行調整 --> ");
                }
                return InstantiationAwareBeanPostProcessor.super.postProcessProperties(pvs, bean, beanName);
            }

        });

        // 添加初始化處理器
        beanFactory.addBeanPostProcessor(new BeanPostProcessor() {

            // 初始化前對bean進行改造
            public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
                if(UserService.class.isInstance(bean)) {
                    System.err.println("初始化前,對Bean進行改造。。 --> ");
                }
                return bean;
            }

            // 初始化后對bean進行改造
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
                if(UserService.class.isInstance(bean)) {
                    System.err.println("初始化后,對Bean進行改造。。 --> ");
                }
                return bean;
            }
        });
        // 定義userService的beanDefinition
        AbstractBeanDefinition userServiceBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        // 定義userDao的beanDefinition
        AbstractBeanDefinition userDaoBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserDao.class).getBeanDefinition();
        // 給userService添加裝配屬性
        userServiceBeanDefinition.getPropertyValues().add("userDao", userDaoBeanDefinition);
        // 給userService設置初始化方法
        userServiceBeanDefinition.setInitMethodName("init");
        
        // 註冊bean
        beanFactory.registerBeanDefinition("userService", userServiceBeanDefinition);
        beanFactory.registerBeanDefinition("userDao", userDaoBeanDefinition);

        IUserService userService = (IUserService)beanFactory.getBean("userService");
        System.err.println(userService.get("userId"));
    }

運行以上方法,控制台打印出了整個處理流程。實際開發中,我們可以通過設置處理器來改變改造生成的 bean 。

以上,基本介紹完 spring-bean 組件的使用,下篇博客再分析源碼,如果在分析過程中發現有其他特性,也會在這篇博客的基礎上擴展。

相關源碼請移步: spring-beans

本文為原創文章,轉載請附上原文出處鏈接:https://www.cnblogs.com/ZhangZiSheng001/p/13126053.html

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

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

唐獎得主珍古德視訊演說 談疫情與永續發展

2{icon} {views}

摘錄自2020年9月22日中央社報導

「唐獎第四屆大師論壇-永續發展場」22日在清華大學登場,第四屆唐獎得主珍古德透過視訊演說、線上提問方式,探討疫情後生活、自然生態保育與環境永續發展。

珍古德演講談到,人類正經歷著武漢肺炎(2019冠狀病毒疾病,COVID-19)疫情,在某種程度上是因為不尊重自然界,例如:當人類摧毀了森林的一部分時,會迫使動物物種與一般不會接觸到的物種相互接觸,造成病原體、病毒或細菌等擴散,自一個物種跳躍到另一個物種的情況,進而產生新的疾病。

珍古德說,人類消耗動物們的糧食,導致動物被迫進入城市尋找食物,進而產生新型人畜共通傳染病,因此可以說,大多數讓人類飽受折磨的新疾病其實都源自於動物。

珍古德指出,雖然這次疫情同時影響了富人和窮人,但對窮人的影響較大,例如非洲有許多人僅能用每天所販售的東西來購買食物、生活三餐不繼,因受到封城關係,無法進城販售東西維持生計。

珍古德說,現今社會、政治和環境正處於第六次生物大滅絕期間,人類不顧後果的燃燒化石燃料、製造溫室氣體,如同毯子蓋著地球一樣,導致各地產生氣候變遷與破壞,這是應該嚴肅面對的課題,減少未來需要承受的後果。

她表示,目前全球正面對疫情,也同時在面對氣候危機,人們若對自然界不尊敬,就會持續造成威脅。她認為在自然資源有限的地球上,不應無止盡的經濟發展,需要思考新方法,找到新型綠色經濟。

唐獎網站指出:

珍古德博士是國際知名的靈長類動物學家,她最著名的事蹟之一,就是以26年的時間,在非洲坦尚尼亞的岡貝溪國家公園和野生黑猩猩直接接觸,並做了深入的硏究。她在1957年去肯亞拜訪朋友時,認識了著名的英國人類學家里奇博士,里奇博士認為「珍古德女士有耐心、思慮周密、可忍受孤立生活並有學習的熱情,是執行野外動物觀察的最佳人選」。里奇博士協助珍古德女士獲得硏究經費,於1960年在岡貝溪國家公園開始進行野外黑猩猩之田野調查,來硏究黑猩猩與人類之相似性。

珍古德女士於1962年進入劍橋大學當博士生,是學校有史以來少數幾位沒有大學文憑而獲准可直接攻讀博士學位的學生,她在1966年取得動物行為學的博士學位,之後仍持續在岡貝地區進行研究。她觀察並紀錄野外黑猩猩的行為,包括捕食猴子,做小工具釣取蟻窩中的白蟻,以手相互請求、輕拍或擁抱等。這些有關野外黑猩猩個別及群聚行為的開創性發現,為廣泛的進化理論之進一步探討,提供了堅實的基礎,也使珍古德博士當之無愧的成為20世紀最偉大的田野科學家之一。她1960年有關黑猩猩可製造及使用工具的發現,震驚了科學界,並改寫了人類與動物的關係,里奇博士説,「這一發現,破除了只有人類才會利用工具的迷思,甚至因而影響到如何定義人類」,哈佛大學的古爾德博士認為珍古德博士的野外觀察成果,是「20世紀最偉大的學術成就之一」。

1965年,珍古德博士在坦尚尼亞創立了岡貝溪研究中心,並在1967–2003年間擔任該中心的科學組主任。珍古德博士不僅是從事科學論文寫作的學者,她也是一位積極行動者,以無與倫比的幹勁及效率,終身致力於地球環境的保存。在1977年,她創立了國際珍古德協會,推動全球野生動物保育和環境教育。她理解到黑猩猩及其棲地所面臨的威脅,積極推動和社區合作,該協會資助社區所提出的需求,同時致力於類人猿的保育,工作項目包括永續農耕方法之引入,提供更好的衞生設施及教育機會等。目前國際珍古德協會在全球30多個地區設有辦公室,其中一個就在台灣。珍古德博士非常擔心人類會因環境變遷而受傷害,她曾言「當自然受難,我們也受難,當自然茁壯,我們也茁壯」,目前地球環境受人類工業化發展所造成的氣候變遷影響,處於危險之中,珍古德博士認為人類仍有足夠的時間採取行動,她對未來抱持希望的理由是「人類具有的智慧、年輕人的熱情、自然環境的復原力、社群媒體的影響力及人類所具不屈不撓的精神」。

在1980年代晚期,珍古德博士注意到各地森林砍伐的速度加遽和黑猩猩族群數量的下降,她理解到只在局部地方從事保育工作是不足夠的,她必須離開岡貝地區,走向世界,才有機會挽救黑猩猩族群的滅絕。使地球成為更好的地方以供人類和動物居住,珍古德博士身體力行的終身投入,是非常有啓發性的。1991年起,她推動「根與芽」國際環境教育計畫,目前超過65個國家參與,輔導成立1萬多個根與芽小組。根與芽計畫的目標在培養年輕人以正向的態度,關心社區、改善環境及愛護動物。珍古德博士已經86歲,仍維持一年平均三百天到各地拜訪學生,發表演講,陳述黑猩猩及地球環境所面臨的危機,並說明她對人類最終能解決人類自己所製造出的問題有信心的原因。

同時,珍古德博士以她所具備的獨特學識,協助指導美國國家衞生院,關鍵性的改變其生物醫學研究的過程,即終止了對黑猩猩的侵入性研究,並有步驟的在黑猩猩退休時將牠們安置於聯邦的庇護所。這個重大的決定,有其重大的意義並有相當深遠的影響。珍古德博士以她研究黑猩猩數十年的心得提供諮詢,才得以有此關鍵性的改變。

珍古德博士第一篇科學論文發表於1963年,其硏究成果也出現在1965年12月份國家地理雜誌的封面。她出版了16本專書、15本童書,近百篇個人或有共同作者的科學論文,其中一些發表在著名的「科學」及「自然」期刋,她的一些專書,曾獲得國際上最高獎項的肯定並被翻譯為13 種以上的語言。此外,她也製作了許多有關人道對待動物及地球環境保育的影片,透過她無數次的演講、專訪、書籍及影片,她成功的讓世人了解到必須做生態保育的原因及永續發展推動的重要性。2002年,聯合國秘書長安南任命她為聯合國和平使者,這個任命,十足的呈現珍古德博士在國際永續發展推動上所代表的重要地位。

珍古德博士以創新及整體性的方法,將其研究目標連結到動物行為、社區及環境,這開創性工作的成果,讓世人更了解黑猩猩、人類及我們所共同居住的地球環境,並體認到其相互間密不可分的關係。她對此關係的闡明,讓世人重新反省及審視人類對待自然環境所應有的態度,這樣的覺醒與認知,是國際間邁向永續發展推動的極重要基礎之一。

國際新聞
環境正義
唐獎
永續發展
珍古德

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

8萬塊買個7座SUV,這車夠氣派夠面子

4{icon} {views}

相比較7座MpV來說,風光580的後備箱容積並沒有優勢。但作為一輛SUV,第二排放倒后的表現就很驚人了。如果選擇同級別的MpV,那麼就相當於放棄了第二排放倒這個優勢。實測的風光580搭載了一台1。5T渦輪增壓發動機,型號為SFG15T,這副發動機最大輸出150馬力,峰值扭矩220牛米,從參數上看完全沒有優勢。

看過了非常適合家用的SUV奇駿、夠大夠霸氣的銳界、大氣實用的奧德賽、精緻好用的途安L之後,你是覺得SUV好還是MpV好呢?有興趣的朋友可以點擊鏈接查看往期文章:

奧德賽:67.9

途安L:64.6分

銳界:66分

奇駿:65.4分

歐尚A800:54.4分

在8-9萬能買到的SUV裏面,東風風光580絕對是最體面的SUV車型,沒有之一。雖然這輛車從參數上看並沒有亮點,配置方面也不會顯得很有優勢,但是設計確實挺好的。

這一次我們把6款7座MpV/SUV拉到一起進行評測,而在十萬元級別裏面,我們選中的這款風光580看起來和20萬級別的SUV沒兩樣,那麼它的實測表現到底如何?帶你去看看。

風光580最大的特點當然是設計、空間了,而在這個級別裏面還有五菱宏光S3以及長安CX70,但是后兩款車型都會表現出該級別車型中常見的廉價感,而風光580卻不會。並且風光580的實際並不算太差,整體表現符合預期。

對比同價位其他SUV車型,風光580展示出了同級別車型少有時尚感與精緻感,外觀大量曲面的運用讓整輛車圓潤。雖然這款車尺寸還不算特別大,但是看上去卻有飽滿大氣的感覺。

風光580的內飾比較簡潔,觸控大屏配合按鍵操作起來比較方便,下方空調操作區的按鈕布局很清晰。整體內飾色調上不突兀,設計也比較得體,只是中庸中會透出些許單調的感覺。

風光580的外觀工藝是比較有水平,外觀的噴漆均勻度都很不錯,厚度平均值約為90微米,只是均值不足100微米的話就顯得有點薄。車身縫隙則沒有車漆那麼均勻了,各個地方無規律地大小不同。

雖然風光580內飾看上去不錯,但是受限於價格,風光580在內飾材質採用了大面積的硬塑料,而且觸感顯得廉價,不過拼裝工藝還是不錯的,塑料件也沒有毛刺。在雙手常觸碰的地方都是軟質材料,這點值得點贊。

因為風光580的尺寸不突出,而且空間利用率也不高,所以整體空間不算十分寬裕。第二排空間表現還不錯,但是第三排就顯得比較拮据了。第三排的座椅規格比前兩排要小一些,坐墊也稍微偏硬。

相比較7座MpV來說,風光580的後備箱容積並沒有優勢。但作為一輛SUV,第二排放倒后的表現就很驚人了。如果選擇同級別的MpV,那麼就相當於放棄了第二排放倒這個優勢。

實測的風光580搭載了一台1.5T渦輪增壓發動機,型號為SFG15T,這副發動機最大輸出150馬力,峰值扭矩220牛米,從參數上看完全沒有優勢。

與之匹配的是CVT無級變速箱,CVT的平順性還不錯,但同樣抑制了發動機的動力爆發。而且由於標定沒做好,起步時的感覺比較突兀。

在百公里加速方面,速度超過了40km/h后的動力爆發比較強,但由於車身比較重,成績不算太理想。同樣的問題出現在剎車上面,由於車輛慣性較大,而且輪胎性能也不好,所以剎車成績差強人意。

作為SUV車型的風光580顯然跟運動不搭邊了,而且風光580的價位擺在那裡,這輛車主攻的方向是城市SUV,所以舒適性才是它的主題。而且這款車的發動機低扭還算不錯,CVT變速箱也沒有大問題,只是底盤表現就差點了。

不過由於SUV的車身較高,而且風光580的懸挂也偏軟,所以風光580在高速行駛的穩定性上完全沒有優勢,尤其是我們測試的這輛車還帶有跑偏的跡象,所以影響了駕駛質感。

在實際駕駛當中,風光580低速行駛時表現不算太好,頓挫感和油門反應都不好。如果把發動機轉速拉高,CVT變速箱的配合也變得更积極一些,所以高速上超車還算比較有信心的。

風光580的隔音真心比較一般,由於車子的迎風面積也大,而且車輛密封性和隔音材料的運用不到位,所以隔音性能在同級別當中也不算好。雖然這車懸架偏軟,但是遇到坑窪的時候動靜卻不小,所以底盤隔音也比較差。

在售價上風光的指導價算是自主入門SUV上略微偏高的了,但性價比還是不錯的,1.5T發動機的油耗水平在7.2L/100km左右,自主品牌車型的保養成本也還是比較低的。

在此次對比評測裏面,同價位的MpV車型是長安歐尚A800。雖然在設計和通過性方面風光580都有優勢,看起來也比歐尚A800更體面,但是實際使用方面卻沒有優勢。首先是駕控方面輸了一籌,而且隔音性能也差得比較遠。但在這個價位里,如果只想要SUV的話,那麼風光580的優勢還是很明顯的。

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

空間大,爛路隨便跑,比SUV更牛的車竟才8萬多?

1{icon} {views}

除了動力系統上的驚喜之外,風駿6皮卡更讓人意外的就是則是底盤表現,在我們的印象中皮卡更多地是工具車型,在底盤懸挂的表現上也是更加偏向於載重要求以及耐用性,對於舒適性的照顧往往不咋地。然而風駿6皮卡顛覆了我對皮卡的固有認知。

說起來你可能不信,美國市場上銷量榜前三的車型全是皮卡。究其原因,除了美國佬大多身寬體胖需要大車以及汽油十分便宜之外,也與皮卡出色的功能性有關。

從自身的優勢上來說,一台F-150皮卡能將霸氣、空間、動力、豪華以及獨立貨箱帶來的實用性融為一體,而從開掛的角度來說,皮卡還可以拉牛、拉遊艇甚至於加裝房車部件,功能性完爆國內那些如日中天的城市SUV,因此對於多數家庭來說,一台皮卡車才是能夠滿足全部使用需求的車型,這也是美國人如此偏愛皮卡的原因。

然而反觀中國市場皮卡的表現,卻只有唏噓感慨的份。2017年國內皮卡市場全年的銷量只有41萬台,國內所有皮卡一年的銷量加起來甚至還不如一款哈弗H6。但是就在這樣小份額的市場容量以及現有幾十款皮卡產品的市場環境中,長城卻能憑藉兩款產品拿下30%的市場份額,可以說表現相當搶眼了。

為什麼長城能夠把皮卡做得如此成功呢?

在看來,原因有二:

1、深耕多年積累的豐富經驗以及良好口碑

2、出色的產品表現

1、深耕多年積累的豐富經驗以及良好口碑

細看長城的發家史,長城皮卡絕對是開疆拓土的重要功臣,1984年,長城成立,總部設在了河北保定,1996年,長城成為專業製造皮卡的車企,並且於1996年推出自己首款皮卡車型迪爾,1997年長城皮卡出口中東,中國汽車正式走出國門,1998年長城皮卡成為了銷量冠軍,並且這個冠軍寶座一坐就坐到了今天。

拿到冠軍不容易,衛冕冠軍就更加難了;而長城也可以說是十分用心,早在十二年前,長城便有了自主知識產權的柴油發動機,2009年,長城皮卡成為國內首個獲得歐洲WVTA認證的品牌,2011年,長城皮卡出口意大利,中國皮卡正式進入歐洲市場。

2012年,風駿5成為國內首款搭載ABS+EBD的皮卡車型。可以說長城一直在潛心研究消費者的用車需求以及偏好,力求得到消費者的認可,而且長城在一番努力之後也確實交出了不錯的答卷。

2、出色的產品表現

這一次主要試駕的是長城的風駿6皮卡,而且還是一個汽油版,對於這樣一輛長達5385mm,軸距達到3200mm的大傢伙來說,2.4L的汽油發動機搭配5擋手動變速箱的動力看似有些弱,但是實際動力表現卻是比較讓人意外的,這台發動機的峰值扭矩在2500轉就能夠全部爆發,因此日常駕駛感覺低扭很不錯,離合的結合點也比較清晰,日常駕駛動力感受還是挺輕快的。

除了動力系統上的驚喜之外,風駿6皮卡更讓人意外的就是則是底盤表現,在我們的印象中皮卡更多地是工具車型,在底盤懸挂的表現上也是更加偏向於載重要求以及耐用性,對於舒適性的照顧往往不咋地;然而風駿6皮卡顛覆了我對皮卡的固有認知。

風駿6的底盤更加類似於硬派SUV,厚實的輪胎以及較長的懸挂行程使得車輛面對大顛簸和坑窪時也十分從容,底盤也有不錯的厚實感,走在路上大大小小的振動顛簸都能被很好地過濾,舒適性確實讓人驚喜。

風駿6皮卡的指導價為8.38-11.48萬,這樣的價格在目前的皮卡市場上屬於偏低的,但是風駿6的基礎配置卻是相當的高,全系標配中控大屏、倒車影像、自動空調、電調后視鏡、多功能方向盤、鋁合金輪轂等配置,中配車型還有ESp、定速巡航等高端配置,對於一款皮卡來說,這樣的配置水準卻是非常高了。

時至今日,市面上可供選擇的皮卡產品已經有了幾十款,並且大多數車型都布局在相近的價位,如果此時還靠着老一輩車型積累的口碑優勢混日子,前途只會越來越迷茫,因此在皮卡上長城並沒有懈怠。這一點風駿5&風駿6就是最好的例子。

總結

雖說如今受政策以及用車環境限制,皮卡市場似乎表現不溫不火,但是隨着政策的逐漸放開,以及諸如福特F-150、道奇RAM、豐田坦途等豪華型皮卡在國內的熱銷,在不久的將來皮卡將不再是低端工具車的代名詞,皮卡的火熱似乎正在慢慢變成現實。

因此,日產、五十鈴等許多合資車企也紛紛布局皮卡產品,甚至於奔馳也造出了自己的皮卡車。可以說皮卡市場未來的體量是不可估量的,而在這大好前程面前,長城皮卡無疑是最具有嗅覺的車企之一,雖然目前長城皮卡的價格局限在6-12萬這個區間,風駿5/風駿6的工具車屬性也略為明顯,不過據廠方工作人員透露,長城在未來兩到三年內會推出更加偏向於家用定位的採用螺旋彈簧后懸挂的皮卡,並且自動擋產品也正在积極研發當中;憑藉技術和體量優勢,長城皮卡的優勢還將繼續擴大;二十年前,長城憑藉皮卡佔據市場,二十年後,長城皮卡的神話還將延續;不過在進軍更高一級皮卡市場的同時,長城是不是還需要創立一個更加高端的皮卡品牌來參与競爭呢?本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

爛路山路都能跑,家用也有面子,這款合資SUV還不到17萬?

1{icon} {views}

不存在的,根本不用鎖止后差速鎖,即便是很陡的坡,2。5L發動機足以應付,有些坡度真的非常陡,特別是越到山頂越陡,從拍攝圖中就可以看出來,頭已經昂起來了,還能看到底盤離地面還是比較高的,離地間隙確實很強,總的來說就是爬了一個很陡,很多彎,很泥濘的山,一般的城市SUV不能像途達那樣平穩通過,難度算是中等吧,也能夠體現其的越野性能的了,遺憾的是由於時間和天氣關係,後面有岩石以及坡度高達40°左右的路段就沒能體驗了,“可以說是相當危險”這是教練的原話,只能以後找機會嘗試一下了。

說起硬派越野車,大家首先想到的應該是Jeep牧馬人 或者是豐田普拉多,而如果是真正玩越野的車友,一定知道日產在硬派越野領域也有着豐厚的經驗,從途樂開始,日產便一直深耕專業越野SUV領域,而就在前两天,另一款全新硬派SUV——途達正式上市,上市的價格就非常具有吸引力,17萬不到的起步,對很多同級別的自主車型有不小的威脅。

途達可以看作是小號的途樂,價格非常漂亮,那麼實力怎麼樣?作為硬派越野SUV,四驅性能夠不夠強悍?把它開到終南山體驗一下越野道路為你揭曉。

途達是硬派越野SUV,所以把最重要的駕駛以及越野體驗部分放到最前面。

駕駛&越野

搭載一款代號為QR25的2.5L 自然吸氣發動機,最大馬力184匹,峰值扭矩可達251N·m,匹配6擋手動或者7擋手自一體變速箱。

大,真的很大,這是開這款車第一感覺,寬度1850mm,老實說即使是老司機一開始也有點難適應,很難把車身穩住在車道正中間,潛意識的就會偏離路邊壓着左側路線走,同行前後車的朋友都有這樣的情況;車身高度1835mm,坐上去讓身高1.7米的也能體驗一把俯視眾車的感覺,在收費站也能與收銀員微笑平視。

僅僅是夠用,是覺得這是對這款車動力比較中肯的評價,試駕車為頂配車型,官方車身重量數據就已經達到1926KG,在試駕的過程中,全車四個成年人,再加幾個背包,二噸多的整備重量,整個動力輸出比較平淡。

即便是比較平淡,但加速還是比較線性的,在渦輪增壓浪潮開着這麼大的自吸車型也是種奇特的體驗,有朋友可能會覺得這股動力日常開肯定是很肉的,2000,甚至到3500轉左右都是比較平淡的輸出,峰值扭矩轉速在4000后爆發,事實這個轉速以後給你的推背感並不是很強。

值得肯定的是4000轉以下的隔音是比較好的,怠速時可以說聽不到發動機的聲音,到了峰值扭矩轉速后發動機聲音就來了,動靜較大,但不難聽。

懸挂偏硬,支撐性不錯,但是由於坐墊柔軟度很好,所以即使是中小顛簸有着不錯的舒適性。

然而其實對於硬派越野來說,最重要的是並非動力性能,而是功能性,扭矩夠大就行,鎖止后差速鎖可以把扭矩放大2.7倍,爬坡、走泥路最合適不過了;在低速行駛時,如果是渦輪增壓發動機,動力輸出控制不平順,渦輪介入那一下子或許會措手不及,平時在城市鋪裝路面滿足通勤需求,在野外也能有好的脫困能力,這麼看2.5的自吸其實是更合適的。

油門和剎車前段都很輕,前40%的行程差不多是輸出20%的力度,開起來很佛系;而由於方向盤用的是油壓助力,感覺是略重的,而且轉向不算精確,同車的女生就覺得很好開很順手,在越野的時候其實是不用太精確的轉向,這同樣是出於側重於功能性的,又輕又假的电子轉向在越野過程當中反而會給不了駕駛員紮實的感覺。

以上是基本的駕駛感受,接下來是重頭戲部分,越野性能如何?駕駛路段時秦嶺七十二峪,通過的是其中前小部分以及牛脊樑森林公園再去挑戰一個當地推薦的越野聖地。

進入牛脊樑國家森林公園,整一段十幾公里都是上山連續爬坡路段,連綿多彎,十來二十米就是一個彎道,並且經常有落石,路況有點複雜險峻,而在這時候225mm離地間隙的優勢就有用武之地了,一路開過來,小石頭可以無視,大石頭稍微躲一下就沒事了,如果是開一輛轎車,石子彈到底盤上都會有點小怕。

連綿的爬坡路段其實並沒有感到動力不足,線性的動力輸出,配合沉穩的方向盤,反而能給足夠的信心,有點要注意的是方向盤在轉到後段時,需要轉動更大的角度才能保證不會轉向不足。

來到越野路段,坡度比牛脊樑景區更加大,比較小的坡度都有20°左右,較大難度的30度、40°都有,路面更窄,剛好適合一輛車通過,而且剛下過雨,路面濕滑泥濘。

一上去就是非常陡的坡,然後就是U型彎,這個時候掛空擋,切到低速四驅,可以實現扭矩放大,轉速控制在一千轉出頭,路面有點難度,然而在強大的越野性能下,輕點油門就能緩慢通過。

上了坡就是急轉彎,好在彎還是比較寬的,一把方向就能安全通過,再次感受到液壓方向盤給人足夠的信心,有點難度的是需要判斷兩側的距離,再次強調這車真的大,兩邊的樹枝不斷拍打車身,不過只要跟着車撤走就沒問題。

即便是坡度很大,路面也很泥濘,得意於良好的輪胎性能,打滑?不存在的,根本不用鎖止后差速鎖,即便是很陡的坡,2.5L發動機足以應付,有些坡度真的非常陡,特別是越到山頂越陡,從拍攝圖中就可以看出來,頭已經昂起來了,還能看到底盤離地面還是比較高的,離地間隙確實很強,總的來說就是爬了一個很陡,很多彎,很泥濘的山,一般的城市SUV不能像途達那樣平穩通過,難度算是中等吧,也能夠體現其的越野性能的了,遺憾的是由於時間和天氣關係,後面有岩石以及坡度高達40°左右的路段就沒能體驗了,“可以說是相當危險”這是教練的原話,只能以後找機會嘗試一下了。

很帥氣的硬漢

再看一下的基本情況,雖然途達是定位為中型SUV,但是它的車身尺寸:4882*1850*1835mm,軸距為2850mm,車身長度和軸距都比中大型的普拉多4840和2790還要長,尺寸相當大。

空載情況下,接近角為32°,離去角為27°,最小離地間隙高達22.5厘米,此等通過性,與同為非承載式車身硬派SUV相比,它的數據甚至比普拉多還要長,對比自主車型更是不在話下,途達硬派越野的基本要素毋庸置疑,當然肯定就比不上牧馬人了,牧馬人本身就是專業級的,新款車型更加強悍。

側面非常好看,車屁股很大,也比較方正,再加上很大的一塊擾流板提供了不錯的視覺效果,整一個車身側面也是威風凜凜。

車身很高,整個前臉的造型設計很威武,V-Motion設計前臉設計很年輕,舒展上揚的LED日行燈的形狀很醒目,中間鍍鉻飾條很粗壯,但是用在這個車上則氣勢十足,這張臉可以說是非常帥氣了,相信很多人都會喜歡。

發動機蓋上面的線條很飽滿,滿滿的肌肉感,坐在前排會看到機蓋兩側有兩個比較明顯的凸起。

還有不少亮點,最大的亮點應該是前杠的兩個獠牙了,視覺上極具攻擊力,下方是黑色塑料護板,蹭着不心疼。

戰鬥過後的划痕

車尾也非常很好看,尾部LED視寬燈和前LED日行燈一致,看起來很順眼,在看來,前後左右哪個角度的視覺效果都很棒,果然大的就是耐看。

18寸輪轂,後輪為鼓剎,採訪日本的技術人員表示,廠商認為鼓剎的性能是最適合這款車的設定,當然後期會根據消費者的需求考慮更換成碟剎,在試駕過程中其實並沒有什麼感覺這個剎車有什麼毛病,也沒感覺到有什麼影響,後期使用性能如何有待以後市場反饋。

空間

真皮座椅很鬆軟,日產沙發廠的名號不是蓋的,坐上去很舒服,給人一種很居家的感覺,日常通勤也可以很輕鬆。身高1.7米,調整到合適的駕駛坐姿后(偏高的坐姿),頭部空間很充裕,有一拳。

而到了後排,保持前排座椅不變的情況下,頭部空間就超過兩拳,腿部空間不用比劃了,一目瞭然。

當然1.7米身高參考性有限,所以找來身高1.82米的體驗者,調整到合適的駕駛坐姿后,在前排仍有一拳頭部空間。

而到了後排,頭部空間和腿部空間都是差不多兩拳,空間足夠大的,而且坐墊柔軟度很好,買了這車回去,家裡可以少買一個沙發,而且後排座椅靠背角度比較大,北京癱也沒問題。

後備廂很平整,而且很大,拉貨拉東西什麼的完全沒問題,第二排座椅可以放倒,無法形成一個平面,算是個可以無視的不足吧,這後備廂空間裝什麼都沒問題了。

居家內飾

進入車內,其實跟外形是有點反差的,和硬朗帥氣的外觀想比,內飾的風格是日產以往居家實用走向,內飾基本都是黑的,也可以看到“V”型的布局,副駕側的中控台只有一條曲線,所以設計一點都不花哨,甚至是平淡的,就是一輛普普通通的家用SUV。

方向盤是日產舊款的款式,如果換上新奇駿哪款應該會讓車內看起來年輕很多,方向盤的尺寸和手感都比較平淡,找不到什麼亮點。

7英寸中控屏是傳統的嵌入式,兩側是銀色裝飾,面板有木紋,使得不那麼沉悶,下方是空調控制按鍵,是非常容易上手的實體按鍵,這非常符合越野、實用性定位,有百度CarLife功能。

行車電腦可以显示多種信息

比較奇特的是倒車影像或者全景攝像的显示是位於內后視鏡左側,這是比較少見的,雖然尺寸小,但是不影響觀察判斷,依然很好用。

下面的這個分時四驅操作區就是把它和普通家用SUV區分開來的地方,分時四驅頂配和次頂配配備,並且配備机械牙嵌式后差速鎖,分時四驅可以實現兩輪驅動、四輪高速、以及四輪低速,四輪高/低速均可以實現前後50:50扭矩分配。

重頭戲是這個后差速鎖,鎖止后差速鎖可以讓左右輪作為一個整體旋轉,然後把動力扭矩傳遞到有抓地力的車輪,以實現脫困;好消息是,30萬以內帶机械式後橋差速鎖的硬派越野車,僅此一款,22.58萬帶回家。

總的來說,途達是一款非常具有吸引力的車型,功能性非常強,長得又帥氣,車身又大,二十萬出頭的價格讓很多人都能實現越野夢,是不折不扣的硬派越野車,除了內飾不太新穎以外,並沒有什麼太大缺點了,另外,那鬆軟舒適的座椅,作為日常開也有不錯的舒適性。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

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

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

7秒破百的霸氣大7座SUV,賣多少萬能讓你忘了漢蘭達?

2{icon} {views}

0T發動機是同一副,採用時下流行的缸內直噴、雙渦管單渦輪增壓、水中冷、缸蓋集成排氣歧管等方式來提升發動機響應和燃油經濟性。而與之配搭的是大家都很熟悉的ZF 9AT變速箱,帶有4個超比擋以降低巡航轉速提升燃油經濟性。

說到國人對SUV或者越野車最早的記憶,除了以前的BJ212以外,就是當時跑遍中國大江南北的BJ213,也就是第二代切諾基。那可以說是所有越野愛好者的美好回憶,也一定程度上塑造了國人對於SUV這種“全能”的基本印象。甚至很多人說SUV還是會用“吉普車”的話術,說明JEEp品牌一直都在國內深入民心。不過現在國內對於SUV的市場需求日益增大,特別是7座SUV。不過一直以來,JEEp都缺少了一個在中大型7座SUV市場的產品。今天,我們來到了寧波,去JEEp專為中國市場研發的第一台7座中大型SUV,新一代大指揮官。

新一代大指揮官是現在JEEp城市SUV裏面的旗艦車型,也是專為中國市場所打造的一款全新的7座SUV,按照官方的話術叫”專業級全場合高端SUV”,設計定位兼顧商務和家用。和大部分7座SUV都以5座車型為基礎的不一樣,大指揮官在設計之初就定位為“真”7座SUV,這從其宣傳“7人VIp商務艙”就可見。按照其定位及價格區間,競爭對手就鎖定為漢蘭達、銳界等不多的幾款車型上。

很“JEEp”的外觀:

從設計上說,大指揮官沿用了雲圖概念車上的一些設計元素。不過從整體的外觀設計來說,相比於指南者的活潑和自由光的時尚,大指揮官更強調沉穩的外觀設計風格,整體的外觀氣氛也偏向商務。雖然說整車造型基本使用JEEp家族的直線條,也保留了7孔前進氣格柵的的家族元素,不過整車看起來在硬派的整體感覺上加入了一些柔性的線條。

加上前格柵、車燈和轉向燈等部分都採用了扁平的造型,讓整個前臉看起來變得更大和更精緻,看起來並沒有美系的粗枝大恭弘=叶 恭弘。

大指揮官的車身尺寸為4873*1892*1738mm,三圍尺寸相比漢蘭達和銳界兩個競爭對手來說並不算特別大,2800mm的軸距也僅僅處於兩者中等的位置。不過由於線條設計的原因,顯得大指揮官較長也較幹練,並不像銳界這種典型的美國味。很多凸顯肌肉感和力量感的稜線和外拋的恭弘=叶 恭弘子板設計也是點到即止,整體風格很沉穩,很符合大部分人對於商務SUV的印象。

照顧周到的乘坐空間

既然能叫做“7人VIp商務艙”,內飾自然不能馬虎。大指揮官的內飾設計雖然並不能說很“精緻”,但很多設計也很考究,整體感覺更像一台歐洲車,而不像粗枝大恭弘=叶 恭弘的美國車。採用淺色和深色的Two Tone配色,加上一些啞光鍍鉻的邊框來提升細節感,透露出一些豪華的屬性。

特別是材質方面大指揮官並不吝嗇。除了大量使用軟性材質作為中控台和門板的包裹材質以外,也大量使用了皮革以提升檔次感。頂配的座椅甚至有麂皮和Nappa皮,這在這個級別的SUV產品中算是非常越級的表現。

除了設計和用料以外,乘坐舒適性也是“7人VIp商務艙”所注重的部分。不過由於其實際車身尺寸在同級並不算很大,因此它的乘坐空間並不算非常寬闊,甚至比漢蘭達等級別標杆會稍窄小一些。不過大指揮官用了其他方法來提升乘坐舒適性。首先座椅的坐墊和靠背都較厚而且較軟。另外全系標配三區自動空調,各種USB接口、230V電源、12V電源、座椅加熱等配置都比較齊全。

第三排的座椅坐墊厚度和軟硬度適中,而且椅背可以小角度微調,也有獨立出風口和獨立的閱讀燈,基本能照顧到第三排乘客的中短途乘坐舒適性要求。

全景天窗的面積較大,整個車內空間的開陽感非常好。副駕駛的靠背邊上更設計了SUV少見的老闆鍵,讓第二排的乘客能夠擁有更好的乘坐空間,加上有隱私玻璃和手動遮陽簾,滿足商務要求。唯一的缺點就是頭枕較硬,這也是不少車型上的通病了。

同級領先的動力性能

動力方面,這次試駕的大指揮官搭載一台2.0T GME T4發動機,最大馬力265匹,最大扭矩400牛米。實際上這是和Giulia上的2.0T發動機是同一副,採用時下流行的缸內直噴、雙渦管單渦輪增壓、水中冷、缸蓋集成排氣歧管等方式來提升發動機響應和燃油經濟性。而與之配搭的是大家都很熟悉的ZF 9AT變速箱,帶有4個超比擋以降低巡航轉速提升燃油經濟性。再加上適時四驅的幫助下,讓這台7座中型SUV獲得低於8秒的百公里加速時間。相比銳界和漢蘭達,百公里加速時間幾乎短了一秒鐘。可以說,動力性能在同級處於領先水平。

實際駕駛時,動力表現還算比較滿意。即使使用很淺的油門深度,引擎的動力反應也比較迅速。而全油門時也有不錯的加速力,以一台這樣的中大型SUV來說,動力表現已經很好,當然不能和那些動輒三四百匹的高性能SUV作比較,畢竟這還是結合家用和商務需求而設計的。而需要注意的是油門和剎車腳感較重行程也較大,新手來說用力過猛或用力過小都會造成頓挫,需要一些習慣的時間。

另外,大指揮官採用了ZF提供的9AT變速箱。據說這副變速箱的TCU標定按照中國的消費者需求所調校,因此整體表現就四個字“超級平順”。任何換擋基本上感受不到頓挫,但隨之而來的代價就是換擋速度較慢。

另外由於為了降低油耗,變速箱非常积極往超比擋上換。雖然巡航轉速較低燃油經濟性較好,但是遇到需要加速的場合,油門一下去變速箱還要思考一下再退擋,動力的反應變得稍弱。還有一個細節,為了能夠在極低速更有效的推動較大的車身重量,因此1擋的齒比設定較大,導致初段加速較“沖”,但換擋時機較晚(約3000rpm),會造成一些低速的頓挫。當然,這副變速箱以舒適性和行駛質感來說還是不錯的。

並不硬派的駕駛感覺

懸挂方面,大指揮官採用了這個級別較常見的前麥弗遜后多連桿設計,調校也是傾向舒適性的設計。由於要照顧一定的非鋪裝路面需求,因此懸挂行程較長。這次我們試駕的均是頂配車型,都配有自適應懸挂,能根據行駛的需求調整避震的阻尼。在行駛過程中會感到舒適性很好,對於顛簸路面的濾震也不錯。整個底盤給人的感覺過濾顛簸很乾脆,較厚實。不過估計照顧到舒適性,因此彈簧腳軟,在較大動力介入的時候,車頭抬起的現象會比較明顯,剎車的時候也會有些點頭。

而操控性方面,大指揮官也採用了可變助力的設計。低速時整體方向手感傾向輕盈,而高速的時候會加重得比較明顯,不過整個變化還算線性不會有很突兀的感覺,方向指向性也都算靈敏了。雖然說懸挂整體偏軟,不過過彎時傾側抑制卻出乎意料並不會很嚴重,整體駕駛還是頗像轎車的。不過由於體重和身高始終在哪裡,因此即使動力表現較好,大指揮官還是不適合去做過分的激烈駕駛。

而隔音方面,大指揮官的表現還是頗不錯的。據悉,為了提升NVH表現,大指揮官採用了同級別車型較主流的“空腔注泡”的工藝,對車身空腔灌注環保發泡材質來降低噪音,整體隔音得水平在同級表現還是不錯的。。只是四氣缸的發動機始終排氣脈衝不夠綿密,因此傳入駕駛室里的引擎聲較單薄,所幸排氣的聲浪還算厚實,彌補了一些質感上的缺失(如果有六缸就好了)。

值得一提的是,大指揮官有一套智能切換的適時四驅系統,我們進行了輕度的非鋪裝路面體驗。在一些較泥濘的道路上四驅能夠讓行駛軌跡更穩定,同時車身剛性很高,在很顛簸的路面行駛也沒有發生各種扭曲和異響,和競品相比有較大的優勢。

總結

總體來說,大指揮官是一台並不很“JEEp”和很“美式”的SUV,這也和它說的“專為中國市場打造”不謀而合。整體而言,較強的動力是大指揮官的優勢,而舒適性也很不錯,整體的駕駛感受符合國人對歐洲車的總體印象。雖然說乘坐空間和外觀尺寸等並不佔優勢,不過整體外觀和內飾設計沉穩大氣,風格兼顧商務和家用,而且國人對JEEp品牌有一個固有的良好印象,在該級別能夠做到差異化競爭,市場潛力還是很不錯的。

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

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

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

面試官:淦!0202年你還不知道面向對象?

3{icon} {views}

2020年6月13日 多雲轉暴雨

I’m sad,tired,negative,powerless,miss,lonely fine 🙂

那你回去等通知吧

面試官:我看你簡歷上說,你的主要編程語言是Java,偶爾也用Python,那麼你可以說一下這兩個的相同點在什麼地方嗎?

山禾:它們都是高級程序設計語言,都是面向對象的語言。

面試官:那你講講你是怎麼理解面向對象的吧。

山禾:emmm。。封裝,繼承,多態是面向對象的三大特徵。

面試官:還有其他要補充的嗎?

山禾:沒了

面試官:那你回去等通知吧

是什麼

痛定思痛,挫折只會讓我變得更強,奧利給!下面開始進入正文,首先我們需要知道面向對象是什麼?

面向對象,顧名思義,是面向對象,也就是說單身狗是不配使用面向對象的編程語言的(當然只是開個玩笑,逃)。

對象,就是把現實世界中的實物映射抽象到虛擬世界中,把實物的屬性和行為,通過代碼的方式表達出來。然後通過設計對象的數據結構(屬性),然後使用算法進行行為的模擬(方法),就完成了從現實到虛擬的一個映射。

實體 對象
屬性 數據結構 變量
行為 算法 方法

與面向對象經常拿來對比的就是面向過程編程,那麼他們之間的區別在什麼地方呢?

面向過程和面向對象

面向過程編程,打個比方,我們買過的一般的玩具(變形金剛),我們必須要按照它說明書上的步驟,一步一步的去組裝,才能得到最後的玩具,如果我們想要一個新的玩具,就要去商場買一個新的,然後按照說明書的順序一步一步的組裝。

而面向對象編程,就可以理解為積木,沒有一個固定的拼裝方式,我們可以發揮自己的想象力,去自由的拼裝和組裝,同樣的模塊在不同的地方可以起到不同的作用(多態),一塊兒積木就是一個最小的單位,我們不用去關心積木是怎麼造的(封裝)。也可以用多個對象組裝起來去拼裝成一個新的對象(繼承)。大大的方便了我們的設計,不再拘泥於過程,極大程度上的放飛了生產力和效率。

為什麼

我們剛剛已經了解了面向對象是什麼,下面我們來說一下,為什麼要用面向對象編程

  1. 首先從理解角度上來說,它符合我們對現實世界的認知習慣,更容易去理解、實現和設計我們的需求。

  2. 其次從軟件設計的角度上來說,行為的實現對於外部是完全封閉的,只需要提供對應的接口就可以獲得相應的結果,降低了代碼與代碼之間的耦合度。符合我們高內聚,低耦合的設計理念。優雅,客觀,層次分明,像積木一樣,可以方便的實現插拔和維護,對象組合而成的模塊化和服務化,更是大大擴展了系統的伸縮性,便於維護、擴展和復用。這也是為什麼越來越多的編程語言選擇向這個方向去靠攏( TypeScript說的就是你

怎麼用

剛剛我一直忍住,沒有去提面向對象的三大特性:封裝、繼承和多態,我相信我們有一些經驗的開發人員來說,這三個詞語是再熟悉不過了。下面,我通過代碼的方式,來看看這三個特性在代碼中的模擬應用。

封裝

@Data
public class Uzi {

    // 选手類型
    private String type;
    // 选手最擅長英雄
    private String bestHero;
    // 选手狀態
    private Status status;
    
    public void play() {
        status.say();
    }
}

public class Status {
   public void say(){}
}

上面是一段非常簡單的代碼,我定義了一個很簡單的類,裏面的nametypebestHero對於我們來說都是一個包裝在盒子里的東西。比如:

    Uzi uzi = new Uzi();
    uzi.setType("ADC");
    uzi.setBestHero("孫尚香");

我們看到的Uzi可能就是一個主玩孫尚香的職業ADC选手(狗頭保命,我啥都不知道),他怎麼玩的,我們不需要知道,我們只需要知道他這樣的一名选手就可以了。這就是封裝的魅力:

  1. 隱藏了實現的細節,提供對外的訪問方式。
  2. 外部如果想要訪問,必須經過這個方法。

繼承

看完了封裝,下面我們來看繼承,我們分別用三個狀態去繼承Status類:

public class ShunFeng extends Status {
    
    @Override
    public void say(){
        System.out.println("順風狂小狗");
    }
}
public class NiFeng extends Status {
    
    @Override
    public void say(){
        System.out.println("逆風簡自豪");
    }
}

public class JueJing extends Status {
    
    @Override
    public void say(){
        System.out.println("絕境Uzi");
    }
}

關鍵字extends表示正在構造的新類派生於一個已經存在類。這個已經存在的類被稱為父類(超類,基類);新創建的類被稱為子類。在通過擴展父類定義子類的時候,僅僅需要指齣子類和父類的不同之處。因此在設計類的時候,應該將通用的方法放在父類中,而將具有特殊用途的方法放在子類中。

我們只是在父類Status中定義了say方法,然後三種不同的類去繼承這個類,然後實現方法,這樣就體現出了面向對象在設計上的可擴展性,但是需要注意的是,繼承會破壞封裝,我們需要謹慎使用,盡量使用一些設計模式去避免繼承,合理使用,才能體現出它的優勢~

說完了繼承,接下來就要去說多態了。

多態

在說多態之前,我們先來看一下代碼:

public class Test {
    public static void main(String[] args) {
        //uzi1
        Uzi uzi1 = new Uzi();
        ShunFeng shunFeng = new ShunFeng();
        uzi1.setStatus(shunFeng);
        //uzi2
        Uzi uzi2 = new Uzi();
        NiFeng niFeng = new NiFeng();
        uzi2.setStatus(niFeng);
        //uzi3
        Uzi uzi3 = new Uzi();
        JueJing jueJing = new JueJing();
        uzi3.setStatus(jueJing);
        
        //uzis
        Uzi[] uzis = {uzi1,uzi2,uzi3};
        // 多態
        for (Uzi uzi : uzis) {
            uzi.play();
        }
    }
}

運行程序的結果: (TMD,淚目,青結)

所謂的多態就是一個對象變量(比如上文中的status變量)可以指示多種實際類型的現象(比如status既可以引用Status對象,也可以引用它的子類ShunFeng對象)被稱為多態。在運行時候能夠自動地選擇調用哪個方法的現象被稱為動態綁定,上面打印出的語句,就說明了這一點。

後續

原來我們天天掛在口邊的面向對象,其實某種程度上來說,更像是一種哲學,一種計算機科學發展的自然規則。

如果你有學到,請給我點贊+關注,這是對一個堅持原創作者的最大支持!我是山禾,千篇一律的皮囊,萬里挑一的靈魂,一個不太一樣的寫手。

世事洞明皆學問,人情練達即文章。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

重學 Java 設計模式:實戰享元模式「基於Redis秒殺,提供活動與庫存信息查詢場景」

6{icon} {views}

作者:小傅哥
博客:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收穫!

一、前言

程序員‍‍的上下文是什麼?

很多時候一大部分編程開發的人員都只是關注於功能的實現,只要自己把這部分需求寫完就可以了,有點像被動的交作業。這樣的問題一方面是由於很多新人還不了解程序員的職業發展,還有一部分是對於編程開發只是工作並非興趣。但在程序員的發展來看,如果不能很好的處理上文(產品),下文(測試),在這樣不能很好的了解業務和產品發展,也不能編寫出很有體繫結構的代碼,日久天長,1到3年、3到5年,就很難跨越一個個技術成長的分水嶺。

擁有接受和學習新知識的能力

你是否有感受過小時候在什麼都還不會的時候接受知識的能力很強,但隨着我們開始長大后,慢慢學習能力、處事方式、性格品行,往往會固定。一方面是形成了各自的性格特徵,一方面是圈子已經固定。但也正因為這樣的故步,而很少願意聽取別人的意見,就像即使看到了一整片內容,在視覺盲區下也會過掉到80%,就在眼前也看不見,也因此導致了能力不再有較大的提升。

編程能力怎樣會成長的最快

工作內容往往有些像在工廠擰螺絲,大部分內容是重複的,也可以想象過去的一年你有過多少創新和學習了新的技能。那麼這時候一般為了多學些內容會買一些技術書籍,但!技術類書籍和其他書籍不同,只要不去用看了也就只是輕描淡寫,很難接納和理解。就像設計模式,雖然可能看了幾遍,但是在實際編碼中仍然很少會用,大部分原因還是沒有認認真真的跟着實操。事必躬親才是學習編程的最好是方式。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三個,可以通過關注公眾號bugstack蟲洞棧,回復源碼下載獲取(打開獲取的鏈接,找到序號18)
工程 描述
itstack-demo-design-11-01 使用一坨代碼實現業務需求
itstack-demo-design-11-02 通過設計模式優化代碼結構,減少內存使用和查詢耗時

三、享元模式介紹

享元模式,主要在於共享通用對象,減少內存的使用,提升系統的訪問效率。而這部分共享對象通常比較耗費內存或者需要查詢大量接口或者使用數據庫資源,因此統一抽離作為共享對象使用。

另外享元模式可以分為在服務端和客戶端,一般互聯網H5和Web場景下大部分數據都需要服務端進行處理,比如數據庫連接池的使用、多線程線程池的使用,除了這些功能外,還有些需要服務端進行包裝后的處理下發給客戶端,因為服務端需要做享元處理。但在一些遊戲場景下,很多都是客戶端需要進行渲染地圖效果,比如;樹木、花草、魚蟲,通過設置不同元素描述使用享元公用對象,減少內存的佔用,讓客戶端的遊戲更加流暢。

在享元模型的實現中需要使用到享元工廠來進行管理這部分獨立的對象和共享的對象,避免出現線程安全的問題。

四、案例場景模擬

在這個案例中我們模擬在商品秒殺場景下使用享元模式查詢優化

你是否經歷過一個商品下單的項目從最初的日均十幾單到一個月後每個時段秒殺量破十萬的項目。一般在最初如果沒有經驗的情況下可能會使用數據庫行級鎖的方式下保證商品庫存的扣減操作,但是隨着業務的快速發展秒殺的用戶越來越多,這個時候數據庫已經扛不住了,一般都會使用redis的分佈式鎖來控制商品庫存。

同時在查詢的時候也不需要每一次對不同的活動查詢都從庫中獲取,因為這裏除了庫存以外其他的活動商品信息都是固定不變的,以此這裏一般大家會緩存到內存中。

這裏我們模擬使用享元模式工廠結構,提供活動商品的查詢。活動商品相當於不變的信息,而庫存部分屬於變化的信息。

五、用一坨坨代碼實現

邏輯很簡單,就怕你寫亂。一片片的固定內容和變化內容的查詢組合,CV的哪裡都是!

其實這部分邏輯的查詢在一般情況很多程序員都是先查詢固定信息,在使用過濾的或者添加if判斷的方式補充變化的信息,也就是庫存。這樣寫最開始並不會看出來有什麼問題,但隨着方法邏輯的增加,後面就越來越多重複的代碼。

1. 工程結構

itstack-demo-design-11-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── ActivityController.java
  • 以上工程結構比較簡單,之後一個控制類用於查詢活動信息。

2. 代碼實現

/**
 * 博客:https://bugstack.cn - 沉澱、分享、成長,讓自己和他人都能有所收穫!
 * 公眾號:bugstack蟲洞棧
 * Create by 小傅哥(fustack) @2020
 */
public class ActivityController {

    public Activity queryActivityInfo(Long id) {
        // 模擬從實際業務應用從接口中獲取活動信息
        Activity activity = new Activity();
        activity.setId(10001L);
        activity.setName("圖書嗨樂");
        activity.setDesc("圖書優惠券分享激勵分享活動第二期");
        activity.setStartTime(new Date());
        activity.setStopTime(new Date());
        activity.setStock(new Stock(1000,1));
        return activity;
    }

}
  • 這裏模擬的是從接口中查詢活動信息,基本也就是從數據庫中獲取所有的商品信息和庫存。有點像最開始寫的商品銷售系統,數據庫就可以抗住購物量。
  • 當後續因為業務的發展需要擴展代碼將庫存部分交給redis處理,那麼久需要從redis中獲取活動的庫存,而不是從庫中,否則將造成數據不統一的問題。

六、享元模式重構代碼

接下來使用享元模式來進行代碼優化,也算是一次很小的重構。

享元模式一般情況下使用此結構在平時的開發中並不太多,除了一些線程池、數據庫連接池外,再就是遊戲場景下的場景渲染。另外這個設計的模式思想是減少內存的使用提升效率,與我們之前使用的原型模式通過克隆對象的方式生成複雜對象,減少rpc的調用,都是此類思想。

1. 工程結構

itstack-demo-design-11-02
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo.design
    │           ├── util
    │           │	└── RedisUtils.java	
    │           ├── Activity.java
    │           ├── ActivityController.java
    │           ├── ActivityFactory.java
    │           └── Stock.java
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java

享元模式模型結構

  • 以上是我們模擬查詢活動場景的類圖結構,左側構建的是享元工廠,提供固定活動數據的查詢,右側是Redis存放的庫存數據。
  • 最終交給活動控制類來處理查詢操作,並提供活動的所有信息和庫存。因為庫存是變化的,所以我們模擬的RedisUtils中設置了定時任務使用庫存。

2. 代碼實現

2.1 活動信息

public class Activity {

    private Long id;        // 活動ID
    private String name;    // 活動名稱
    private String desc;    // 活動描述
    private Date startTime; // 開始時間
    private Date stopTime;  // 結束時間
    private Stock stock;    // 活動庫存
    
    // ...get/set
}
  • 這裏的對象類比較簡單,只是一個活動的基礎信息;id、名稱、描述、時間和庫存。

2.2 庫存信息

public class Stock {

    private int total; // 庫存總量
    private int used;  // 庫存已用
    
    // ...get/set
}
  • 這裡是庫存數據我們單獨提供了一個類進行保存數據。

2.3 享元工廠

public class ActivityFactory {

    static Map<Long, Activity> activityMap = new HashMap<Long, Activity>();

    public static Activity getActivity(Long id) {
        Activity activity = activityMap.get(id);
        if (null == activity) {
            // 模擬從實際業務應用從接口中獲取活動信息
            activity = new Activity();
            activity.setId(10001L);
            activity.setName("圖書嗨樂");
            activity.setDesc("圖書優惠券分享激勵分享活動第二期");
            activity.setStartTime(new Date());
            activity.setStopTime(new Date());
            activityMap.put(id, activity);
        }
        return activity;
    }

}
  • 這裏提供的是一個享元工廠,通過map結構存放已經從庫表或者接口中查詢到的數據,存放到內存中,用於下次可以直接獲取。
  • 這樣的結構一般在我們的編程開發中還是比較常見的,當然也有些時候為了分佈式的獲取,會把數據存放到redis中,可以按需選擇。

2.4 模擬Redis類

public class RedisUtils {

    private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

    private AtomicInteger stock = new AtomicInteger(0);

    public RedisUtils() {
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            // 模擬庫存消耗
            stock.addAndGet(1);
        }, 0, 100000, TimeUnit.MICROSECONDS);

    }

    public int getStockUsed() {
        return stock.get();
    }

}
  • 這裏處理模擬redis的操作工具類外,還提供了一個定時任務用於模擬庫存的使用,這樣方面我們在測試的時候可以觀察到庫存的變化。

2.4 活動控制類

public class ActivityController {

    private RedisUtils redisUtils = new RedisUtils();

    public Activity queryActivityInfo(Long id) {
        Activity activity = ActivityFactory.getActivity(id);
        // 模擬從Redis中獲取庫存變化信息
        Stock stock = new Stock(1000, redisUtils.getStockUsed());
        activity.setStock(stock);
        return activity;
    }

}
  • 在活動控制類中使用了享元工廠獲取活動信息,查詢后將庫存信息在補充上。因為庫存信息是變化的,而活動信息是固定不變的。
  • 最終通過統一的控制類就可以把完整包裝后的活動信息返回給調用方。

3. 測試驗證

3.1 編寫測試類

public class ApiTest {

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    private ActivityController activityController = new ActivityController();

    @Test
    public void test_queryActivityInfo() throws InterruptedException {
        for (int idx = 0; idx < 10; idx++) {
            Long req = 10001L;
            Activity activity = activityController.queryActivityInfo(req);
            logger.info("測試結果:{} {}", req, JSON.toJSONString(activity));
            Thread.sleep(1200);
        }
    }

}
  • 這裏我們通過活動查詢控制類,在for循環的操作下查詢了十次活動信息,同時為了保證庫存定時任務的變化,加了睡眠操作,實際的開發中不會有這樣的睡眠。

3.2 測試結果

22:35:20.285 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":1},"stopTime":1592130919931}
22:35:21.634 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":18},"stopTime":1592130919931}
22:35:22.838 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":30},"stopTime":1592130919931}
22:35:24.042 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":42},"stopTime":1592130919931}
22:35:25.246 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":54},"stopTime":1592130919931}
22:35:26.452 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":66},"stopTime":1592130919931}
22:35:27.655 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":78},"stopTime":1592130919931}
22:35:28.859 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":90},"stopTime":1592130919931}
22:35:30.063 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":102},"stopTime":1592130919931}
22:35:31.268 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":114},"stopTime":1592130919931}

Process finished with exit code 0
  • 可以仔細看下stock部分的庫存是一直在變化的,其他部分是活動信息,是固定的,所以我們使用享元模式來將這樣的結構進行拆分。

七、總結

  • 關於享元模式的設計可以着重學習享元工廠的設計,在一些有大量重複對象可復用的場景下,使用此場景在服務端減少接口的調用,在客戶端減少內存的佔用。是這個設計模式的主要應用方式。
  • 另外通過map結構的使用方式也可以看到,使用一個固定id來存放和獲取對象,是非常關鍵的點。而且不只是在享元模式中使用,一些其他工廠模式、適配器模式、組合模式中都可以通過map結構存放服務供外部獲取,減少ifelse的判斷使用。
  • 當然除了這種設計的減少內存的使用優點外,也有它帶來的缺點,在一些複雜的業務處理場景,很不容易區分出內部和外部狀態,就像我們活動信息部分與庫存變化部分。如果不能很好的拆分,就會把享元工廠設計的非常混亂,難以維護。

八、推薦閱讀

  • 1. 重學 Java 設計模式:實戰工廠方法模式(多種類型商品發獎場景)
  • 2. 重學 Java 設計模式:實戰抽象工廠模式(替換Redis雙集群升級場景)
  • 3. 重學 Java 設計模式:實戰建造者模式(裝修物料組合套餐選配場景)
  • 4. 重學 Java 設計模式:實戰原型模式(多套試每人題目和答案亂序場景)
  • 5. 重學 Java 設計模式:實戰橋接模式(多支付渠道「微信、支付寶」與多支付模式「刷臉、指紋」場景)
  • 6. 重學 Java 設計模式:實戰組合模式(營銷差異化人群發券,決策樹引擎搭建場景)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

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

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!