到底是用"靜態類"還是單例

orangex發表於2017-11-30

更新了一個疑惑,請盆友們不吝賜教,見第四點。


這裡把都是靜態成員的類叫做靜態類,區別於靜態內部類這個概念。

疑問來自寫各個 Util、Manager、Helper 的時候,到底是應該寫成擁有自己成員變數成員方法的單例模式,還是把所有的成員都做成靜態。這應該是個經典的論題,根據社群和部落格以及個人的理解,我想至少在我自己這裡終結問題下的很多子問題,下面是歸納和一些疑惑思考。

  1. 沒有高低之分,只有使用場景的差別

  2. 比較他們的時候總是有人說單例可以懶載入。所謂單例的延遲載入、懶載入,跟誰比延遲了?延遲的是什麼?是如何延遲的?

    • 是單例模式的懶漢實現(Double Check,靜態內部類,列舉)和餓漢實現比,延遲了

    • 延遲的是 new 這個單例的帶來的開銷(我理解下來,這個開銷不只是空間,時間也有)

    • 這裡就要說道說道了,很多部落格裡根本沒有弄清楚前因後果就搬磚式的 Copy 一堆陳詞濫調放在文章裡。之所以要延遲載入,是想根據業務、場景需要,在第一次用到單例的時候才去例項化它,也就是把建立例項的開銷往後挪了。但是實際上,餓漢式本身就是延遲載入的,我們看一下餓漢式的寫法

      public class SampleClass {
          private static SampleClass sInst = new SampleClass();
          public static SampleClass getInst() {
              return sInst;
          }
      }
      複製程式碼

      Line 2 中 sInst 的建立是在什麼時候執行的呢?是在類載入的最後一步“初始化”中執行的,而初始化這一步是條件的!當且僅當 JVM [^注1]規定的六種條件下才會去初始化,分別是

      1. 建立類的例項

      2. 訪問類或介面的靜態變數(特例:如果是用 static final 修飾的常量,那就不會對類進行顯式初始化。static final 修飾的變數則會做顯式初始化)

      3. 呼叫類的靜態方法

      4. 反射(Class.forName(packagename.className))

      5. 初始化類的子類。注:子類初始化問題,只有父類訪問子類中的靜態變數、方法,子類才會初始化;否則僅父類初始化。

      6. jvm 啟動時被標明為啟動類的類

      所以餓漢式本來就自帶延遲載入的屬性,只有在訪問 Line 3 中 getInst 方法才會去初始化 sInst 並建立例項(滿足了初始化的條件3)。那搞個懶漢式出來幹啥呢,是因為上面這句話是有前提條件的,如果在 getInst 之前訪問了別的靜態成員,或者用了反射等等其他滿足初始化的條件,類就提前載入了,導致例項化提前,違背了需求。因此,為了達到延遲例項化[^注2]的目的,有兩種方法。其一就是換成懶漢式的寫法,不將單例的建立放在靜態成員的初始化中,避免了類載入時機帶來的影響。其二是仍然採用餓漢式,不過從設計上避免類的提前載入:*比如有觀點認為,從程式碼設計的角度看,單例的 Class 本來就不該擁有除了 getInst 方法以外任何能被訪問的靜態成員,Kotlin 就認為,你定義在 object 內部的變數都是和 object 有關的,而且 Kotlin 中不需要懶漢式的寫法,因為這門語言不存在上述提前載入的問題。

    說了這麼多關於單例模式,那麼靜態類呢。其實兩者無從比較,因為靜態類並不會去堆中例項化一個物件,如果只考慮堆記憶體的佔用的話,單例在靜態類面前並沒有所謂延遲載入的優勢。

  3. 兩者的效能問題。而從記憶體佔用來講,靜態方法和例項方法沒有區別!無論哪個例項,靜不靜態,方法只有一個,JVM 會在方法區儲存方法相關的資訊,時機也沒有區別。而靜態變數和例項變數只是分配的位置不同,前者在方法區,後者在堆中,並且因為類的生命週期是伴隨 JVM 的,前者永駐方法區,後者因為其所屬例項是靜態的,被類持有,所以在堆中也用遠不會被回收。從訪問速度來講,兩者並無明顯差別(本人木有做過驗證),但我有個小猜想,*類物件是分配在堆中的,它是方法區的入口,當通過它直接訪問方法區的靜態成員和通過它訪問方法區的單例再通過單例訪問堆中的例項成員,這個速度怕還是應該有區別的吧。*很多同學在討論這個問題時所說的單例的優缺點,根本不是相較與靜態類來說的,而是相較於非單例來說的,什麼單例節省資源咯、單例就是個記憶體洩漏咯。

  4. 從程式設計思想上,單例的出現並不是為了解決什麼高深複雜的效能之類的問題,它只是物件導向的一種體現,可以繼承擴充,可以實現介面,比較靈活(場景見得少,我對這個理解不深,有個例子是 Java.getRuntime 方法會根據 JVM 的不同,返回不同的例項)。靜態類更像是一個方法的集合,提供全域性的訪問而已, 比如大多數的 Util。靜態類不適合需要維護狀態的情形和比如對於資源的訪問。我看到一句話總結的很好,但我暫時只有感性的理解囧,可能是程式碼讀的少。

    靜態方法用來執行無狀態的一個完整操作,例項方法則相反,它通常是一個完整邏輯的一部分,並且需要維護一定的狀態值.

    現在理解下來,狀態可以簡單的認為就是需要維護的屬性,如果方法沒有對屬性的寫操作,那就說是無狀態的。**那為什麼靜態方法不適合做有狀態的操作呢?**Google 了一下,很多部落格都是搬的一句話,說靜態方法在維護狀態方面會導致許多狡猾的 BUG(黑人問號),不加以同步機制會存在競態衝突。但是單例模式維護狀態難道就不需要同步了??請大家不吝賜教

  5. 測試的問題, Mock。這條暫時沒有去了解。


很多自己的理解,如有疏漏錯誤,懇請補充指正。原文連結

[^注1]: 文章裡的內容都基於 JVM,更為嚴謹的做法應該是基於 DVM ,後續有時間會去調研一哈在類載入這個事情上 DVM 有木有什麼大的改動 [^注2]: 我個人覺得這個事情本來就應該叫做延遲例項化,而不應該叫做延遲載入。載入會與類載入搞混淆,類的載入(載入、連結、初始化這個大過程)是我們不可控的。

相關文章